# Design System — Token Reference

> Source of truth: `src/app.css`
> Last updated: 2026-03-27

All UI values must come from these tokens unless listed under [Intentional Raw Values](#intentional-raw-values).

---

## Spacing

4px grid. Token name = multiplier (`--sp-3` = 3 × 4px = 12px).

| Token | Value | Use for |
|-------|-------|---------|
| `--sp-0` | 0px | Explicit zero |
| `--sp-px` | 1px | Borders, hairlines |
| `--sp-0.5` | 2px | Micro-nudge (margin-top on meta text) |
| `--sp-1` | 4px | Tight gap, field label gap, small padding |
| `--sp-1.5` | 6px | Badge gap, icon gap, footer gap |
| `--sp-2` | 8px | Compact gap, button group gap, inner padding |
| `--sp-3` | 12px | Standard gap, row padding, list item gap |
| `--sp-4` | 16px | Card padding (mobile), section margin, tab margin |
| `--sp-5` | 20px | Card padding (desktop), module gap, sidebar gap |
| `--sp-6` | 24px | Large padding, overlay padding, page container |
| `--sp-7` | 28px | Primary card padding, section group margin |
| `--sp-8` | 32px | Page top padding, empty state, desktop grid gap |
| `--sp-10` | 40px | Large elements (avatar width), empty list padding |
| `--sp-12` | 48px | Empty state padding, large spacing |
| `--sp-16` | 64px | Reserved |
| `--sp-20` | 80px | Page bottom padding (scroll clearance) |

### Semantic spacing aliases

| Token | Resolves to | Use for |
|-------|-------------|---------|
| `--section-gap` | `--sp-7` (28px) | Gap between major page sections |
| `--card-pad` | `--sp-5` (20px) | Default module/card padding |
| `--card-pad-primary` | `--sp-7` (28px) | Hero/primary module padding |
| `--card-pad-secondary` | `--sp-4` (16px) | Compact card padding |
| `--row-gap` | `--sp-3` (12px) | Gap between list rows |
| `--module-gap` | `--sp-5` (20px) | Gap between dashboard modules |
| `--row-pad-y` | 14px | Vertical padding inside data rows (off-grid, intentional) |
| `--row-pad-x` | `--sp-4` (16px) | Horizontal padding inside data rows |
| `--inner-gap` | `--sp-3` (12px) | Gap between items within a row |

### Mobile overrides (≤768px)

| Token | Desktop | Mobile |
|-------|---------|--------|
| `--card-pad` | 20px | 16px |
| `--card-pad-primary` | 28px | 20px |
| `--row-pad-y` | 14px | 16px |
| `--section-gap` | 28px | 20px |

---

## Radius

| Token | Value | Use for |
|-------|-------|---------|
| `--radius-xs` | 4px | Tiny pills, skeleton placeholders, kbd hints |
| `--radius-sm` | 6px | Badges, chips, nav links, danger buttons |
| `--radius-md` | 8px | Buttons, inputs, tabs, entry rows, icon containers |
| `--radius` | 12px | Cards, modals, panels, main containers |
| `--radius-lg` | 16px | Hero cards, action cards, pill chips, ImmichPicker modal |
| `--radius-full` | 9999px | Circles, toggles, avatars |

---

## Elevation (Shadows)

Light and dark mode have separate values. Dark mode uses higher opacity.

| Token | Light | Use for |
|-------|-------|---------|
| `--shadow-xs` | `0 1px 2px rgba(0,0,0,0.03)` | Row hover, active tabs, inner elements |
| `--shadow-sm` | `0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.04)` | Secondary cards, inputs, budget tables |
| `--shadow-md` | `0 2px 6px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06)` | Standard card elevation (default `.module`) |
| `--shadow-lg` | `0 4px 12px rgba(0,0,0,0.06), 0 16px 40px rgba(0,0,0,0.1)` | Hero cards, primary modules, dropdowns |
| `--shadow-xl` | `0 8px 24px rgba(0,0,0,0.08), 0 24px 60px rgba(0,0,0,0.15)` | Modals, overlay panels |

**Legacy aliases**: `--card-shadow` → `--shadow-md`, `--card-shadow-sm` → `--shadow-sm`

---

## Colors

### Surfaces (3-layer depth)

| Token | Light | Dark | Layer |
|-------|-------|------|-------|
| `--canvas` | `#F5F6F8` | `#09090b` | Page background — everything sits on this |
| `--surface` | `#FFFFFF` | `#0f0f12` | Sidebars, panels, slide-out sheets |
| `--surface-secondary` | `#FAFAFB` | `#111114` | Input backgrounds, secondary panels |
| `--card` | `#FFFFFF` | `#161619` | Content containers, elevated with shadow |
| `--card-secondary` | `#FAFAFB` | `#111114` | Secondary cards, button backgrounds |
| `--card-hover` | `#f0f0f3` | `#1c1c20` | Row hover, interactive feedback |

### Borders

| Token | Light | Dark |
|-------|-------|------|
| `--border` | `rgba(0,0,0,0.06)` | `rgba(255,255,255,0.06)` |
| `--border-strong` | `rgba(0,0,0,0.1)` | `rgba(255,255,255,0.1)` |

### Text hierarchy

| Token | Light | Dark | Use for |
|-------|-------|------|---------|
| `--text-1` | `#1a1a1f` | `#fafafa` | Headings, names, amounts — read first |
| `--text-2` | `#4a4a55` | `#a1a1aa` | Body text, descriptions — read second |
| `--text-3` | `#6b6b76` | `#71717a` | Labels, metadata, captions — supporting |
| `--text-4` | `#b4b4bd` | `#3f3f46` | Placeholders, disabled, timestamps — background |

### Accent (indigo / blue)

| Token | Light | Dark | Use for |
|-------|-------|------|---------|
| `--accent` | `#4F46E5` | `#3b82f6` | Primary actions, links, active states |
| `--accent-bg` | `#EEF2FF` | `rgba(59,130,246,0.1)` | Icon wells, strong highlight backgrounds |
| `--accent-dim` | `rgba(79,70,229,0.06)` | `rgba(59,130,246,0.08)` | Subtle hover, selection backgrounds, focus rings |
| `--accent-border` | `rgba(79,70,229,0.10)` | `rgba(59,130,246,0.12)` | Accent-tinted borders |
| `--accent-focus` | `rgba(79,70,229,0.12)` | `rgba(59,130,246,0.15)` | Active states, selection bars |

### Semantic colors

| Token | Light | Dark | Use for |
|-------|-------|------|---------|
| `--success` | `#16A34A` | `#22c55e` | Positive values, income, completed |
| `--success-bg` | `#F0FDF4` | `rgba(34,197,94,0.1)` | Icon wells |
| `--success-dim` | `rgba(34,197,94,0.08)` | `rgba(34,197,94,0.08)` | Badge backgrounds |
| `--error` | `#DC2626` | `#ef4444` | Errors, issues, delete actions |
| `--error-bg` | `#FEF2F2` | `rgba(239,68,68,0.1)` | Icon wells |
| `--error-dim` | `rgba(239,68,68,0.08)` | `rgba(239,68,68,0.08)` | Badge backgrounds |
| `--warning` | `#d97706` | `#f59e0b` | Warnings, pending states |
| `--warning-bg` | `rgba(245,158,11,0.08)` | `rgba(245,158,11,0.1)` | Badge backgrounds |

### Overlay

| Token | Light | Dark | Use for |
|-------|-------|------|---------|
| `--overlay` | `rgba(0,0,0,0.3)` | `rgba(0,0,0,0.6)` | Modal backdrop, reading pane overlay |
| `--overlay-strong` | `rgba(0,0,0,0.5)` | `rgba(0,0,0,0.75)` | Heavy overlays |
| `--nav-bg` | `rgba(255,255,255,0.9)` | `rgba(15,15,18,0.9)` | Navbar blur background |

---

## Typography

| Token | Desktop | Mobile (≤768px) | Use for |
|-------|---------|-----------------|---------|
| `--text-xs` | 11px | 12px | Badges, pills, tiny counters |
| `--text-sm` | 13px | 15px | Labels, meta, captions, button text |
| `--text-base` | 14px | 16px | Body text, list items, inputs |
| `--text-md` | 15px | 17px | Card titles, important rows (16px+ avoids iOS zoom) |
| `--text-lg` | 17px | 18px | Section headers, modal titles |
| `--text-xl` | 22px | 22px | Page titles |
| `--text-2xl` | 28px | 26px | Hero headings |
| `--text-3xl` | 36px | 32px | Large hero numbers |

### Line heights

| Token | Value | Use for |
|-------|-------|---------|
| `--leading-tight` | 1.2 | Headings, hero numbers |
| `--leading-snug` | 1.35 | Card titles, compact text |
| `--leading-normal` | 1.5 | Body text |
| `--leading-relaxed` | 1.65 | Article content |
| `--leading-loose` | 1.8 | Long-form reading |

### Fonts

| Token | Value |
|-------|-------|
| `--font` | `'Inter', -apple-system, system-ui, sans-serif` |
| `--mono` | `'JetBrains Mono', ui-monospace, monospace` |

---

## Global Component Classes

Defined in `app.css`, usable in any component without local `<style>` duplication.

| Class | Description |
|-------|-------------|
| `.module` | Card container (bg, border, shadow, padding) |
| `.module.primary` | Hero card (more padding + elevation) |
| `.module.flush` | No padding (for flush content) |
| `.module-header` | Flex header row (title left, action right) |
| `.module-title` | Uppercase label |
| `.module-action` | Accent link → "View all →" |
| `.data-row` | Standard list item with hover + zebra striping |
| `.badge` + `.error/.success/.warning/.accent/.muted` | Semantic status badges |
| `.tab-bar` + `.tab` + `.tab-badge` | Pill-style tab navigation |
| `.section-label` | Uppercase group header |
| `.btn-primary` / `.btn-primary.full` | Primary action buttons |
| `.btn-secondary` | Secondary action buttons |
| `.btn-icon` | Square icon button (36×36) |
| `.input` | Standard text input |
| `.skeleton` | Shimmer loading placeholder |
| `.page` / `.page-header` / `.page-title` / `.page-greeting` | Page-level layout |
| `.app-surface` | Centered max-width container |

---

## Intentional Raw Values

These values exist outside the token system by design. Do not convert them.

### Non-scale spacing

Values that don't land on the 4px grid. Used for optical tuning where grid steps are too coarse.

| Value | Where | Why |
|-------|-------|-----|
| `1px` | `margin-top` on meta text, feed separators | Sub-pixel nudge for vertical alignment |
| `3px` | Badge padding-y, kbd padding | Optical centering within small elements |
| `5px` | Pill padding-y, chip padding | Between sp-1 (4px) and sp-1.5 (6px), tuned per element |
| `7px` | View-btn padding-y, sidebar gap | Between sp-1.5 (6px) and sp-2 (8px) |
| `9px` | Nav item padding-y, suggestion row padding-y | Between sp-2 (8px) and 10px |
| `10px` | Input padding-y, dropdown padding, various gaps | Common "comfortable touch" size, between sp-2 and sp-3 |
| `11px` | Entry row padding-y, toggle btn padding-bottom | Asymmetric optical alignment |
| `13px` | FAB action padding-y, list row padding | Between sp-3 (12px) and 14px |
| `14px` | Button padding-x, row padding-x, modal gap, field row gap | Most common off-grid value. Used as standard horizontal rhythm for interactive elements. Also `--row-pad-y` for data rows. |
| `15px` | Transaction row padding-y | Between sp-3.5 and sp-4, tuned for readability |
| `18px` | Detail header margin-bottom, AI guide padding-top | Between sp-4 (16px) and sp-5 (20px) |
| `22px` | Trip stats padding-x, modal body padding | Between sp-5 (20px) and sp-6 (24px) |

### Non-scale border-radius

| Value | Where | Why |
|-------|-------|-----|
| `9px` | Status segment control inner radius | Between radius-md (8px) and radius (12px) |
| `10px` | Event cards, photo thumbnails, notes, modals, unscheduled items, food rows | Heavily used "soft card" radius. Between radius-md (8px) and radius (12px). |
| `14px` | CommandPalette box, toggle track | Large interactive controls |
| `20px` | Bottom sheet top corners | Extra-round for sheet feel |
| `50%` | Circular elements (cover nav dots, meal-number, image-delete) | True circle, differs from radius-full on non-square elements |

### Data visualization colors

These are visual category identifiers, not semantic UI colors. They must remain consistent within their visualization set, independent of theme.

**Fitness macros:**
| Color | Hex | Use |
|-------|-----|-----|
| Protein | `#8B5CF6` | Purple macro bar |
| Carbs | `#F59E0B` | Amber macro bar |
| Fat | `#3B82F6` | Blue macro bar |

**Fitness meal weights:**
| Color | Background | Text | Meaning |
|-------|------------|------|---------|
| Heavy | `rgba(239,68,68,0.1)` | `#DC2626` | High-calorie meal |
| Moderate | `rgba(245,158,11,0.1)` | `#B45309` | Medium-calorie meal |
| Light | `rgba(34,197,94,0.1)` | `#15803D` | Low-calorie meal |

**Trip categories:**
| Color | Background | Text | Category |
|-------|------------|------|----------|
| Hotel | `rgba(168,85,247,0.1)` | `#a855f7` | Lodging |
| Restaurant | `rgba(249,115,22,0.1)` | `#f97316` | Food & dining |
| Hike | `rgba(34,197,94,0.15)` | `#16a34a` | Outdoor activity |
| Logistics | `rgba(161,161,170,0.1)` | `--text-3` | Transport, other |

**Other:**
| Color | Hex | Use |
|-------|-----|-----|
| Favorite star | `#F59E0B` | Star icon fill (reader, fitness) |
| AI badge | `rgba(59,130,246,0.1)` / `#3B82F6` | AI-logged entry indicator |
| Transfer pill | `rgba(59,130,246,0.1)` / `#3b82f6` | Budget transfer indicator |

### Glass & overlay effects

Applied to elements layered over images. Not theme-switchable because they depend on photo content, not UI surface.

| Value | Where |
|-------|-------|
| `rgba(0,0,0,0.7)..0.1` | Cover image gradient (trips) |
| `rgba(255,255,255,0.15)` | Cover nav button background |
| `rgba(255,255,255,0.3)` | Cover nav button hover |
| `rgba(0,0,0,0.3)` / `rgba(0,0,0,0.5)` | Cover share button / hover |
| `rgba(0,0,0,0.35)` | Modal overlays (trips), detail overlay (inventory) |
| `rgba(0,0,0,0.5)` | Image delete button, search saving overlay |

### Directional panel shadows

Non-standard shadow directions for slide-in panels and bottom sheets. Cannot use elevation tokens.

| Value | Where |
|-------|-------|
| `0 -8px 32px rgba(0,0,0,0.12)` | FAB bottom sheet (fitness, trips) |
| `-12px 0 40px rgba(0,0,0,0.12)` | Detail slide-in sheet (inventory) |
| `-6px 0 28px rgba(0,0,0,0.08)` | Reading pane (reader) |
| `-8px 0 32px rgba(0,0,0,0.1)` | Edit sheet (trips) |
| `8px 0 24px rgba(0,0,0,0.08)` | Mobile sidebar (reader) |

### Accent-tinted FAB shadows

Colored shadows for floating action buttons. Not in the elevation scale because they use accent color, not black.

| Value | Where |
|-------|-------|
| `0 8px 24px rgba(79,70,229,0.3)` | FAB resting (fitness) |
| `0 12px 32px rgba(79,70,229,0.4)` | FAB hover (fitness) |
| `0 6px 20px rgba(79,70,229,0.3)` | FAB resting (trips) |
| `0 8px 28px rgba(79,70,229,0.4)` | FAB hover (trips) |

### Near-match shadows

Shadows that are close to tokens but intentionally differ in blur radius or opacity.

| Value | Nearest token | Diff |
|-------|--------------|------|
| `0 2px 6px rgba(0,0,0,0.05)` | `--shadow-xs` | Larger blur, higher opacity |
| `0 1px 2px rgba(0,0,0,0.04)` | `--shadow-xs` | Opacity 0.04 vs 0.03 |
| `0 1px 4px rgba(0,0,0,0.04)` | `--shadow-xs` | Larger blur |
| `0 1px 4px rgba(0,0,0,0.08)` | `--shadow-xs` | Larger blur + opacity |
| `0 16px 48px rgba(0,0,0,0.15)` | `--shadow-xl` | Single layer vs dual |
| `0 1px 3px rgba(0,0,0,0.06)` | `--shadow-xs` | Higher opacity |
| `0 8px 24px rgba(0,0,0,0.12)` | `--shadow-md` | Higher opacity |
| `0 20px 60px rgba(0,0,0,0.15)` | `--shadow-xl` | Single layer |
| `0 20px 60px rgba(0,0,0,0.2)` | `--shadow-xl` | Single layer, higher opacity |
| `0 1px 3px rgba(0,0,0,0.15)` | `--shadow-sm` | Toggle thumb, much higher opacity |

---

## Adding a New App (Frontend Guide)

Step-by-step for adding a new app page to the platform.

### 1. Create the route

Create `src/routes/(app)/yourapp/+page.svelte`. Every app is a single self-contained file.

### 2. Page structure template

```svelte
<script lang="ts">
  import { onMount } from 'svelte';

  // -- Types --
  interface Item {
    id: string;
    name: string;
  }

  // -- State (Svelte 5 runes) --
  let loading = $state(true);
  let items = $state<Item[]>([]);
  let activeTab = $state<'all' | 'recent'>('all');
  let searchQuery = $state('');

  // -- Derived --
  const filtered = $derived(
    items.filter(i => i.name.toLowerCase().includes(searchQuery.toLowerCase()))
  );

  // -- API helper (scoped to this app's gateway prefix) --
  async function api(path: string, opts: RequestInit = {}) {
    const res = await fetch(`/api/yourapp${path}`, { credentials: 'include', ...opts });
    if (!res.ok) throw new Error(`${res.status}`);
    return res.json();
  }

  // -- Data mapper (normalize backend shape to UI interface) --
  function mapItem(raw: any): Item {
    return { id: raw.id || raw.Id, name: raw.name || raw.title || '' };
  }

  // -- Load --
  async function loadItems() {
    try {
      const data = await api('/items');
      items = (data.items || data).map(mapItem);
    } catch (e) { console.error('Failed to load items', e); }
  }

  onMount(async () => {
    await loadItems();
    loading = false;
  });
</script>

<div class="page">
  <div class="page-header">
    <h1 class="page-title">Your App</h1>
  </div>

  {#if loading}
    <div class="module"><div class="skeleton" style="height: 200px"></div></div>
  {:else}
    <div class="tab-bar">
      <button class="tab" class:active={activeTab === 'all'} onclick={() => activeTab = 'all'}>All</button>
      <button class="tab" class:active={activeTab === 'recent'} onclick={() => activeTab = 'recent'}>Recent</button>
    </div>

    <div class="module">
      <div class="module-header">
        <span class="module-title">Items</span>
        <button class="module-action" onclick={...}>View all &rarr;</button>
      </div>
      {#each filtered as item}
        <div class="data-row">
          <span style="color: var(--text-1)">{item.name}</span>
        </div>
      {:else}
        <p style="color: var(--text-3); padding: var(--card-pad)">No items found</p>
      {/each}
    </div>
  {/if}
</div>
```

### 3. Register in navigation

**`(app)/+layout.server.ts`** -- add ID to allApps:
```ts
const allApps = ['trips', 'fitness', 'inventory', 'budget', 'reader', 'media', 'yourapp'];
```

**`Navbar.svelte`** -- add link:
```svelte
{#if showApp('yourapp')}
  <a href="/yourapp" class="navbar-link" class:active={isActive('/yourapp')}>Your App</a>
{/if}
```

**`MobileTabBar.svelte`** -- add to primary tabs or the "More" sheet.

### 4. Key conventions

| Rule | Detail |
|------|--------|
| All state uses `$state()` | Never plain `let` for reactive values |
| Computed values use `$derived()` | For filtered lists, counts, conditions |
| API calls go through `/api/yourapp/*` | Gateway proxies to backend |
| `credentials: 'include'` on every fetch | Cookie-based auth |
| Map raw API data through `mapX()` | Normalize backend shapes to clean interfaces |
| No shared stores or context | Each page is self-contained |
| No separate `+page.ts` load function | Data loads in `onMount` |
| Types defined inline in script | No shared type files |

### 5. UI component classes to use

| Class | When |
|-------|------|
| `.page` / `.page-header` / `.page-title` | Page-level layout |
| `.module` / `.module.primary` / `.module.flush` | Card containers |
| `.module-header` / `.module-title` / `.module-action` | Card headers |
| `.data-row` | List items with hover + zebra |
| `.badge` + `.error/.success/.warning/.accent/.muted` | Status indicators |
| `.tab-bar` + `.tab` | Pill-style tabs |
| `.btn-primary` / `.btn-secondary` / `.btn-icon` | Buttons |
| `.input` | Text inputs |
| `.skeleton` | Loading placeholders |
| `.section-label` | Uppercase group header |

### 6. Styling rules

- All colors from tokens: `var(--text-1)`, `var(--accent)`, `var(--card)`, etc.
- All spacing from tokens: `var(--sp-3)`, `var(--card-pad)`, `var(--row-gap)`, etc.
- All radii from tokens: `var(--radius)`, `var(--radius-md)`, etc.
- All shadows from tokens: `var(--shadow-md)`, `var(--shadow-lg)`, etc.
- All font sizes from tokens: `var(--text-sm)`, `var(--text-base)`, etc.
- Never use raw `#hex` colors, raw `px` spacing, or raw shadows unless listed in the Intentional Raw Values section above
- Dark mode is automatic via CSS custom properties — no manual `prefers-color-scheme` needed
