# Echo Dashboard — Design System

This document is the source of truth for visual decisions across the Echo
Dashboard (port 8088, served at `/echo/`). Tokens live in
`dashboard/static/tokens.css`. Page-level CSS is in `common.css` and per-page
`<style>` blocks. **Pages must include `tokens.css` before `common.css`.**

---

## Theme

- **Default**: dark. Background `--bg-base: #13131a` (near-black neutral).
- **Light theme**: opt-in via `<html data-theme="light">`. Light tokens override
  the dark palette in the same `:root`-equivalent block.
- **Toggle**: header `.theme-toggle` button — persisted in `localStorage`.

Surfaces are translucent overlays on `--bg-base`, never solid greys, so
elevation reads consistently against future backgrounds.

---

## Color tokens

### Surfaces (dark)

| Token | Value | Use |
|---|---|---|
| `--bg-base` | `#13131a` | App background |
| `--bg-surface` | `rgba(255,255,255,0.12)` | Cards, panels, inputs |
| `--bg-surface-hover` | `rgba(255,255,255,0.16)` | Hover state on surfaces |
| `--bg-surface-active` | `rgba(255,255,255,0.20)` | Pressed / active surfaces |
| `--bg-elevated` | `rgba(255,255,255,0.14)` | Selects, popovers |
| `--header-bg` | `rgba(19,19,26,0.95)` | Sticky header backdrop |

### Text

| Token | Value | Use |
|---|---|---|
| `--text-primary` | `#ffffff` | Headings, key labels |
| `--text-secondary` | `#f5f5f5` | Body copy |
| `--text-muted` | `#e5e5e5` | Meta, timestamps, captions |

### Accent + borders

| Token | Value | Use |
|---|---|---|
| `--accent` | `#3b82f6` | Primary buttons, focus, links |
| `--accent-hover` | `#2563eb` | Hover on `--accent` |
| `--accent-subtle` | `rgba(59,130,246,0.2)` | Active nav background |
| `--border` | `rgba(255,255,255,0.3)` | Card / input outline |
| `--border-focus` | `rgba(59,130,246,0.7)` | Card hover, input focus |

### Semantic state

| Token | Value | Meaning |
|---|---|---|
| `--success` | `#22c55e` | OK, saved, healthy |
| `--warning` | `#eab308` | Caution, soft fail |
| `--error` | `#ef4444` | Hard fail, destructive |

### Status palette (workflow states)

These drive the `.status-pill[data-status]` system on workspace cards.

| Token | Value | State name |
|---|---|---|
| `--status-running` | `rgb(34, 197, 94)` | `running-ralph`, `running-manual` |
| `--status-blocked` | `rgb(245, 158, 11)` | `blocked` |
| `--status-failed` | `rgb(239, 68, 68)` | `failed` |
| `--status-complete` | `rgb(156, 163, 175)` | `complete` |
| `--status-idle` | `var(--text-muted)` | `idle` |
| `--status-planning` | `rgb(167, 139, 250)` | `planning` *(new)* |
| `--status-pending` | `rgb(96, 165, 250)` | `pending` *(new)* |
| `--status-approved` | `rgb(234, 179, 8)` | `approved` *(new)* |

---

## Typography

- **Sans**: `'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`
  — self-hosted woff2 at `/echo/static/fonts/inter-{400,500,600,700}.woff2`.
- **Mono**: `'JetBrains Mono', 'Fira Code', ui-monospace, monospace` — for
  logs, code blocks, slugs, IDs. Loaded by browser if present (not bundled).

### Size scale

| Token | rem | px @ 16px |
|---|---|---|
| `--text-xs` | 0.75 | 12 |
| `--text-sm` | 0.875 | 14 |
| `--text-base` | 1 | 16 |
| `--text-lg` | 1.125 | 18 |
| `--text-xl` | 1.25 | 20 |

### Weights

400 (body), 500 (medium emphasis), 600 (headings, button labels),
700 (rare — page titles only). No 800/900.

---

## Spacing — 8px grid

All padding, margin, and gap values use these tokens. No hard-coded pixels.

| Token | px |
|---|---|
| `--space-1` | 4 |
| `--space-2` | 8 |
| `--space-3` | 12 |
| `--space-4` | 16 |
| `--space-5` | 20 |
| `--space-6` | 24 |
| `--space-8` | 32 |
| `--space-10` | 40 |

---

## Border radius

| Token | px | Use |
|---|---|---|
| `--radius-sm` | 4 | Tags, micro-pills |
| `--radius-md` | 8 | Buttons, inputs |
| `--radius-lg` | 12 | Cards, modals, panels |
| `--radius-full` | 9999 | Status pills, badges, avatars |

---

## Buttons

All buttons share `.btn` (8px radius, 14px font, 8/16 padding,
`--transition-fast`).

| Variant | Class | Surface | Text | Use |
|---|---|---|---|---|
| Primary | `.btn-primary` | `--accent` | white | The one CTA per row |
| Secondary | `.btn-secondary` | `--bg-surface` + `--border` | `--text-secondary` | Side actions |
| Ghost | `.btn-ghost` | transparent | `--text-secondary` | Tertiary, destructive-soft |
| Danger | `.btn-danger` | `--error` | white | Stop, delete, irreversible |

Disabled state: `opacity: 0.5; cursor: not-allowed;`. Never grey out by
swapping colors — keep variant identity.

---

## Card component (`.project-card`)

- `border-radius: var(--radius-lg)` (12px)
- `background: var(--bg-surface)`
- `border: 1px solid var(--border)`
- `padding: var(--space-5)`
- `transition: border-color var(--transition-base)`
- **Hover**: `border-color: var(--border-focus)` (blue glow). No surface
  brightening — border-only hover keeps the grid calm.

---

## Status pill system

A `.status-pill` is a `--radius-full` chip placed on every project card. It
encodes the current workflow state via `data-status="<state>"`.

### Visual recipe

- **Background**: state color at **18% alpha** (`color-mix(in srgb, var(--status-X) 18%, transparent)` or precomputed `rgba(...)`).
- **Text**: solid state color (full alpha).
- **Border**: 1px state color at 30% alpha.
- **Padding**: `var(--space-1) var(--space-3)` — slim.
- **Font**: `var(--text-xs)`, weight 500.

### Pulse-dot

Active states render a 6px CSS-shape circle that pulses (no SVG, no emoji).

```css
.status-pill::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; margin-right: var(--space-2);
}
.status-pill[data-status="running-ralph"]::before,
.status-pill[data-status="running-manual"]::before,
.status-pill[data-status="planning"]::before {
  animation: pulse-dot 1.6s ease-in-out infinite;
}
@keyframes pulse-dot { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
```

### State matrix

| `data-status` | Color token | Pulse | Label |
|---|---|---|---|
| `running-ralph` | `--status-running` | yes | Ralph running |
| `running-manual` | `--status-running` | yes | Manual run |
| `planning` | `--status-planning` | yes | Planning |
| `approved` | `--status-approved` | no | Approved |
| `pending` | `--status-pending` | no | Pending |
| `blocked` | `--status-blocked` | no | Blocked |
| `failed` | `--status-failed` | no | Failed |
| `complete` | `--status-complete` | no | Complete |
| `idle` | `--status-idle` | no | Idle |

---

## BUTTONS_FOR_STATE matrix

Each project card surfaces ≤3 actions, ordered Primary / Secondary / Ghost.
The renderer picks the row matching `data-status`.

| State | Primary | Secondary | Ghost |
|---|---|---|---|
| `running-ralph` | Stop Ralph (danger) | Logs | PRD |
| `running-manual` | Stop (danger) | Open server | Logs |
| `planning` | Continue chat | — | Cancel |
| `approved` | — | Unapprove | Plan |
| `pending` | Approve | Plan with Echo | Cancel |
| `blocked` | View logs | Resume | — |
| `failed` | View logs | Retry | Rollback |
| `complete` | View plan | — | Run again |
| `idle` | Run Ralph | — | Delete |

Rules:
- **Stop / Delete** are always `.btn-danger`, never primary blue.
- A dash (`—`) means render nothing (no placeholder, no greyed-out slot).
- The Primary slot is the default action when the card is keyboard-focused
  and Enter is pressed.

---

## Toast taxonomy

Toasts appear top-right, stack vertically, dismiss after 4s (errors: 8s).
**Five types**, distinguished by a 3px colored left bar — no emoji, no icon
fill. Body uses `--text-primary` on `--bg-surface`.

| Type | Bar color | Use |
|---|---|---|
| `success` | `--success` | Saved, approved, deployed |
| `info` | `--accent` | Neutral confirmation |
| `warning` | `--warning` | Soft fail, retried |
| `busy` | `--status-planning` | Long-running op started |
| `error` | `--error` | Hard fail, action required |

Toast renderer is shared across pages and reads from a single global
`window.showToast(type, msg)` helper.

---

## SSE indicator

Top-right of pages with a live stream (workspace, ralph). Three states
indicated via a CSS-shape pulse-dot — never an emoji.

| State | Dot color | Label | Pulse |
|---|---|---|---|
| Live | `--success` | "Live" | yes |
| Polling | `--warning` | "Polling" | no |
| Offline | `--error` | "Offline" | no |

Uses the same `.pulse-dot` 6px CSS shape as `.status-pill::before`. The dot
sits before the label, both inside a tiny `.sse-indicator` chip on
`--bg-surface`.

---

## Modal pattern

Used for the planning chat, PRD viewer, log tail, propose-feature form.

- **Overlay**: full viewport, `background: rgba(0,0,0,0.6)`,
  `backdrop-filter: blur(4px)`, `display: flex` centered.
- **Container** (`.modal`): `--radius-lg`, `--bg-base`, `--border`,
  max-width 720px, max-height 80vh, scroll on overflow.
- **Header / Footer**: 1px border separators using `--border`.
- **Focus trap**: first focusable element gets focus on open; Tab cycles
  inside the modal.
- **ESC**: closes — but if the modal has unsaved input, prompt
  "Discard changes?" before closing. Click on overlay = same behavior.
- **Mobile (≤640px)**: full-screen takeover. Header / footer stick; body
  scrolls. Implemented in `tokens.css` via the shared `@media (max-width:640px)`
  block.

---

## No-emoji rule

**No emoji anywhere in the dashboard.** This is a hard rule, not a
preference.

- Buttons are **text-only**. No leading/trailing emoji decoration.
- Status indicators use **CSS-shape colored dots** (`.pulse-dot`,
  `.status-pill::before`) — never `🟢 ⏱ 🛑 ✅` etc.
- The login monogram is the **letter `E`** rendered in Inter 700 inside a
  square with `--accent` background. Not an emoji, not an SVG logo.
- Where icons are needed (nav, action buttons), use **Lucide-style stroke
  SVGs inlined** — `stroke: currentColor`, `fill: none`, `stroke-width: 2`,
  `stroke-linecap: round`, `stroke-linejoin: round`. Never use emoji as a
  substitute for an icon.

This rule keeps the UI legible across themes, scales correctly at all sizes,
and avoids OS-dependent rendering (Apple, Twemoji, Noto all draw the same
emoji differently).

---

## Pages that include this system

Every dashboard page (`index.html`, `workspace.html`, `ralph.html`, `notes.html`,
`habits.html`, `files.html`, `login.html`) **must** include in `<head>`:

```html
<link rel="stylesheet" href="/echo/static/tokens.css">
<link rel="stylesheet" href="/echo/common.css">
```

In that order — tokens first so `common.css` and per-page styles can resolve
the variables.
