diff --git a/docs/DESIGN-SYSTEM.md b/docs/DESIGN-SYSTEM.md
new file mode 100644
index 0000000..4ae4fa6
--- /dev/null
+++ b/docs/DESIGN-SYSTEM.md
@@ -0,0 +1,246 @@
+# HNF Stack — Design System
+
+## Visual Language
+
+The stack uses a **split chrome / content** approach:
+
+- **Sidebar / shell chrome** — dark navy (`#1a1a2e`). This is the brand anchor. It stays dark regardless of the content area.
+- **Content area** — light grey body (`#f4f5f7`), white cards with subtle shadows. Readable for data-heavy pages and matches the aesthetic of the Kitchen Flash app that preceded this stack.
+- **Standalone login screens** — full-screen dark (`#0f0f20` body, `#1a1a2e` card). Intentional contrast; feels like signing into something, not just another content page.
+
+The gold accent (`#c9a84c`) is used sparingly: active nav indicator, primary action buttons, pinned/flagged items, update badges. It reads as the hotel brand colour on both dark and light surfaces.
+
+---
+
+## CSS Variables
+
+Every app in the stack must include this `:root` block verbatim in its `index.css`. Do **not** invent new colour values — add a variable here if the palette genuinely needs extending.
+
+```css
+:root {
+ /* ── Shell chrome (dark) ── */
+ --navy: #1a1a2e; /* sidebar background, dark login card */
+ --navy-dark: #0f0f20; /* login page background */
+ --gold: #c9a84c; /* accent: active nav, primary buttons, badges */
+ --gold-light: #e8c96d; /* gold hover/highlight variant */
+ --surface: rgba(255,255,255,0.07); /* active nav item background */
+ --surface-2: rgba(255,255,255,0.08); /* sidebar dividers / borders */
+ --text: rgba(255,255,255,0.88); /* sidebar / dark-bg text */
+ --text-muted: rgba(255,255,255,0.48); /* sidebar muted text */
+
+ /* ── Content area (light) ── */
+ --body-bg: #f4f5f7; /* page background */
+ --card-bg: #ffffff; /* card / panel background */
+ --card-border: #e4e8ee; /* card border, input border */
+ --text-dark: #1e293b; /* primary text on light backgrounds */
+ --text-mid: #64748b; /* muted / secondary text on light backgrounds */
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.07), 0 1px 2px rgba(0,0,0,0.04);
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.08);
+
+ /* ── Shared ── */
+ --danger: #dc2626; /* error states, destructive actions */
+ --radius: 10px; /* standard border radius */
+ --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+}
+
+body {
+ background: var(--body-bg);
+ color: var(--text-dark);
+ font-family: var(--font);
+}
+```
+
+---
+
+## Colour Usage Guide
+
+| Situation | Variable |
+|---|---|
+| Page / content background | `--body-bg` |
+| Card / panel background | `--card-bg` |
+| Card / input border | `--card-border` |
+| Primary text (light bg) | `--text-dark` |
+| Secondary / muted text (light bg) | `--text-mid` |
+| Primary text (dark bg / sidebar) | `--text` |
+| Muted text (dark bg / sidebar) | `--text-muted` |
+| Primary action button | `background: var(--gold); color: var(--navy)` |
+| Destructive action | `var(--danger)` |
+| Success state | `#16a34a` (green-600) |
+| Warning / running state | `#d97706` (amber-600) |
+| Status badge — success | `background: #dcfce7; color: #16a34a` |
+| Status badge — failed | `background: #fee2e2; color: var(--danger)` |
+| Status badge — running | `background: #fef9c3; color: #ca8a04` |
+| Terminal / code output | `background: #1e293b; color: #94a3b8` |
+
+---
+
+## Icons
+
+Use **Lucide React** (`lucide-react` npm package) for all icons. Do not use emoji as functional UI elements.
+
+```bash
+npm install lucide-react
+```
+
+```tsx
+import { ChefHat, ClipboardList } from 'lucide-react'
+
+
+```
+
+**Conventions:**
+- `size`: 14–16px for inline/nav icons, 20–28px for tile/feature icons
+- `strokeWidth`: always `1.75` (the default is 2, which reads slightly heavy)
+- Nav icons inherit `color` from the parent element (let CSS handle active state)
+- App tile icons use the app's `theme_color` from the DB
+
+**App icon registry** (Lucide name → app slug):
+
+| App | Lucide name |
+|---|---|
+| Noticeboard | `ClipboardList` |
+| Kitchen Flash | `ChefHat` |
+| Cash Up | `Banknote` |
+| Housekeeping | `BedDouble` |
+| Forecasting | `TrendingUp` |
+| Rate Scraper | `Tag` |
+
+Stored in `apps.icon` column in the auth DB. The portal renders them via `AppIcon` component (`portal/src/components/AppIcon.tsx`).
+
+---
+
+## Typography
+
+No custom typeface — system font stack only. This avoids a font load and looks native on every device.
+
+| Use | Size | Weight |
+|---|---|---|
+| Section heading | `0.95rem` | `600` |
+| Card title | `0.88–0.95rem` | `600` |
+| Body text | `0.875rem` | `400` |
+| Small / meta | `0.72–0.78rem` | `400` |
+| Label / tag | `0.65rem` | `500–700`, uppercase, `letter-spacing: 0.04em` |
+| Monospace (hashes, IDs) | `0.72rem`, `font-family: monospace` | `400` |
+
+---
+
+## Card Pattern
+
+Every content card follows this pattern:
+
+```tsx
+
+```
+
+Use `borderLeft: '3px solid
'` to add a status accent stripe on the left edge. Use `var(--shadow-md)` on hover.
+
+---
+
+## Input Pattern
+
+```tsx
+const inputStyle: React.CSSProperties = {
+ background: 'var(--body-bg)',
+ border: '1px solid var(--card-border)',
+ borderRadius: '7px',
+ color: 'var(--text-dark)',
+ padding: '0.6rem 0.75rem',
+ fontSize: '0.9rem',
+ width: '100%',
+ outline: 'none',
+}
+```
+
+On focus, add `border-color: var(--gold)` via CSS or inline `onFocus`.
+
+---
+
+## Button Patterns
+
+**Primary action** (e.g. submit, open, update):
+```tsx
+style={{
+ background: 'var(--gold)', color: 'var(--navy)',
+ border: 'none', borderRadius: '7px',
+ padding: '0.45rem 1rem', fontSize: '0.82rem', fontWeight: 600,
+}}
+```
+
+**Secondary / ghost** (e.g. refresh, cancel):
+```tsx
+style={{
+ background: 'var(--card-bg)', color: 'var(--text-mid)',
+ border: '1px solid var(--card-border)', borderRadius: '7px',
+ padding: '0.35rem 0.9rem', fontSize: '0.8rem',
+}}
+```
+
+**Icon button** (e.g. pin, delete):
+```tsx
+style={{
+ background: 'var(--body-bg)', border: '1px solid var(--card-border)',
+ borderRadius: '6px', padding: '0.3rem',
+ color: 'var(--text-mid)',
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
+}}
+```
+
+---
+
+## Hotel Name
+
+The hotel name is injected at build time via a Vite env var:
+
+```
+VITE_HOTEL_NAME=Number Four at Stow
+```
+
+Set in `/opt//.env` on the server before rebuilding. Reference it in any component with:
+
+```tsx
+{import.meta.env.VITE_HOTEL_NAME}
+```
+
+Each app's `Dockerfile` must declare it as a build arg with the default:
+
+```dockerfile
+ARG VITE_HOTEL_NAME="Number Four at Stow"
+ENV VITE_HOTEL_NAME=$VITE_HOTEL_NAME
+```
+
+And `docker-compose.yml`:
+
+```yaml
+build:
+ context: .
+ args:
+ VITE_HOTEL_NAME: ${VITE_HOTEL_NAME:-Number Four at Stow}
+```
+
+Add `src/vite-env.d.ts` so TypeScript knows about it:
+
+```ts
+///
+interface ImportMetaEnv { readonly VITE_HOTEL_NAME: string }
+interface ImportMeta { readonly env: ImportMetaEnv }
+```
+
+---
+
+## App Checklist (new app)
+
+- [ ] Copy the full `:root` CSS block above into `src/index.css`
+- [ ] Install `lucide-react`, pick an icon, add it to `apps.icon` in auth DB
+- [ ] Add `VITE_HOTEL_NAME` build arg to `Dockerfile` and `docker-compose.yml`
+- [ ] Add `src/vite-env.d.ts`
+- [ ] Set `base: '/your-path/'` in `vite.config.ts`
+- [ ] All API calls use the base path prefix (e.g. `/your-path/api/...`)
+- [ ] Standalone login screen: explicit `background: var(--navy-dark)` on outer container
+- [ ] Content area: `--body-bg` / `--card-bg` / `--card-border` / `--text-dark` / `--text-mid`
+- [ ] No hardcoded colour hex values in component files — variables only