# Design System — ECHO (Threat Intel)

> **Single source of truth**
> - Spec (canonical): [`uploads/DESIGN.md`](./uploads/DESIGN.md)
> - CSS variables: [`echo-tokens.css`](./echo-tokens.css)
> - Portable JSON: [`design-tokens.json`](./design-tokens.json)
> - Interactive board: [`design-preview.html`](./design-preview.html)
> - Enforcer: `node echo-lint.mjs`

## First principles

1. **Monochrome at rest.** The base is a neutral zinc ramp. At rest the screen is grayscale.
2. **Color is a signal, never decoration.** Hue appears only where it carries meaning: a threat tier, a priority, an urgent action. If it isn't a signal, it's gray.
3. **Two-level text.** Exactly two text brightnesses. No dim/faint tier — dim text is dangerous when someone is scanning fast.
   - **Level 1 — `#fafafa`** (`--foreground`): threat names, values, headings, tier labels.
   - **Level 2 — `#a1a1aa`** (`--muted-foreground`): descriptions, captions, meta.
4. **One brand thread.** Iris appears only on active/selected. It never competes with a signal.
5. **Living, not static.** Motion communicates state, never ornaments it.

## Color

Canonical ship value is **hex / rgba**. Source table: `uploads/DESIGN.md`.

### Neutral base (monochrome)

| Token | Hex | Role |
|-------|-----|------|
| `--background` | `#09090b` | App canvas (zinc-950) |
| `--sidebar` | `#0b0b0e` | Sidebar canvas |
| `--card` / `--popover` | `#161619` | Card + surface |
| `--muted` | `#1c1c20` | Wells, input fills |
| `--secondary` | `#232327` | Secondary surface, hover |
| `--accent` | `#26262b` | Stronger hover |
| `--border` | `rgba(255,255,255,0.09)` | Hairlines, dividers, card outlines |
| `--foreground` | `#fafafa` | Text — Level 1 |
| `--muted-foreground` | `#a1a1aa` | Text — Level 2 |
| `--foreground-bright` | `#d4d4d8` | Bright icon / hover / nav chrome — not a 3rd body tier |
| `--dim` | `#71717a` | Rest switch — mono meta |
| `--dim-2` | `#52525b` | Rest switch — faintest / non-text |
| `--primary` | `#fafafa` | Primary action fill (monochrome CTA) |
| `--primary-foreground` | `#09090b` | Text on primary fill |

Dark theme only — **inverted zinc ramp** (light zinc = ink, dark zinc = surfaces). No light theme.

### shadcn bridge (aliases)

Same hexes, extra names so shadcn/ui doesn't fall back to defaults. Source: [`uploads/globals.css`](./uploads/globals.css).

| shadcn token | Points at |
|---|---|
| `--popover` | `--card` |
| `--card-foreground` / `--popover-foreground` / `--secondary-foreground` / `--accent-foreground` | `--foreground` (L1) |
| `--destructive` | `--critical` |
| `--input` | `rgba(255,255,255,0.14)` |
| `--ring` / `--sidebar-ring` / `--sidebar-primary` | `--brand` (Iris) |
| `--chart-1`…`5` | mono zinc series `#fafafa` → `#3f3f46` |
| `--sidebar-foreground` | `var(--foreground-bright)` (nav chrome only) |
| `--sidebar-accent` | `--muted` |

**Rule:** one hex, many names — never two hexes for the same job. Tier badges still use `--critical` / `--elevated` / `--guarded` by name.

### Signal — meaning only, always paired with icon / label

| Tier | Tokens | Value | Contrast |
|---|---|---|---|
| Critical (Crimson) | `--critical` + `-tint` `-line` | `#ef4444` | 4.6 / 4.1 — use ≥600wt / ≥16px on cards |
| Elevated (Saffron) | `--elevated` + `-tint` `-line` | `#ffb929` | 11.5 / 10.5 · AAA |
| Guarded (Emerald) | `--guarded` + `-tint` `-line` | `#16a34a` | 5.9 / 5.4 · AA |

### Brand — active / selected only

| Token | Value | Note |
|---|---|---|
| `--brand` (Iris) | `#6a5ed9` | fills & rings only — **never text** (3.9:1) |
| `--brand-foreground` | `#a79ef2` | Iris *text* on dark · 8.9:1 · AAA |
| `--brand-tint` / `--brand-line` | `0.14` / `0.32` alpha | active fill & border |
| `--ring` | `#6a5ed9` | focus ring (= `--brand`) |

## Typography

| Role | Token | Size | Weight | Usage |
|---|---|---|---|---|
| display | `--text-display` | 28px | 600 | resolution / hero numerics |
| h1 | `--text-h1` | 16px | 600 | section heading |
| body | `--text-body` | 13.5px | 400 | analyst copy |
| meta | `--text-meta` | 11.5px | 400 | captions, secondary meta |
| mono | `--text-mono` | 15px | 600 · `tabular-nums` | metrics |
| label | `--text-label` | 10px | 600 · `0.18em` tracking | caps group labels |

Families: `--font-sans` (Inter) · `--font-mono` (JetBrains Mono). Self-hosted via `echo-fonts.css`.

## Spacing

4px base — `--sp-1` … `--sp-8` → `4 · 8 · 12 · 16 · 20 · 24 · 32 · 48`. No arbitrary increments.

## Radius

| Token | Value | Usage |
|---|---|---|
| `--r-micro` | 4px | carets, bars, kbd |
| `--r-chip` | 9px | nav items, chips |
| `--r-btn` | 10px | buttons |
| `--r-tile` | 11px | inset tiles |
| `--r-well` | 12px | wells / grouped panels |
| `--r-card` | 14px | cards / popovers |
| `--r-badge` | 999px | pills / dots |

**`13` and `16` are off-scale** and flagged by the linter.

## Layout

| Token | Value |
|---|---|
| `--sidebar-width` | 236px |
| `--header-height` | 60px |
| `--content-max` | 1360px |

## Components

- **Buttons** — one primary per view (`--foreground` fill). `--critical` fill for escalate. Outline / ghost for secondary. **Iris never on a button.** No 1px resting border.
- **Cards** — `--card` surface, `--border` hairline, `--r-card`. Inset tiles use `--card-inset` + `--r-tile`. Never nest a card in a card.
- **Tier badges** — pill, `tint` fill + solid dot + label. Never color-only.
- **Inputs** — `--muted` fill, `--border`, focus ring `--ring`.
- **Nav** — single source: `echo-chrome.js` (do not hardcode sidebars).

## Motion

| Token | Value | When |
|---|---|---|
| `--motion-micro` | 120ms | micro feedback |
| `--motion-state` | 200ms | hover / open / close |
| `--motion-entrance` | 550ms | enter |
| `--motion-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | entrance / rise |
| `--motion-ease-state` | `cubic-bezier(0.4, 0, 0.2, 1)` | state changes |

Named keyframes: `echo-pulse` (live), `echo-ripple` (threat origin), `echo-rise` (entrance), `echo-caret` (reasoning), `echo-scan` (scanning). Respect `prefers-reduced-motion`.

## Do / Don't

**Do**
- Reference tokens (`var(--…)`); when adding a gray, add a ramp step.
- Pair every signal color with an icon or label.
- Use `--brand-foreground` for Iris text; `--brand` only for fills / rings.
- Keep nav in `echo-chrome.js`.
- Use only `--foreground` and `--muted-foreground` for text.
- Keep the board grayscale **and** ~2 ink scales dimmer at rest (`brightness(0.64)`); hover / alert restores authored luminance + hue.

**Don't**
- Inline new hex values in screens.
- Use Iris on buttons or as decoration.
- Use `--dim` / `--dim-2` / `--foreground-bright` as body copy — they are companions (rest / chrome), not a third body tier.
- Show signal hue at rest without hover/alert.
- Use radius `13` or `16`.
- Animate for ornament — every motion must communicate state.

## How to extend

1. Edit `echo-tokens.css` (CSS is law for the app).
2. Mirror the change in `design-tokens.json`.
3. Update this spec if a principle or usage rule changes.
4. Refresh `design-preview.html` if a new group appears.
5. Run `node echo-lint.mjs` — it re-reads `@echo-*` directives from the CSS.
