# ReachOut Design System

**Canonical reference for anyone (humans and agents) building ReachOut interfaces.**
Staging stylebook: **<https://stage.usereachout.com/design>** (publicly accessible, no auth).
Tokens: `apps/dashboard/src/styles/tokens.css` · Component library:
`apps/dashboard/src/styles/design-system.css` · Chart renderers:
`apps/dashboard/src/lib/charts/`.

> ReachOut is marketing automation run by AI agents. The interface is a precision
> instrument: **Fira Code renders every figure the machine produces, Fira Sans
> carries the brand voice in headings, Inter keeps human prose readable.** When in
> doubt, make the data louder and the chrome quieter.

---

## 1. Principles

1. **Data first.** Tables and charts are the hero content. They get the most careful
   typography (tabular mono figures), the calmest chrome, and the first layout
   decisions.
2. **Quiet chrome, loud data.** Cards carry a 1px border and never a shadow. Color is
   spent only where it means something: accent for actions, semantic hues for signed
   values, the accent ramp for intensity.
3. **The machine speaks mono.** Every number, ID, date, percentage, MCP tool name and
   log line is Fira Code with tabular numerals (`font-feature-settings: 'tnum'`).
4. **Motion with purpose.** Subtle, fast, functional. Entrances rise+fade, state
   changes take 80–220ms, charts draw once on mount. Only `transform` and `opacity`
   animate. Everything is disabled under `prefers-reduced-motion`.

## 2. File map

| File | What it owns |
|---|---|
| `src/styles/tokens.css` | Every design token (`:root`, `html.dark`, `html.light`). The only file allowed to contain literal `oklch()`/hex values. |
| `src/styles/global.css` | Imports `tokens.css`, then base typography + shell (rail/topbar/stream) + legacy component classes. |
| `src/styles/design-system.css` | The canonical component library (this document). Imported by all layouts **after** `global.css`. |
| `src/styles/public-refresh.css` | Public/auth surface composition, overflow and responsive guardrails. It consumes canonical tokens; it does not define them. |
| `src/styles/studio-refresh.css` | Studio shell and legacy-adoption bridge: route hierarchy, responsive rail/panels, overflow safety and semantic harmonization. |
| `src/components/widget-refresh.css` | Tokenized presentation and responsive behavior shared by hydrated Contacts, Settings and Template React islands. Runtime geometry alone may remain inline. |
| `src/components/design/stylebook-refresh.css` | Scoped content-safety, focus and responsive rules for the live stylebook and its specimens. |
| `src/components/demo/demo-refresh.css` | Scoped interaction, table and mobile guardrails for the deterministic Studio demo routes. |
| `src/lib/charts/palette.ts` | `chartPalette()`, `accentRamp()`, `accentRampScale()`, `semanticColors()`, `isDarkTheme()`, `onThemeChange()`. |
| `src/lib/charts/line-chart.ts` | `renderLineChart(host, opts)` — trends. |
| `src/lib/charts/bar-chart.ts` | `renderBarChart(host, opts)` — grouped/stacked categories. |
| `src/lib/charts/donut-chart.ts` | `renderDonutChart(host, opts)` — share of total. |
| `src/lib/charts/heatmap.ts` | `renderHeatmap(host, opts)` — intensity matrix. |
| `src/lib/charts/sparkline.ts` | `renderSparkline(host, opts)` — inline trends. |
| `src/lib/charts/d3-theme.ts` | `themeColors()`, shared `.d3-chart-tooltip` singleton, formatters. |
| `src/pages/design/*` | The live stylebook (`/design`, `/design/tokens`, `/design/components`, `/design/data`, `/design/patterns`, `/design/examples/*`). |
| `src/pages/design/tokens.json.ts` | `GET /design/tokens.json` — machine-readable token manifest parsed from `tokens.css` at build time (the agent/API surface). |
| `src/lib/design/modal.ts` | Accessible modal controller: initial focus, focus trap, Escape/backdrop close, background isolation and focus restoration. |

## 3. Tokens

All tokens are CSS custom properties, themed per `html.dark` / `html.light`. Never
hardcode a color, radius, shadow or font-size — reference the token.

### 3.1 Typography

| Token | Value | Use |
|---|---|---|
| `--font-sans` | Fira Sans | **All headings, bold.** The brand voice — no exceptions. |
| `--font-body` | Inter | Body, labels, buttons, table cells. |
| `--font-mono` | Fira Code | Figures, code, IDs, dates, tool names, eyebrows. Always tabular. |

Type scale — perfect fourth (1.333) on a 16px base:

Compact specimen and chart labels use `--text-5xs` 8.5 · `--text-4xs` 9 ·
`--text-3xs` 9.5 · `--text-2xs` 10 · `--text-xxs` 10.5 before the main scale.
`--text-xs` 11 · `--text-sm` 13 · `--text-base` 16 · `--text-md` 18 · `--text-lg` 21 ·
`--text-xl` 24 · `--text-2xl` 28 · `--text-3xl` 32 · `--text-4xl` 42 ·
`--text-5xl` 56 (public display headings only)

The three families are bundled from Fontsource and served by the application;
the interface does not depend on Google Fonts at runtime.

Weights: `--weight-regular` 400, `--weight-medium` 500, `--weight-semi` 600,
`--weight-bold` 700. Line-heights: `--lh-tight` 1.1 (H1/H2), `--lh-snug` 1.25 (H3–H6),
`--lh-body` 1.55 (prose), `--lh-ui` 1.4 (labels/buttons). Tracking:
`--tracking-tight` −0.02em (display), `--tracking-snug` −0.01em (sections),
`--tracking-wide` 0.04em (small caps), `--tracking-mono` 0.08em (uppercase mono).

```css
/* The machine hook: any element with [data-machine] or .mono renders Fira Code tabular. */
<span class="mono">12,408</span>
<td class="num">41.7%</td>   /* .num = right-aligned mono tabular */
```

### 3.2 Color

**Neutrals:** `--bg`, `--bg-elev`, `--bg-sunken`, `--surface`, `--ink`,
`--ink-muted`, `--ink-dim`, `--line`, `--line-bright`, `--grid-line`, `--overlay`,
`--backdrop`.

**Accent:** `--accent` `oklch(0.527 0.2 260)` (actions, links, focus),
`--accent-deep` (hover), `--accent-soft` (tints), `--accent-fg` (text on accent).

**Accent ramp (9 steps):** `--accent-50`…`--accent-900`, 500 === `--accent`.
**Unsigned ranges only** (heatmaps, intensity). Pick a contiguous slice.

**Semantic (signed values):** `--positive`/`--negative`/`--warning`, each with
`-soft` and `-deep`. Status aliases: `--err` (= `--negative`), `--info` (= `--accent`).
Legacy aliases `--ok`/`--warn` still work; prefer the full names in new code.

**Chart categorical palette:** `--chart-1`…`--chart-8` (blue, green, amber, red,
violet, cyan, pink, lime — theme-tuned). Multi-series charts assign them **in order**
via `chartPalette()`.

### 3.3 Geometry & spacing

Radii: `--radius-xs` 2 (chart marks) · `--radius-accent` 3 (selected edges) ·
`--radius-sm` 4 (tags/badges) · `--radius-md` 6 (buttons/inputs) ·
`--radius-lg` 7 (cards/panels/modals) · `--radius-xl` 10 (hero figures, mockup frames)
· `--radius-full` 999 (dots/avatars). **Never a literal pixel radius.**

Spacing (4px base): `--space-1` 4 … `--space-20` 80 (1,2,3,4,5,6,8,10,12,16,20).

### 3.4 Elevation

Cards never have shadows — 1px `--line` border. Shadows only on floating elements:
`--shadow-soft` (subtle lift), `--shadow-pop` (dropdowns/popovers/mockups),
`--shadow-modal` (modals only).

### 3.5 Motion

Durations: `--dur-instant` 80ms (hovers) · `--dur-fast` 140ms (buttons/tooltips) ·
`--dur-base` 220ms (panels/modals) · `--dur-slow` 360ms (drawers) · `--dur-chart`
640ms (chart entrances).
Easings: `--ease-standard` (default), `--ease-emphasized` (firm landings),
`--ease-spring` (micro-interactions only).

Entrance utilities (auto-disabled under reduced-motion):

```html
<div class="card animate-in">…</div>          <!-- rise + fade -->
<div class="card animate-in d2">…</div>       <!-- staggered: d1…d5 = 60…300ms delay -->
<div class="card animate-fade">…</div>        <!-- fade only -->
```

### 3.6 Focus & z-index

Every interactive element gets the same focus halo: `box-shadow: var(--focus-ring)`.
Z-index scale: `--z-base` 1, `--z-sticky` 5, `--z-rail` 6, `--z-dropdown` 30,
`--z-popover` 50, `--z-modal` 100, `--z-toast` 200, `--z-tooltip` 300.

### 3.7 Console layer (Direction A — locked 2026-08-12)

Console-native visual language for the **public/marketing surface**, built on the
existing palette — no color changes. Axes locked via
`prototype/direction-a-exploration.html`:

| Axis | Decision |
|---|---|
| Type voice | **V3 hybrid** — Fira Sans 800 stays the display face; the emphasized keyword inside display headings is Fira Code `--display-keyword-weight` at `--display-keyword-scale` (0.92em), in `--accent`, followed by the block cursor. One loud headline per viewport. |
| Mono family | Fira Code (unchanged). |
| Hero | **H3 split-ops** — status strip on top, headline + CTAs left, live agent transcript right. |
| Buttons | **Flat console** — Fira Code labels with `$` / `❯` sigils, `--accent` fill primary, bordered ghost secondary; hover swaps to `--accent-deep` in `--dur-instant` with `--ease-console`. No offset shadows. |
| Signature components | Compare strip renders as a **diff** (`+`/`-` rows, `--diff-strike` strikethrough on what you leave behind); pricing strip renders as a **readout** (selected tier = inset accent edge + tint). |
| Texture | **T1 dotted grid** (`--texture-dots`, `--texture-dot-size`) on hero and section dividers only. |
| Motion | Block cursor `--dur-cursor-blink` (1.06s, steps(1)); transcript lines print in instantly with `--dur-print-stagger` (150ms) stagger. **Console-layer rule: if it eases, it's wrong** — steps() and instant appearances only; editorial/blog content keeps the standard easings. |
| Theme | Dark primary; light supported (`--texture-dots` softens to 8% accent on light). |

Tokens (all in `tokens.css`, additive — no existing token altered):
`--font-display-keyword`, `--display-keyword-weight`, `--display-keyword-scale`,
`--font-action`, `--ease-console`, `--dur-cursor-blink`, `--dur-print-stagger`,
`--texture-dot-size`, `--texture-dots`, `--diff-strike`.

Studio surfaces do not inherit the console visual language; this layer scopes to
public pages. The v2.0.2 release did include a coordinated Studio, admin, demo, auth,
legal, and stylebook visual-quality pass: canonical data-first components were kept,
while spacing, card balance, touch targets, overflow safety, and responsive rhythm
were verified across every route. Component implementations (split-ops hero,
terminal card, diff strip, pricing readout) live in `design-system.css` and the
`/design` stylebook.
**Full implementation handover for v2.0.2: `docs/v2.0.2-design-handover.md`.**

## 4. Rules of use (normative)

1. **Signed values** (deltas, gains/losses) → `--positive` / `--negative` /
   `--warning` exclusively. **Never the accent.**
2. **Unsigned ranges** (heatmaps, intensity) → contiguous slice of the `--accent-*`
   ramp.
3. **Multi-series charts** → `--chart-1…8` in series order via `chartPalette()`.
4. **Actions, links, focus rings** → `--accent`; hover → `--accent-deep`.
5. **Headings** Fira Sans bold · **body** Inter · **machine content** Fira Code
   tabular. No exceptions.
6. **Cards**: 1px border, never a shadow. Shadows only on floating elements.
7. **Focus**: `var(--focus-ring)` on every interactive element.
8. **No literal hex/rgb/oklch** outside `tokens.css`; no pixel font-sizes outside the
   scale; no pixel radii; no `!important` in canonical design-system CSS.

### 4.1 Operational dashboards

Root and operations surfaces follow the same system, with stricter information
density rules:

- Lead with four comparable stat cards, then one wide time-series panel, then
  diagnostic tables. The data hierarchy matters more than decorative chrome.
- Separate health (`ready`, `completed`) from attention (`retry`, `stale`, `failed`)
  with semantic status badges. Never use accent blue to imply health.
- Live views must show their refresh cadence, last successful timestamp, paused or
  disconnected state, and preserve the last good data after transient errors.
- Worker health requires a durable heartbeat. Queue emptiness is not evidence that
  a worker process is alive.
- Tables may scroll inside bounded cards, but rows, errors, identifiers, and status
  text must never overflow the viewport. Machine identifiers and figures remain
  mono/tabular.
- Root navigation is rendered only for root users, but every data endpoint must
  independently enforce root authorization. Hidden navigation is not access
  control.

## 5. Components

All classes below are global (from `design-system.css`) — use them in any `.astro`
page without imports. Live examples: `/design/components`.

### 5.1 Buttons

```html
<button class="btn btn-primary">Primary</button>   <!-- one per view -->
<button class="btn">Secondary</button>
<button class="btn btn-ghost">Ghost</button>
<button class="btn btn-danger">Delete</button>      <!-- destructive only -->
<button class="btn btn-sm">Small</button>
<button class="btn btn-lg">Large</button>
<button class="btn-icon" aria-label="More">…</button>
```

### 5.2 Badges (status)

```html
<span class="badge badge-live"><span class="dot"></span>Live</span>
<span class="badge badge-done"><span class="dot"></span>Sent</span>
<span class="badge badge-err"><span class="dot"></span>Failed</span>
<span class="badge badge-warn"><span class="dot"></span>Near cap</span>
<span class="badge badge-draft">Draft</span>
<span class="badge">v1.0.1</span>
```

Mono, uppercase, optional pulsing dot. `.badge-live` = running, `.badge-done` =
success, `.badge-err` = failure, `.badge-warn` = limit/attention, `.badge-draft` =
inactive. (Legacy `.pill.live/.done/.err` still works; prefer `.badge` in new code.)

### 5.3 Cards

```html
<div class="card">
  <div class="card-head">
    <div class="card-head-main">
      <h3 class="card-title">Deliverability</h3>
      <p class="card-sub">Last 30 days across all campaigns</p>
    </div>
    <div class="card-head-actions"><button class="btn btn-sm">Export</button></div>
  </div>
  <div class="card-body">…</div>
  <div class="card-foot"><span>Updated 4 min ago</span><span class="mono">src: ses-eu-1</span></div>
</div>
```

`.card` = `--bg-elev` + 1px `--line` + `--radius-lg`. `.card-pad` for simple padded
cards. `a.card`/`button.card` get hover affordance. Interactive state:
`.stat-card.selected` shows the accent left-bar.

### 5.4 Stat cards & deltas

```html
<div class="stat-grid">
  <button class="stat-card selected" aria-pressed="true">
    <span class="stat-label">Emails sent <span class="delta positive delta-framed">↗ +18.2%</span></span>
    <span class="stat-value">12,408</span>
    <span class="stat-sub">vs 10,498 previous period</span>
    <span class="stat-spark" id="spark"></span>   <!-- optional sparkline host -->
  </button>
  …
</div>

<span class="delta positive">↗ +18.2%</span>
<span class="delta negative">↗ +0.3 pts</span> <!-- direction and desirability are independent -->
<span class="delta neutral">→ 0.0%</span>
```

`.stat-grid` is 4-up (2-up ≤1100px, 1-up ≤560px). Values are mono tabular; use
`<span class="unit">%</span>` for the smaller unit suffix. Prefer
`positive`/`negative`/`neutral` for outcome sentiment and render the real direction
with the arrow; `up`/`down`/`flat` remain legacy aliases. This is the **one** stat
card pattern — do not invent per-page variants.

### 5.5 Forms

```html
<div class="field">
  <label class="field-label" for="name">Campaign name <span class="req">*</span></label>
  <input id="name" type="text" placeholder="spring-launch" />
  <span class="field-hint">Shown to your agent in tool calls.</span>
</div>
<div class="field has-error">
  <label class="field-label" for="from">Sender</label>
  <input id="from" type="email" value="not-an-email" />
  <span class="field-error">Enter a valid sender address.</span>
</div>
<div class="field-row">…two fields side by side…</div>

<label class="check"><input type="checkbox" checked /> GDPR consent stored</label>
<label class="check radio"><input type="radio" name="r" checked /> Immediate send</label>
<label class="switch"><input type="checkbox" checked /> Live tracking</label>
```

### 5.6 Segmented control & tabs

```html
<div class="segmented" role="group" aria-label="Range">
  <button aria-pressed="true">7d</button><button aria-pressed="false">30d</button>
</div>

<nav class="tabs" aria-label="Analytics sections">
  <a href="/studio/analytics" class="active" aria-current="page">Overview</a>
  <a href="…">Deliverability</a>
</nav>
```

Segmented = switch a view within a panel (`aria-pressed`). Tabs = page-level
navigation (`aria-current="page"`, accent underline).

### 5.7 Modal (the one canonical dialog)

```html
<div class="modal-backdrop" id="my-modal" hidden>
  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="my-modal-title" tabindex="-1">
    <div class="modal-head">
      <div>
        <div class="modal-kicker">contacts.delete</div>   <!-- mono machine context -->
        <h3 id="my-modal-title">Delete 2 contacts?</h3>
      </div>
      <button class="modal-close" aria-label="Close">✕</button>
    </div>
    <div class="modal-body">…</div>
    <div class="modal-foot">
      <span class="modal-foot-note">This cannot be undone.</span>
      <button class="btn">Cancel</button>
      <button class="btn btn-danger">Delete</button>
    </div>
  </div>
</div>
```

Initialize `createModalController(backdrop, trigger)` from `src/lib/design/modal.ts`;
it owns visibility, initial focus, the focus trap, Escape/backdrop close, background
isolation and focus restoration. `.modal-wide` is the only width variant. Do not
build per-page modal behavior.

### 5.8 Alerts

```html
<div class="alert alert-info"><span class="alert-icon">ⓘ</span><span>…</span></div>
<div class="alert alert-success"><span class="alert-icon">✓</span><span>…</span></div>
<div class="alert alert-warning"><span class="alert-icon">⚠</span><span>…</span></div>
<div class="alert alert-danger"><span class="alert-icon">✕</span><span>…</span></div>
```

Optional inner structure: `.alert-title` (strong) + `.alert-sub` (muted xs).
Page-level alert stacks sit directly under `.page-head`, most severe first.

### 5.9 Progress

```html
<div class="progress"><span class="progress-fill" style="width: 64%"></span></div>
<div class="progress progress-warn">…</div>   <!-- ok / warn / err states -->

<div class="progress-labeled">
  <div class="progress-head"><span>Emails this cycle</span><b>8,412 / 10,000</b></div>
  <div class="progress progress-warn"><span class="progress-fill" style="width: 84%"></span></div>
</div>
```

### 5.10 Loading progress, skeleton, empty state, KV list, code

```html
<div class="studio-page-progress" aria-hidden="true">
  <span class="studio-page-progress-fill"></span>
</div>

<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-line skeleton-line-wide"></div>
<div class="skeleton skeleton-chart"></div>

<div class="empty-state">
  <div class="empty-state-icon">⌁</div>
  <h3>No segments yet</h3>
  <p>Segments target campaigns. Create one here, or ask your agent.</p>
  <button class="btn btn-primary">Create segment</button>
  <div class="mono-note">mcp · segments.create</div>
</div>

<dl class="kv">
  <div class="kv-row"><dt>API key</dt><dd>ro_live_9f2…c41</dd></div>
</dl>

<div class="code-block">
  <span class="code-lang">bash</span>
  <pre><code><span class="tk-prompt">$</span> <span class="tk-cmd">reachout campaigns send</span>
<span class="tk-ok">✓</span> queued <span class="tk-num">12,408</span> emails</code></pre>
</div>
```

Every `/studio` route gets its loading UI from `DashboardLayout.astro`; pages
must not create a second viewport progress bar. The shared loader is active
before first paint, follows initial ReachOut API requests, makes the unresolved
page content inert, and completes after the request queue becomes quiet. It also
starts immediately for same-origin `/studio` links and forms, including back and
forward navigation.

Skeleton compositions mirror the content they replace: stat cards use a short
label, value block, and supporting line; charts retain their grid and data
silhouette; lists keep avatar, text, and numeric columns. Skeletons are
decorative (`aria-hidden="true"`); the shared loader owns the single polite
status announcement. Shimmer and progress motion are disabled when
`prefers-reduced-motion: reduce` is active.

For an exceptional long-running Studio operation that should take over the
whole page, dispatch `studio:loading-start` with an optional `detail.label`, then
dispatch `studio:loading-end`. Prefer local button or panel busy states for
ordinary mutations so the rest of the page remains usable.

Code token colors: `.tk-prompt` (accent), `.tk-cmd`, `.tk-flag`, `.tk-str`,
`.tk-num`, `.tk-kw`, `.tk-comment`, `.tk-ok`, `.tk-err`.

### 5.11 Page header & dots

```html
<header class="page-head">
  <div class="page-head-main">
    <div class="eyebrow">Saturday · 8 August 2026 · Analytics</div>
    <h1>Analytics</h1>
    <p class="lede">Delivery and engagement across every campaign.</p>
  </div>
  <div class="page-head-actions">
    <button class="btn">Export</button>
    <button class="btn btn-primary">New campaign</button>
  </div>
</header>

<span class="dot live"></span>   <!-- pulsing status dot: live / accent / warn / err -->
```

One `.page-head` per page. Eyebrow = mono uppercase machine context.

## 6. Data tables (flagship)

Tables are the most important component on the platform. Canonical form:
`.table-wrap` > `table.data-table`. Live examples: `/design/data`.

**Column-type rules (non-negotiable):**

| Content | Markup | Font |
|---|---|---|
| Text / names | plain `td` | Inter |
| Numbers, %, counts | `td.num` (right-aligned) | Fira Code tabular |
| IDs, timestamps, emails | `td.mono` | Fira Code, muted |
| Status | `.badge` variants | — |
| Signed change | `.delta positive/negative/neutral` | Fira Code |
| Primary entity | `.cell-primary` (dot + title + mono sub) | Fira Sans + Fira Code |

```html
<div class="table-toolbar">
  <h2 class="table-toolbar-title">Campaigns</h2>
  <span class="table-toolbar-count">6 rows</span>
  <div class="table-toolbar-spacer"></div>
  <input class="table-toolbar-search" type="search" placeholder="Filter campaigns…" />
  <button class="btn">Export</button>
  <button class="btn btn-primary">New campaign</button>
</div>

<div class="table-wrap">
  <table class="data-table">
    <thead>
      <tr>
        <th class="select-col"><input type="checkbox" aria-label="Select all" /></th>
        <th class="sortable" aria-sort="ascending">
          <span class="th-inner">Campaign <span class="sort-arrow"></span></span>
        </th>
        <th>Status</th>
        <th class="num">Sent</th>
        <th class="num">Open rate</th>
        <th class="num">Δ week</th>
        <th>Trend</th>
        <th>Delivery</th>
        <th class="actions-col"><span class="sr-only">Actions</span></th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td class="select-col"><input type="checkbox" aria-label="Select row" /></td>
        <td>
          <div class="cell-primary">
            <span class="dot accent"></span>
            <div class="cell-primary-main">
              <span class="cell-primary-title">Spring launch — EU</span>
              <span class="cell-primary-sub">cmp_01J9…A4 · segment: eu-customers</span>
            </div>
          </div>
        </td>
        <td><span class="badge badge-live"><span class="dot"></span>Sending</span></td>
        <td class="num">12,408</td>
        <td class="num">41.7%</td>
        <td class="num"><span class="delta up">↗ +18.2%</span></td>
        <td><span class="cell-spark" data-spark="4,6,5,8,7,10,9,12"></span></td>
        <td>
          <div class="cell-progress">
            <div class="progress"><span class="progress-fill" style="width: 64%"></span></div>
            <span class="progress-label">64%</span>
          </div>
        </td>
        <td class="actions-col"><button class="btn-icon" aria-label="Actions">…</button></td>
      </tr>
    </tbody>
  </table>
  <div class="table-pagination">
    <span>1–6 of 128 campaigns</span>
    <div class="table-pagination-pages">
      <span class="current">1</span><a href="?page=2">2</a><a href="?page=3">3</a>
    </div>
  </div>
</div>
```

**Variants & states:**
- `.data-table.dense` — xs cells, tight padding (logs, admin, wide schemas).
- `.data-table.comfortable` — roomier for marketing-ish lists.
- `.table-wrap.sticky` + `max-height` — sticky header while rows scroll.
- `tr.is-selected` — accent-tinted selected row.
- Sortable headers: `th.sortable` + `aria-sort="ascending|descending"` + `.sort-arrow`.
- Empty results: replace the tbody with `.empty-state` inside the same `.table-wrap`.
- In-cell sparkline: `<span class="cell-spark" data-spark="1,2,3…"></span>` +
  `renderSparkline(el, { values })`.

## 7. Charts

Renderers are in `src/lib/charts/`. They read **every color from tokens** (never pass
hex), re-render automatically on theme change (`onThemeChange`), show the shared
`.d3-chart-tooltip` on hover, and animate once on mount (disabled under
reduced-motion). Each returns an unsubscribe function — call it when the host
unmounts.

```ts
import { renderLineChart } from '../../lib/charts/line-chart';
import { renderBarChart } from '../../lib/charts/bar-chart';
import { renderDonutChart } from '../../lib/charts/donut-chart';
import { renderHeatmap } from '../../lib/charts/heatmap';
import { renderSparkline } from '../../lib/charts/sparkline';

renderLineChart(host, {
  labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
  series: [
    { name: 'Emails sent', values: [1420, 1680, 1510, 1890, 2240, 980, 860] },
    { name: 'Delivered',   values: […], color: 'var(--chart-2)' },  // optional override
  ],
  area: true,          // gradient fill under first series (default: true for 1 series)
  height: 280,
  formatY: (v) => `${v}`,           // axis ticks
  formatValue: (v) => `${v} sends`, // tooltip values
});

renderBarChart(host, { labels, series, stacked: false, height: 280 });
renderDonutChart(host, {
  segments: [{ label: 'Email', value: 62 }, { label: 'MCP', value: 24 }],
  centerLabel: 'events',   // caption under the mono center total
  size: 200, thickness: 22,
});
renderHeatmap(host, { columns: ['Mon', …], rows: ['09:00', …], values: [[…]] });
renderSparkline(host, { values: [4, 7, 5, 9], type: 'line', height: 28 });
```

**Palette helpers** (`src/lib/charts/palette.ts`):

```ts
import { chartPalette, accentRampScale, semanticColors, onThemeChange } from '../../lib/charts/palette';

const [c1, c2] = chartPalette(2);        // ['var-driven blue', 'var-driven green', …]
const scale = accentRampScale(maxValue);  // v∈[0,max] → accent ramp step (choropleths)
const { positive, negative, warning } = semanticColors();
const stop = onThemeChange(() => rerender());
```

**The chart card** is the canonical container:

```html
<div class="chart-card">
  <div class="chart-card-head">
    <div><h3>Emails sent</h3><p>Last 7 days · all campaigns</p></div>
    <div class="chart-card-controls"><div class="segmented">…</div></div>
  </div>
  <div class="chart-host" id="chart"></div>
  <div class="chart-card-foot">
    <div class="chart-legend">
      <span class="legend-item">
        <span class="legend-swatch" style="background: var(--chart-1);"></span>
        Emails sent <span class="legend-value">10,580</span>
      </span>
    </div>
    <span class="mono text-dim">src: events_hourly</span>
  </div>
</div>
```

**Color semantics (rules 1–3):** deltas/gains/losses → `--positive/--negative/--warning`;
intensity/heatmaps → `--accent-*` slice (or `accentRampScale(max)`); multi-series →
`chartPalette()` in order; a single brand trend line → `--accent`.

## 8. Patterns

Full composed recipes with live demos at `/design/patterns`: page header, metric grid
+ chart card, settings form (card + fields + footer save bar), alert stack, loading
skeleton composition, agent activity row (mono timestamp + accent tool name + Inter
description + mono meta).

Two complete pages composed from canonical design-system components plus narrowly
scoped responsive layout wrappers:
- `/design/examples/studio` — analytics page: header, alert, selectable stat grid with
  sparklines, line + donut chart cards, full data table.
- `/design/examples/landing` — public page: hero with a **live mockup** (see below),
  feature grid, logo strip, pricing tiers, FAQ.

### Mockups (public pages)

Never ship a flat screenshot when the product can present itself. Use
`.mockup` (browser chrome) or `.mockup-terminal`, and fill `.mockup-body` with real
components at reduced scale — always on-brand, theme-aware, sharp at any DPI:

```html
<div class="mockup">
  <div class="mockup-chrome">
    <span class="mockup-dots"><i></i><i></i><i></i></span>
    <span class="mockup-url"><span class="lock">●</span> stage.usereachout.com/studio</span>
  </div>
  <div class="mockup-body">
    <!-- mini stat-cards, a renderLineChart at height:120, a mini table -->
  </div>
</div>
```

## 9. Accessibility

- Every icon-only button gets `aria-label`; segmented and selectable metric buttons
  use `aria-pressed`; page-level tab links use `aria-current="page"`; sortable columns keep
  `aria-sort` on the `th` and place a real button inside it.
- Focus is always visible via `var(--focus-ring)` — never remove it without replacing
  it.
- Charts render `role="img"` + `aria-label`; tooltips are pointer-only enhancements,
  never the sole carrier of information (legends/values must also be visible).
- Status is never color-only: badges pair color with a label and/or dot.
- All animations honor `prefers-reduced-motion`.

### 9.1 Responsive and content-safety baseline

- Every route must remain usable at **320 CSS px** without page-level horizontal
  overflow. Long prose, identifiers, domains, email addresses and translated labels
  wrap or truncate inside their own bounded component; they never widen the page.
- Dense data tables preserve readable columns inside `.table-wrap`, which owns the
  horizontal scroll. Do not shrink tabular text into illegibility or apply a minimum
  width to every table globally.
- Primary mobile controls are at least 44 CSS px high. Text inputs use the base type
  size on phones so mobile browsers do not zoom the page on focus.
- Page-header actions, card headers/footers, chart controls and modal actions wrap at
  narrow widths. Dialog bodies use the dynamic viewport and remain scrollable while
  their title and actions stay reachable.
- Navigation must expose the active destination and every destination at narrow
  widths. A scrollable rail is acceptable; silently dropping destinations is not.
- Safe-area insets are respected for fixed mobile navigation and full-height dialogs.

### 9.2 Application route contract

Every human-facing HTML route has exactly one `<main>` landmark and one visible `<h1>`.
Public, authentication, demo and Studio routes all inherit the same tokens and
canonical component library. Surface-specific CSS may own composition and responsive
layout, but it may not redefine the token values or fork a canonical component.

Runtime coordinates and data-driven dimensions (chart points, resizable table columns,
progress values) may be written as element styles when CSS custom properties or DOM
attributes cannot express the value cleanly. Static presentation belongs in a class.
External font stylesheets and per-page font imports are prohibited; Fontsource is the
single delivery path.

Before an application-wide design change is considered complete, exercise every route
at desktop and mobile widths in light and dark themes. The pass must check keyboard
focus, dialog dismissal/restoration, touch-sized controls, body overflow, clipped text,
console exceptions and failed application requests. Representative data-heavy routes
also run at the 768px and 1280px shell breakpoints.

## 10. Migration notes (legacy → canonical)

| Legacy (do not use in new code) | Canonical |
|---|---|
| Per-page modal implementations (`.modal-shell`, inline-styled divs, bare `<dialog>`) | `.modal-backdrop` + `.modal` |
| `.pill.live/.done/.err` | `.badge badge-live/-done/-err` with `.dot` |
| OverviewPanel / campaigns `.stat-card` copies / analytics `.metric-card` | `.stat-grid` + `.stat-card` (+ `.delta`) |
| Hardcoded chart hexes (`#2563eb`, `#22c55e`, d3 `interpolateBlues`…) | `chartPalette()` / `accentRampScale()` / semantic tokens |
| `var(--warn)` / `var(--ok)` | `var(--warning)` / `var(--positive)` (aliases still work) |
| `var(--orange)` (undefined, dropped) | `var(--warning)` |
| `.section-head-h2` copied per page | in `design-system.css` — just use it |
| `rgba(230,126,34,…)` legacy orange | `color-mix(in oklch, var(--accent) …)` |

The legacy harmonization block at the end of `global.css` remains transitional, but
its shared button/type/card rules no longer force the canonical library to use
`!important`. New design-system components must remain free of `!important`.

## 11. The /design route

Publicly accessible on the isolated Dokploy staging origin (no auth; allowed on all
staging dashboard surfaces including the studio-only surface). Map:

| Route | Contents |
|---|---|
| `/design` | Principles, typography & color at a glance |
| `/design/tokens` | Full token reference — swatches (click to copy), type scale, spacing, radii, elevation, motion, rules of use |
| `/design/tokens.json` | Machine-readable token manifest (JSON, prerendered, 1h cache) — the agent surface |
| `/design/components` | Every component, live, with copy-pasteable HTML |
| `/design/data` | **Flagship** — charts (line/bar/donut/heatmap/sparklines) + data tables (full-featured, dense, sticky, empty) |
| `/design/patterns` | Composed patterns: page header, metric grid, settings form, alert stack, skeletons, agent activity |
| `/design/examples/studio` | Complete Studio composition; public but `noindex` while illustrative copy is reviewed |
| `/design/examples/landing` | Complete public-page composition with live mockups; public but `noindex` |

Per the platform's three-surfaces rule, the design system is reachable by humans
(`/design`, UI), programmatically (`/design/tokens.json`, API), and as this document
(agent reference).

When adding a new component or token: add it to `tokens.css` or
`design-system.css`, document it on the matching `/design` page, and update this file.
