# Dashboard style guide: the RaceIntake look

Version 1 · 2026-10-05

This guide fully describes the dark "navy" dashboard style that was first built for the RaceIntake dashboard. Use
it to build a new dashboard or restyle an existing one on any stack:

- plain HTML or server templates
- React, Vue, Svelte or Angular
- Tailwind
- a component library
- a tool that is not a web app (Grafana, Streamlit, native apps)

Nothing in this guide depends on a particular machine, repository or server. Every value you need is written out
below.

| Section | Use it for |
|---|---|
| §1 The look, §2 Rules | What the result must look and feel like |
| §3 Design tokens | Every colour (hex and rgba), font, size, spacing, radius and shadow. This is the source of truth for any stack. |
| §4 Component specs | The anatomy, sizes and colours of each component, state by state |
| §5 Reference CSS | Drop-in `theme.css` and `components.css` for web apps, plus how to load the fonts |
| §6 Page skeleton, §7 Snippets | Ready-made HTML for the page shell and for each component |
| §8 Other stacks | Tailwind, component libraries, charts and non-web tools |
| §9 Behaviour, §10 Checklist | How pages behave, and how to check the result |

**Quick start for an agent**

1. Read §1 and §2.
2. For a web app, copy §5.1 to §5.3 into `theme.css`, `components.css` and `page.css`, then load the fonts (§5.4).
   For another stack, map the §3 tokens into that stack's theme (§8).
3. Build your pages from §6 and §7. Map each of your own states onto one of the six status tones (§3.3).
4. Check the result against §10.

---

## 1. The look

The UI is dark ink-navy. Surfaces are flat and get slightly brighter in steps: the sidebar is darkest, then the
page, the cards and the chips. They are separated by 1 px borders and a faint top highlight.

One azure accent (`#22B5DC`) marks links, focus, the primary button, selection and finished progress. Status colour
appears only on small elements: dots, rails, rings, pill text and track segments. Large areas always stay navy.

Text uses IBM Plex Sans. Every id, code, count and time uses IBM Plex Mono. Labels are quiet and in sentence case,
so the data is the brightest thing on the page.

Page layout:

- **Sidebar (left, 232 px):** the brand, nav groups under small grey headings, and a "Services" health box pinned
  to the bottom.
- **Header:** a 28 px semibold title, with a line under it holding a day pager and a summary. On the right is a
  toolbar: a label, a select, a search box, a mono "Updated" time and a Refresh button.
- **Stage tiles:** a row of filter buttons. Each tile has a 2 px top rail in its status colour, a dot with a
  label, and a big mono count. The pressed tile glows in its status colour.
- **Table and panel:** a card holding a dense table, next to a 400 px sticky detail panel. The table has a recessed
  header band, lifted group bands, and 52 px rows. Each row has a 5-segment pipeline track and a status pill.

```
┌─ sidebar 232 ──────────┬─ main: padding 28 top, 32 sides ────────────────────────────────────────────────┐
│ ⌁ AppName              │ Overview                          Source [All sources▾] [search] Updated… [Refresh] │
│ ▦ Overview  ▌current   │ ‹ Monday 28 September 2026 ›  1 group · 1 item · 38 skipped · Show                │
│ Pipeline               │                                                        (20 px between blocks)      │
│ ↓ Load                 │ [All 1][Loaded 1][Built 0][Dispatching 0][Done 0][Needs review 0][Rejected 0]     │
│ ⬡ Build            (1) │ ┌ card: table ──────────────────────────────────────┐ ┌ detail panel 400 ─────┐ │
│ ➤ Dispatch             │ │ Item  Type  Document and preprocess  Pipeline  …   │ │ sticky, 28 px from top │ │
│ Setup                  │ │ Group A  Feed A · AU · Daily import    chip  1 item│ │ head · keys · error    │ │
│ …                      │ │ A-1         Transcript · TXT    ▬▭▭▭▭  (Loaded) 09:10│ │ timeline · actions     │ │
│ ┌ Services ──────────┐ │ └────────────────────────────────────────────────────┘ └────────────────────────┘ │
│ │ ● Worker      Idle │ │                                 (under 1280 px the panel moves below the table)  │
│ └────────────────────┘ │                                                                                   │
└────────────────────────┴───────────────────────────────────────────────────────────────────────────────────┘
```

## 2. Rules

1. **Dark only.** Use `color-scheme: dark`. Do not add a light theme or a theme toggle unless the product owner
   asks for one.
2. **Colours come only from the §3 tokens.** On the web, colour literals live only in `theme.css`, and every other
   rule uses `var(--…)`. To check, run `grep -nE '#[0-9A-Fa-f]{3,8}\b|rgba?\(' components.css page.css`. It must
   print nothing.
3. **Fonts.** Use IBM Plex Sans (400/500/600) and IBM Plex Mono (400/500) and nothing else. Use 400 for body
   text, 500 for labels, buttons and the current nav item, and 600 for titles only.
4. **Mono** is for ids, codes, file names, versions, counts and times. Numbers in tables are right-aligned.
5. **Use at most one primary (accent-filled) button per view:** one per page, plus one per open dialog.
6. **Status colour goes on small elements only.** Never fill a large area with a status colour, and never use
   pure black or pure white.
7. **Colour is never the only signal.** Every pill and tile has a word. Every track has an `aria-label`. Every dot
   sits next to text.
8. **There are six status tones (§3.3).** Map your states onto them. Do not invent new hues.
9. **Use sentence case** for every label, heading and button ("Needs review", not "NEEDS REVIEW").
10. **Use real elements.** `<a>` is for navigation, `<button>` is for actions, and every control has a
    `<label>`. Toggles and filters use `aria-pressed`. The current nav link has `aria-current="page"`. Never
    remove the focus ring.
11. **A disabled control shows why.** Put the reason as visible text beside the control and link it with
    `aria-describedby`.
12. **Times** are 24 h `HH:MM` (`HH:MM:SS` for "Updated") in the viewer's time zone. Name the zone **once** per
    page, in the header. Dates look like `Monday 28 September 2026`. Never parse a calendar day (a date with no
    time) as UTC: west of Greenwich, that shifts it to the previous day.
13. **Data reaches the page as text**, through `textContent` or a framework's normal escaping. Never pass data
    through `innerHTML`.

## 3. Design tokens

The token names are the CSS custom properties used in §5. Other stacks can keep or rename them; only the values
matter. For a colour with transparency, both the 8-digit hex and the rgba form are given.

The text colours keep at least 4.5:1 contrast on the surfaces they sit on. The weakest pairs are about 5.3:1
(sidebar headings on the sidebar) and 5.6:1 to 6.2:1 (muted text on group bands and cards). Keep that minimum
for any new pair you add.

### 3.1 Surfaces and lines

| Token | Value | Used for |
|---|---|---|
| `--sidebar-bg` | `#070C16` | The sidebar (darkest surface) |
| `--bg-page` | `#0A101C` | Page background, drawer background |
| `--bg-control` | `#0C1422` | The inside of inputs and selects |
| `--bg-subtle` | `#0D1524` | Recessed bands: table header, card footer, code blocks |
| `--bg-card` | `#111A2B` | Cards, tiles, panels, the sign-in card. Also the text colour on the primary button. |
| `--row-hover` | `#152036` | Hover on rows, tiles and buttons |
| `--bg-raised` | `#16223A` | Secondary buttons and segmented buttons |
| `--bg-group` | `#17223A` | Group band rows, skeleton bars, muted chips |
| `--bg-chip` | `#1C2841` | Default chips |
| `--border` | `#1F2B42` | Card and tile borders, header and band rules |
| `--border-row` | `#19233A` | Row separators, panel section rules |
| `--border-control` | `#3A4A68` | Control borders, dashed chips, segmented dividers |
| `--border-control-hover` | `#4F6288` | Control borders on hover |
| `--edge-hi` | `#FFFFFF0A` = `rgba(255,255,255,0.04)` | 1 px highlight along the inside top of cards |
| `--shadow` | `rgba(2,6,15,0.45)` | Card drop shadow |
| `--backdrop` | `rgba(2,6,15,0.66)` | Dialog backdrop |

### 3.2 Text and accent

| Token | Value | Used for |
|---|---|---|
| `--text` | `#E8EEF7` | Primary text and values |
| `--text-strong-secondary` | `#C2CDDD` | Tile labels, panel sub-titles, the field and hint lines of error cards |
| `--text-secondary` | `#A3B1C6` | Labels, meta, table headers, times |
| `--text-muted` | `#8C9BB2` | De-emphasised text: skipped rows, steps not started, file formats |
| `--accent` | `#22B5DC` | Links, focus ring, primary button, selected-row bar, "done" track segments, the "All" tile |
| `--accent-hover` | `#4FCBEA` | Hover of accent elements, the count in a pressed segmented button |
| `--accent-tint` | `#22B5DC24` = `rgba(34,181,220,0.14)` | Selected-row fill, halo of the pressed "All" tile |
| `--accent-surface` | `#14364B` | Pressed segmented button |
| `--accent-wash` | `#10304A` | Wash inside the pressed "All" tile |

### 3.3 Status tones

There is one hue per tone, used in four strengths:

- **Strong:** dots, tile rails, track segments, the pressed tile's border, label and count.
- **Fill** (`-bg`): the pill background and the pressed tile's wash.
- **Ring** (`-tint`): the pill's 1 px inner ring and the pressed tile's 3 px halo.
- **Text** (`-fg`): the pill label.

| Tone | Strong | Fill `-bg` | Ring `-tint` | Text `-fg` | Meaning |
|---|---|---|---|---|---|
| `neutral` | `#94A3B8` | `#1E293D` | `#94A3B84D` = `rgba(148,163,184,0.30)` | `#C3CEDF` | Waiting, idle, loaded, skipped |
| `active` | `#6B9BFF` | `#16264A` | `#6B9BFF59` = `rgba(107,155,255,0.35)` | `#A9C3FF` | Running, in progress, sending |
| `ready` | `#A78BFA` | `#251F48` | `#A78BFA59` = `rgba(167,139,250,0.35)` | `#CBBBFF` | Built or staged, not final yet |
| `warn` | `#F2B33D` | `#2E2410` | `#F2B33D52` = `rgba(242,179,61,0.32)` | `#F7CC74` | Needs review, retrying, stale, degraded |
| `ok` | `#34D399` | `#0F2E2A` | `#34D39952` = `rgba(52,211,153,0.32)` | `#6EE7B7` | Accepted, finished, live, healthy |
| `fail` | `#F87171` | `#351820` | `#F8717159` = `rgba(248,113,113,0.35)` | `#FFA4A4` | Rejected, failed, down |

The "All" tile uses the accent as its tone: strong `--accent`, fill `--accent-wash`, ring `--accent-tint`.

**Pipeline track and timeline:**

| State | Token | Value |
|---|---|---|
| Done | `--track-done` | `#22B5DC` (the accent, not green) |
| In progress | `--track-active` | `#4C7DFF` |
| Needs attention | `--track-warn` | `#F2B33D` |
| Failed | `--track-fail` | `#F87171` |
| Not started | `--track-todo` | `#2C3954` |
| Timeline ring for "not started" | `--timeline-todo-ring` | `#45556F` |

**Map your states onto the tones.** For example:

| Tone | Pipeline dashboard | Scraper / ops dashboard |
|---|---|---|
| `neutral` | Loaded, Skipped | Idle, Paused, Scheduled |
| `active` | Building, Queued, Sending | Running, Fetching |
| `ready` | Built | Ready, Staged |
| `warn` | Needs review, Retrying | Stale, Degraded, Retrying |
| `ok` | Accepted | Live, OK, Healthy |
| `fail` | Rejected, Failed | Down, Error, Blocked |

### 3.4 Message tones

These are for notices, error cards, note rows and field states.

| Tone | Background | Border | Text | Extras |
|---|---|---|---|---|
| info | `#10203F` | `#28457F` | `#9EBBFF` | Strong text `#D3E0FF`; info-button border `#3F66BF` |
| warn | `#271F0E` | `#6B5220` | `#F5C76A` | "Missing" field background `#1C170C`; quiet repeated-warning text `#D6BC85` |
| error | `#2C141B` | `#7D2E3A` | `#FFA4A4` | Error note-row background `#22131C` |
| ok | `#0F2E2A` | `#0F2E2A` | `#5EE3A8` | — |

Token names: `--info-bg --info-border --info-text --info-strong --info-control-border`,
`--warn-bg --warn-border --warn-text --warn-control-bg --warn-note-text`,
`--error-bg --error-border --error-text --error-row-tint` and `--ok-text`.

### 3.5 Sidebar and services

| Token | Value | Used for |
|---|---|---|
| `--sidebar-active` | `#13203A` | Background of the hovered or current item |
| `--sidebar-text` | `#A9B6CB` | Nav text |
| `--sidebar-text-active` | `#F2F6FC` | Brand text, hovered or current text |
| `--sidebar-icon-active` | `#4FCBEA` | Brand icon, current icon and its 2 px bar, the focus ring inside the sidebar |
| `--sidebar-heading` | `#7586A0` | Section headings, idle icons, the "checking/unknown" service dot |
| `--sidebar-divider` | `#1A253A` | Right edge line, Services box border, badge background |
| `--sidebar-badge-text` | `#C9D3E3` | Badge text |
| `--sidebar-badge-warn-bg`, `--sidebar-badge-warn-text` | `#3A2C10`, `#F7CC74` | Amber badge |
| `--svc-text`, `--svc-text-dim` | `#C9D3E3`, `#8D9BB2` | Service name, right-hand state text |
| `--svc-dot-ok` | `#34D399` | Service OK |
| `--svc-dot-busy` | `#6B9BFF` | Service busy |
| `--svc-dot-warn`, `--svc-text-warn` | `#E0A93B`, `#F7CC74` | Service warning (dot, row text) |
| `--svc-dot-error`, `--svc-text-error` | `#F87171`, `#FFA4A4` | Service error (dot, row text) |

### 3.6 Typography

Font stacks: `"IBM Plex Sans", system-ui, sans-serif` and `"IBM Plex Mono", ui-monospace, monospace`.
Body line-height is 1.45.

| Role | Font | Size / weight | Colour | Notes |
|---|---|---|---|---|
| Page title (h1) | Sans | 28 / 600 | `--text` | Letter-spacing −0.01em |
| Panel title (detail panel, dialogs, sign-in) | Sans | 20 / 600 | `--text` | |
| Brand | Sans | 16 / 600 | `--sidebar-text-active` | Letter-spacing 0.01em |
| Header date, card title | Sans | 15 / 500 (date), 15 / 600 (card title) | `--text` | |
| Body, table cells, nav, buttons | Sans | 14 / 400 (buttons 500) | `--text` | |
| Labels, meta, notices, group bands | Sans | 13 / 400 | `--text-secondary` | |
| Table header, pills, chips, hints, badges | Sans | 12 (header and pills 500) | `--text-secondary` | Sentence case, never all caps |
| Tile count | Mono | 28 / 500 | `--text` (the tone colour when pressed) | Line-height 1 |
| Pill sub-line, small codes | Mono | 11 / 400 | `--text-secondary` | Wraps, never cut |
| Ids, codes, times, versions | Mono | Same size as the surrounding text | — | |

### 3.7 Spacing, sizes, radii

| Thing | Value |
|---|---|
| Sidebar | 232 px wide; padding 20 px top/bottom, 12 px sides; 2 px between items |
| Main area | Padding 28 px top, 32 px sides, 32 px bottom; 20 px between blocks |
| Header | 24 px between titles and tools; 6 px from title to the line under it; 12 px between tools |
| Controls | 40 px high (36 px inside tables); input padding 0 12 px; button padding 0 16 px |
| Icon button | 32 × 32 px |
| Table | Header 40 px, group band 36 px, row 52 px (44 px compact), 12 px between columns, 16 px at the row edges |
| Tiles | Minimum 84 px high, padding 14 px 16 px, 12 px between tiles |
| Detail panel | 400 px wide; sections padded 16 px 20 px |
| Radii | Controls 8 px · inner boxes 10 px · cards, tiles and notices 12 px · chips 6 px · pills and badges 999 px |
| Breakpoint | Under 1280 px, the detail panel moves below the table |

### 3.8 Lines, elevation, focus, motion, icons

- **Borders** are 1 px. Tiles use 1.5 px, and so do accent borders on chips and fields.
- **Elevation** applies to cards and panels only: `inset 0 1px 0 rgba(255,255,255,0.04)` (the top highlight) plus
  `0 1px 3px rgba(2,6,15,0.45)`. There are no other shadows, no blur and no glass effects.
- **Rails and stripes** are drawn inside the border, as inset shadows:

  | Element | Size and position |
  |---|---|
  | Tile top rail | 2 px, top |
  | Current nav item bar | 2 px, left |
  | Selected row bar | 1.5 px, left |
  | Note row stripe | 3 px, left |
  | Pressed segmented button | 2 px underline |
  | Pill ring | 1 px, all round |
- **Gradients** are used in exactly two places:
  - The pressed-tile wash: the tone fill fading to transparent at 80 %, top to bottom.
  - Warning note rows: `--st-warn-bg` fading to transparent at 55 %, left to right.
- **Focus** is a 2 px solid `--accent` outline with a 2 px offset. Inside the sidebar it uses
  `--sidebar-icon-active`.
- **Motion:** there is none, except the busy spinner. It rotates linearly every 0.8 s, or every 2.4 s under
  `prefers-reduced-motion`.
- **Icons** are outline icons on a 24 × 24 viewBox: stroke 1.8 (chevrons 2), round caps and joins, no fill,
  `currentColor`. They are 18 px (22 px for the brand), in Lucide/Feather style (lucide.dev).

## 4. Component specs

Each spec gives the anatomy, sizes and colours (token names from §3), state by state. The reference CSS in §5
implements exactly these specs.

**Sidebar**
- Background `--sidebar-bg` with a 1 px `--sidebar-divider` line on its right edge. It is full viewport height,
  sticky, and scrolls on its own.
- Brand: a 40 px row with a 10 px gap between icon and name, and 18 px of space below it.
- Section heading: 12 / 500 in `--sidebar-heading`, with 22 px above and 8 px below.
- Nav link: 40 px high, radius 8, 12 px gap between icon and label, text `--sidebar-text`, icon
  `--sidebar-heading`.
  - Hover: background `--sidebar-active`, text `--sidebar-text-active`.
  - Current: the same background, text at 500, icon `--sidebar-icon-active`, and a 2 px `--sidebar-icon-active`
    bar on the left.
- Badge (a count of items that need action): a pill at the right end of the link. It is mono 12, with
  `--sidebar-badge-text` on a `--sidebar-divider` fill. The amber version uses `--sidebar-badge-warn-bg` and
  `--sidebar-badge-warn-text`. Hide the badge at 0.

**Services box** (at the foot of the sidebar)
- 1 px `--sidebar-divider` border, radius 10, padding 14 px 12 px, 10 px between rows, 12 px text.
- Title: 12 / 500 in `--sidebar-heading`.
- Row: a 7 px dot, the service name (`--svc-text`, cut with an ellipsis), and the state text at the right
  (`--svc-text-dim`). Each row links to its own page.
- Dot colour by state:

  | State | Dot | Row text |
  |---|---|---|
  | ok | `--svc-dot-ok` | unchanged |
  | busy | `--svc-dot-busy` | unchanged |
  | warn | `--svc-dot-warn` | `--svc-text-warn` |
  | error | `--svc-dot-error` | `--svc-text-error` |
  | checking / unknown | `--sidebar-heading` | unchanged |

**Page header**
- Titles sit on the left and tools on the right, aligned along their bottom edges.
- Under the title: the date between two 32 px icon buttons (‹ ›), then the summary in `--text-secondary`,
  ending with a link button ("Show"). On narrow pages, the summary wraps under the date first, so the tools stay on
  the right.
- Tools, in order:
  - a 13 px label;
  - 40 px controls: background `--bg-control`, border `--border-control` (`--border-control-hover` on hover),
    radius 8; the search box is 180 px wide;
  - the "Updated HH:MM:SS zone" text, mono 12 in `--text-secondary` (`--warn-text` when the data is stale);
  - a secondary Refresh button.

**Card**
- Background `--bg-card`, 1 px `--border`, radius 12, with the card elevation from §3.8.
- Optional head: padding 14 px 20 px, a `--border` line below, title 15 / 600, meta 14 in `--text-secondary`.
- Body padding: 20 px 24 px.
- Optional footer: padding 14 px 20 px, background `--bg-subtle`, 13 px text in `--text-secondary`.
- A card that holds a table clips its content to its rounded corners.

**Stage tile** (a filter button with a count)
- Tiles share the row equally, at least 120 px each, 12 px apart. "All" comes first.
- Tile: minimum 84 px high, padding 14 px 16 px, radius 12, 1.5 px `--border`, background `--bg-card`, and a 2 px
  top rail in the tone's strong colour.
- Content: an 8 px dot (tone strong) and a 13 px label (`--text-strong-secondary`). Below them, 10 px down, the
  count: mono 28 / 500, line-height 1, `--text`.
- Hover: background `--row-hover`.
- Pressed (the active filter):
  - the border, the label (at 500) and the count take the tone's strong colour;
  - a 3 px halo in the tone's ring colour surrounds the tile;
  - the background is the tone fill fading to transparent at 80 %, over the card colour.
- A count of 0 keeps the normal look; there is no special alarm styling.

**Data table**
- Fixed column widths, with flexible columns sharing the rest. Full width of its card, 14 px text.
- Spacing: 12 px between columns, 16 px at the left and right edges of each row.
- Header row: 40 px high, background `--bg-subtle`, 1 px `--border` below, 12 / 500 in `--text-secondary`.
  Left-aligned, except numbers, which are right-aligned. It stays stuck to the top while you scroll.
- Group band (one per group, for example per source): 36 px high, background `--bg-group`, 1 px `--border` above
  and below, 13 px text. Left to right it holds:
  - the title, 600 in `--text`;
  - meta in `--text-secondary`;
  - an optional chip, pushed to the right;
  - the item count, mono 12 in `--text-secondary`.
- Row: 52 px high (44 px compact), 1 px `--border-row` below, vertically centred, text cut with an ellipsis.
  - Hover: `--row-hover`.
  - Selected: `--accent-tint` fill and a 1.5 px `--accent` bar on the left edge.
  - Muted (skipped items that are being shown): text `--text-muted`.
- Two-line cell:
  - Main line: 14 px in `--text`. A secondary part, such as the format, is `--text-muted`.
  - Sub-line: mono 12 in `--text-secondary`, or `--warn-note-text` when the value is not set.
- Note row: a full-width row under the row it explains, with 8 px vertical padding, 13 px text and a 3 px stripe
  on the left. Small table buttons may follow the text.

  | Note tone | Stripe | Text | Background |
  |---|---|---|---|
  | info / busy | `--info-border` | `--info-text` | none (busy adds a 10 px spinner) |
  | warn | `--warn-border` | `--warn-note-text` | `--st-warn-bg` fading to transparent at 55 % |
  | error | `--error-border` | `--error-text` | `--error-row-tint` |
- Empty: one 96 px row, centred, in `--text-secondary`: one sentence plus a link to the next step.
- Loading: three rows, each holding a 12 px `--bg-group` bar with radius 6.
- Checkboxes: 18 px, with `accent-color` set to `--accent`.

**Status pill**
- An inline pill: padding 3 px 10 px, radius 999, 12 / 500, never wraps. Fill = the tone's `-bg`, text = the
  tone's `-fg`, plus a 1 px inner ring in the tone's `-tint`.
- Optional sub-line, 2 px under the pill: mono 11 in `--text-secondary`, for example an error code or "Next
  08:35". It wraps rather than being cut.

**Pipeline track**
- 5 segments, each 20 × 6 px, radius 3, 3 px apart, one per stage.
- Segment colours: `--track-todo`, `--track-done` (the accent), `--track-active`, `--track-warn`, `--track-fail`.
- Each segment has a `title` with the stage name. The track has an `aria-label` that reads all the stages out.

**Chips**
- Padding 2 px 8 px, radius 6, 12 px. They do not wrap, except inside table cells. Default: `--bg-chip` with
  `--text`.

  | Variant | Look | Typical use |
  |---|---|---|
  | mono | Mono text | Rule or code text |
  | outline | `--bg-card`, 1 px `--border-control` | A template name |
  | accent | 1.5 px `--accent` border | The current choice |
  | dashed | 1 px dashed `--border-control`, text `--text-muted` | "None set" |
  | tone chips | The tone's fill and text, no ring | A small status label |
  | note | `--st-warn-bg`, `--warn-note-text` | A repeated warning |
  | info | `--info-bg`, `--info-text` | Edited |
  | muted | `--bg-group`, `--text-muted` | Skipped |

**Buttons**
- 40 px high (36 px in tables), padding 0 16 px, radius 8, 14 / 500, 1 px border, 8 px gap between icon and label.

  | Kind | Look | Hover |
  |---|---|---|
  | Secondary (default) | `--bg-raised`, `--border-control`, text `--text` | `--row-hover`, `--border-control-hover` |
  | Primary (at most one per view) | `--accent` fill and border, text `--bg-card` | `--accent-hover` |
  | Info (a call to review) | `--info-bg`, `--info-control-border`, text `--info-strong` | — |
  | Link | No box, text `--accent` | — |
  | Icon | 32 × 32 px square, secondary colours, needs an `aria-label` | as secondary |
- Disabled: 55 % opacity and a not-allowed cursor, with the reason in 12 px `--text-secondary` beside the button.

**Fields**
- The label sits above the control: 13 px `--text-secondary`, 6 px gap. An optional hint sits below: 12 px
  `--text-secondary`.
- Control: 40 px high, padding 0 12 px, radius 8, background `--bg-control`, 1 px `--border-control` (on hover
  `--border-control-hover`), 14 px `--text`.
- States:

  | State | Look | Meaning |
  |---|---|---|
  | edited | `--info-bg`, `--info-border`, `--info-text` | Changed from the default |
  | missing | `--warn-control-bg`, `--warn-border` | Must be filled |
  | accent | 1.5 px `--accent` border | The field to fill next |

**Segmented control** (tabs and filters)
- One row of buttons inside a box with a 1 px `--border-control` border, radius 8 and 1 px dividers.
- Button: 40 px high, padding 0 16 px, 14 / 500.
  - Inactive: background `--bg-raised`, text `--text-secondary`.
  - Hover: background `--row-hover`, text `--text`.
  - Pressed: background `--accent-surface`, text `--text`, and a 2 px `--accent` underline.
- A count follows the label, as in "Waiting · 5". In the pressed button the count is `--accent-hover`.

**Notice** (banners and results)
- Padding 12 px 16 px, radius 12, 1 px border, 13 px text. An optional title (14 / 500) goes above the text, and
  an optional row of buttons below it.
- Tones: info, warn, error and ok (colours in §3.4). Error notices are announced (`role="alert"`); the others are
  announced politely (`role="status"`).

**Detail panel**
- A card 400 px wide, beside the table. It is sticky, 28 px from the top, and never taller than the viewport
  minus 56 px; beyond that it scrolls inside. Sections are separated by 1 px `--border-row` lines. When nothing is
  selected, it shows one sentence in `--text-secondary`.
- Head: padding 20 px 20 px 16 px, 8 px gaps.
  - A meta line, 12 px in `--text-secondary` (for example "Source · country"), with the status pill at the right.
  - The title, 20 / 600.
  - A sub-line, 14 px in `--text-strong-secondary` ("type · document · format").
- Key block: two columns, a 96 px label column (13 px `--text-secondary`) and the values. Gaps are 10 px by 12 px,
  padding 16 px 20 px. Ids are mono, notes under a value are 12 px `--text-muted`, and edited parts are
  `--info-text`.
- Error card: 16 px below the section above it and 20 px in from the panel sides, padding 14 px 16 px, radius 10,
  in the warn or error tone (§3.4). Its lines, top to bottom:
  1. the origin (12 / 500), with the retry state in mono at the right;
  2. the code, mono 14 / 500;
  3. the message, 14 px;
  4. the field, mono 12 in `--text-strong-secondary`;
  5. a hint, 13 px, under a 1 px rule.
- Timeline: one row per step, 14 px between steps.
  - A 12 px dot: solid in `--track-done`, `-active`, `-warn` or `-fail`, or a hollow `--timeline-todo-ring` ring
    for a step not started.
  - The label (14 px) with a 12 px `--text-muted` detail below. Labels of steps not started are `--text-muted`.
  - The time, mono 12, right-aligned in a 44 px column.
- Action bar: a 1 px rule above, padding 16 px 20 px 20 px. Buttons are 8 px apart, with at most one primary. An
  "Open logs →" link is pushed to the right. A result line (13 px) goes below; errors are `--error-text`.

**Dialogs**
- Use native modal dialogs, with the `--backdrop` colour behind them. Esc closes a dialog, and focus returns to
  the element that opened it.
- Drawer (a side sheet for details): docked on the right, full height, up to 560 px wide, background `--bg-page`
  with a 1 px `--border` on its left edge. It has a close icon button at the top right, and the detail panel
  inside with 12 px margins.
- Modal: up to 760 px wide (and at least 16 px from each screen edge), at most 85 % of the viewport height,
  background `--bg-card`, 1 px `--border`, radius 12, padding 16 px 20 px, at most one primary button.

**Sign-in page**
- A centred column, at most 360 px wide, with 20 px gaps.
- The brand (20 / 600 with a 22 px `--accent` icon) sits above a card (padding 24 px, 16 px gaps, radius 12).
- The card holds the title (20 / 600), the fields and a full-width primary button.

## 5. Reference CSS (web)

### 5.1 `theme.css`

```css
/* theme.css: design tokens of the RaceIntake dashboard look (dark navy).
   The ONLY file with colour literals; every other rule uses var(--…). Load the IBM Plex fonts separately (§5.4). */
:root {
  color-scheme: dark;

  /* Surfaces, darkest to lightest: sidebar < page < control well < recessed band < card < hover < raised < group band < chip */
  --sidebar-bg:#070C16;
  --bg-page:#0A101C; --bg-control:#0C1422; --bg-subtle:#0D1524; --bg-card:#111A2B; --row-hover:#152036;
  --bg-raised:#16223A; --bg-group:#17223A; --bg-chip:#1C2841;
  --border:#1F2B42; --border-row:#19233A; --border-control:#3A4A68; --border-control-hover:#4F6288;
  --backdrop:rgb(2 6 15 / 0.66); --shadow:rgb(2 6 15 / 0.45); --edge-hi:#FFFFFF0A;

  /* Text */
  --text:#E8EEF7; --text-strong-secondary:#C2CDDD; --text-secondary:#A3B1C6; --text-muted:#8C9BB2;

  /* Accent (azure): links, focus ring, primary button, selection, "done" */
  --accent:#22B5DC; --accent-hover:#4FCBEA; --accent-tint:#22B5DC24;
  --accent-surface:#14364B;   /* pressed segmented button */
  --accent-wash:#10304A;      /* pressed "All" tile */

  /* Status tones, one hue each. strong = dots, rails, tracks · -bg = pill fill, pressed-tile wash
     · -tint = pill ring, pressed-tile halo · -fg = pill text */
  --st-neutral:#94A3B8; --st-neutral-bg:#1E293D; --st-neutral-tint:#94A3B84D; --st-neutral-fg:#C3CEDF;
  --st-active:#6B9BFF;  --st-active-bg:#16264A;  --st-active-tint:#6B9BFF59;  --st-active-fg:#A9C3FF;
  --st-ready:#A78BFA;   --st-ready-bg:#251F48;   --st-ready-tint:#A78BFA59;   --st-ready-fg:#CBBBFF;
  --st-warn:#F2B33D;    --st-warn-bg:#2E2410;    --st-warn-tint:#F2B33D52;    --st-warn-fg:#F7CC74;
  --st-ok:#34D399;      --st-ok-bg:#0F2E2A;      --st-ok-tint:#34D39952;      --st-ok-fg:#6EE7B7;
  --st-fail:#F87171;    --st-fail-bg:#351820;    --st-fail-tint:#F8717159;    --st-fail-fg:#FFA4A4;

  /* Pipeline track + timeline */
  --track-done:var(--accent); --track-active:#4C7DFF; --track-warn:var(--st-warn); --track-fail:var(--st-fail);
  --track-todo:#2C3954; --timeline-todo-ring:#45556F;

  /* Message tones: notices, error cards, note rows, edited/missing fields */
  --info-bg:#10203F; --info-border:#28457F; --info-text:#9EBBFF; --info-strong:#D3E0FF; --info-control-border:#3F66BF;
  --warn-bg:#271F0E; --warn-border:#6B5220; --warn-text:#F5C76A; --warn-control-bg:#1C170C;
  --warn-note-text:#D6BC85;   /* repeated warnings ("No template", "Not set"): readable, not shouty */
  --error-bg:#2C141B; --error-border:#7D2E3A; --error-text:#FFA4A4; --error-row-tint:#22131C;
  --ok-text:#5EE3A8;

  /* Sidebar + Services box */
  --sidebar-active:#13203A; --sidebar-text:#A9B6CB; --sidebar-text-active:#F2F6FC; --sidebar-icon-active:#4FCBEA;
  --sidebar-heading:#7586A0; --sidebar-divider:#1A253A;
  --sidebar-badge-text:#C9D3E3; --sidebar-badge-warn-bg:#3A2C10; --sidebar-badge-warn-text:#F7CC74;
  --svc-text:#C9D3E3; --svc-text-dim:#8D9BB2;
  --svc-dot-ok:#34D399; --svc-dot-busy:#6B9BFF; --svc-dot-warn:#E0A93B; --svc-text-warn:#F7CC74;
  --svc-dot-error:#F87171; --svc-text-error:#FFA4A4;

  /* Type */
  --font-sans:"IBM Plex Sans", system-ui, sans-serif; --font-mono:"IBM Plex Mono", ui-monospace, monospace;
  --fs-title:28px; --fs-panel-title:20px; --fs-card-title:15px;
  --fs-body:14px; --fs-meta:13px; --fs-small:12px; --fs-tiny:11px;

  /* Shape, size, spacing */
  --r-control:8px; --r-inner:10px; --r-card:12px; --r-pill:999px; --r-chip:6px;
  --h-control:40px; --h-control-table:36px; --h-row:52px; --h-row-compact:44px; --h-thead:40px; --h-group:36px;
  --pad-top:28px; --pad-x:32px; --gap-section:20px; --w-sidebar:232px; --w-panel:400px;
}
body.compact { --h-row: var(--h-row-compact); }   /* optional density toggle */
```

### 5.2 `components.css`

```css
/* components.css: shared components of the RaceIntake dashboard look. No colour literals: var(--…) only.
   Load after theme.css. */

/* ---------- Base ---------- */
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font-family: var(--font-sans); font-size: var(--fs-body); line-height: 1.45; color: var(--text); background: var(--bg-page); }
a { color: var(--accent); }
a:hover { color: var(--accent-hover); }
button, input, select, textarea { font: inherit; color: inherit; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
[hidden] { display: none !important; }
.mono { font-family: var(--font-mono); }
.dim { color: var(--text-muted); }
.meta { font-size: var(--fs-small); color: var(--text-secondary); }
.cluster { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.icon { width: 18px; height: 18px; flex-shrink: 0; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.chev { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.skip-link { position: absolute; left: 12px; top: -48px; z-index: 10; padding: 8px 12px; border-radius: var(--r-control); background: var(--bg-card); color: var(--text); }
.skip-link:focus { top: 12px; }
/* Tone plumbing: .pill .tile .dot .chip read --c (strong), --c-bg, --c-tint, --c-fg; a tone class sets them (end of file) */
:root { --c: var(--border-control); --c-bg: var(--bg-chip); --c-tint: transparent; --c-fg: var(--text); }

/* ---------- App shell: sidebar ---------- */
.app { display: flex; min-height: 100vh; }
.sidebar { position: sticky; top: 0; flex: 0 0 var(--w-sidebar); width: var(--w-sidebar); height: 100vh; overflow-y: auto;
           display: flex; flex-direction: column; gap: 2px; padding: 20px 12px;
           background: var(--sidebar-bg); color: var(--sidebar-text); box-shadow: inset -1px 0 0 var(--sidebar-divider); }
.sidebar :focus-visible { outline-color: var(--sidebar-icon-active); }
.brand { display: flex; align-items: center; gap: 10px; height: 40px; padding: 0 12px; margin-bottom: 18px;
         color: var(--sidebar-text-active); font-size: 16px; font-weight: 600; letter-spacing: 0.01em; text-decoration: none; }
.brand:hover { color: var(--sidebar-text-active); }
.brand .icon { width: 22px; height: 22px; color: var(--sidebar-icon-active); }
.nav-section { margin: 22px 12px 8px; font-size: var(--fs-small); font-weight: 500; color: var(--sidebar-heading); }
.nav-link { display: flex; align-items: center; gap: 12px; height: 40px; padding: 0 12px; border-radius: var(--r-control);
            color: var(--sidebar-text); font-size: var(--fs-body); text-decoration: none; }
.nav-link:hover { color: var(--sidebar-text-active); background: var(--sidebar-active); }
.nav-link .icon { color: var(--sidebar-heading); }
.nav-link[aria-current] { background: var(--sidebar-active); color: var(--sidebar-text-active); font-weight: 500;
                          box-shadow: inset 2px 0 0 var(--sidebar-icon-active); }
.nav-link[aria-current] .icon { color: var(--sidebar-icon-active); }
.nav-badge { margin-left: auto; padding: 1px 8px; border-radius: var(--r-pill); background: var(--sidebar-divider);
             color: var(--sidebar-badge-text); font-family: var(--font-mono); font-size: var(--fs-small); font-weight: 400; }
.nav-badge--warn { background: var(--sidebar-badge-warn-bg); color: var(--sidebar-badge-warn-text); }

/* Services box, pinned to the sidebar foot. data-state: checking | unknown | ok | busy | warn | error */
.services { margin-top: auto; display: flex; flex-direction: column; gap: 10px; padding: 14px 12px;
            border: 1px solid var(--sidebar-divider); border-radius: var(--r-inner); font-size: var(--fs-small); }
.services__title { margin: 0; font-size: var(--fs-small); font-weight: 500; color: var(--sidebar-heading); }
.svc { display: flex; align-items: center; gap: 8px; color: var(--svc-text); text-decoration: none; }
.svc:hover { color: var(--sidebar-text-active); }
.svc__dot { width: 7px; height: 7px; flex-shrink: 0; border-radius: 50%; background: var(--sidebar-heading); }
.svc__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.svc__text { margin-left: auto; color: var(--svc-text-dim); white-space: nowrap; }
.svc[data-state="ok"] .svc__dot { background: var(--svc-dot-ok); }
.svc[data-state="busy"] .svc__dot { background: var(--svc-dot-busy); }
.svc[data-state="warn"], .svc[data-state="warn"] .svc__text { color: var(--svc-text-warn); }
.svc[data-state="warn"] .svc__dot { background: var(--svc-dot-warn); }
.svc[data-state="error"], .svc[data-state="error"] .svc__text { color: var(--svc-text-error); }
.svc[data-state="error"] .svc__dot { background: var(--svc-dot-error); }

/* ---------- Page area + header ---------- */
.main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: var(--gap-section); padding: var(--pad-top) var(--pad-x) 32px; }
.main:focus { outline: none; }
.page-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; }
.page-header__titles { display: flex; flex-direction: column; gap: 6px; }
.page-header__tools { display: flex; align-items: center; gap: 12px; }
.page-header h1 { margin: 0; font-size: var(--fs-title); font-weight: 600; letter-spacing: -0.01em; }
.eyebrow { font-size: var(--fs-meta); color: var(--text-secondary); }
.back-link { font-size: var(--fs-body); font-weight: 500; text-decoration: none; }
.page-root { display: flex; flex-direction: column; gap: var(--gap-section); }
/* The line under the h1 (‹ day › + summary) and the toolbar pieces */
.subnav { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 2px; }
.subnav__title { font-size: 15px; font-weight: 500; }
.subnav__meta { font-size: var(--fs-body); color: var(--text-secondary); white-space: nowrap; }
.tool-label { font-size: var(--fs-meta); color: var(--text-secondary); }
.control { height: var(--h-control); padding: 0 12px; border: 1px solid var(--border-control); border-radius: var(--r-control);
           background: var(--bg-control); color: var(--text); font-size: var(--fs-body); }
.control:hover { border-color: var(--border-control-hover); }
.control--search { width: 180px; }
.updated { font-family: var(--font-mono); font-size: var(--fs-small); color: var(--text-secondary); white-space: nowrap; }
.updated.is-stale { color: var(--warn-text); }

/* ---------- Cards, notices ---------- */
.card, .dp, .empty-state { border: 1px solid var(--border); border-radius: var(--r-card); background: var(--bg-card);
                           box-shadow: inset 0 1px 0 var(--edge-hi), 0 1px 3px var(--shadow); }
.card--clip { overflow: clip; }   /* clips a table/head/foot to the radius; unlike hidden, keeps the sticky thead working */
.card__head { display: flex; align-items: center; gap: 12px; padding: 14px 20px; border-bottom: 1px solid var(--border); }
.card__title { margin: 0; font-size: var(--fs-card-title); font-weight: 600; }
.card__meta { font-size: var(--fs-body); color: var(--text-secondary); }
.card__body { padding: 20px 24px; }
.card__foot { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; padding: 14px 20px; background: var(--bg-subtle);
              font-size: var(--fs-meta); color: var(--text-secondary); }
.empty-state { margin: 0; padding: 32px; color: var(--text-secondary); text-align: center; }
.notice { margin: 0; padding: 12px 16px; border: 1px solid var(--info-border); border-radius: var(--r-card);
          background: var(--info-bg); color: var(--info-text); font-size: var(--fs-meta); }
.notice--warn { border-color: var(--warn-border); background: var(--warn-bg); color: var(--warn-text); }
.notice--error { border-color: var(--error-border); background: var(--error-bg); color: var(--error-text); }
.notice--ok { border-color: var(--st-ok-bg); background: var(--st-ok-bg); color: var(--ok-text); }
.notice-title { font-size: var(--fs-body); font-weight: 500; }
.notice-text { font-size: var(--fs-meta); }
.notice-actions { display: flex; gap: 8px; margin-top: 4px; }

/* ---------- Buttons: one .btn--primary per view ---------- */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: var(--h-control); padding: 0 16px;
       border: 1px solid var(--border-control); border-radius: var(--r-control); background: var(--bg-raised); color: var(--text);
       font-size: var(--fs-body); font-weight: 500; text-decoration: none; cursor: pointer; }
.btn:disabled { cursor: not-allowed; opacity: 0.55; }
.btn:not(.btn--primary):not(.btn--link):not(.btn--info):not(:disabled):hover { background: var(--row-hover); border-color: var(--border-control-hover); }
.btn--primary { border-color: var(--accent); background: var(--accent); color: var(--bg-card); }
.btn--primary:hover { border-color: var(--accent-hover); background: var(--accent-hover); color: var(--bg-card); }
.btn--info { border-color: var(--info-control-border); background: var(--info-bg); color: var(--info-strong); }
.btn--link { height: auto; padding: 0; border: 0; background: none; color: var(--accent); }
.btn--icon { width: 32px; height: 32px; padding: 0; }
.btn--table { height: var(--h-control-table); }
.btn-wrap { display: inline-flex; align-items: center; gap: 8px; }
.why, .field-hint { font-size: var(--fs-small); color: var(--text-secondary); }

/* ---------- Fields: label above control; states plain | edited | missing | accent ---------- */
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; align-items: end; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field > label { font-size: var(--fs-meta); color: var(--text-secondary); }
.field input, .field select, .field textarea { height: var(--h-control); padding: 0 12px; border: 1px solid var(--border-control);
  border-radius: var(--r-control); background: var(--bg-control); color: var(--text); font-size: var(--fs-body); }
.field textarea { height: auto; padding: 8px 12px; }
.field input:hover, .field select:hover, .field textarea:hover { border-color: var(--border-control-hover); }
.field--edited select, .field--edited input { background: var(--info-bg); border-color: var(--info-border); color: var(--info-text); }
.field--missing select, .field--missing input { background: var(--warn-control-bg); border-color: var(--warn-border); }
.field--accent select, .field--accent input { border: 1.5px solid var(--accent); }

/* ---------- Status pill (+ mono sub-line), pipeline track, chips ---------- */
.status { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 2px; min-width: 0; }
.pill { display: inline-flex; align-items: center; padding: 3px 10px; border-radius: var(--r-pill); font-size: var(--fs-small);
        font-weight: 500; white-space: nowrap; background: var(--c-bg); color: var(--c-fg); box-shadow: inset 0 0 0 1px var(--c-tint); }
.pill-sub { max-width: 100%; font-family: var(--font-mono); font-size: var(--fs-tiny); color: var(--text-secondary); overflow-wrap: anywhere; }
.track { display: inline-flex; gap: 3px; }
.track__seg { width: 20px; height: 6px; border-radius: 3px; background: var(--track-todo); }
.track__seg--done { background: var(--track-done); }
.track__seg--active { background: var(--track-active); }
.track__seg--warn { background: var(--track-warn); }
.track__seg--fail { background: var(--track-fail); }
.chip { display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: var(--r-chip);
        background: var(--c-bg); color: var(--c-fg); font-size: var(--fs-small); white-space: nowrap; }
.chip--mono { font-family: var(--font-mono); }
.chip--outline { background: var(--bg-card); border: 1px solid var(--border-control); }
.chip--accent { background: var(--bg-card); border: 1.5px solid var(--accent); }
.chip--dashed { background: var(--bg-card); border: 1px dashed var(--border-control); color: var(--text-muted); }
.chip--note { background: var(--st-warn-bg); color: var(--warn-note-text); }
.chip--info { background: var(--info-bg); color: var(--info-text); }
.chip--muted { background: var(--bg-group); color: var(--text-muted); }

/* ---------- Stage tiles: filter buttons with a 2 px tone rail; the pressed tile takes its tone ---------- */
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; }
.tile { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; min-height: 84px; padding: 14px 16px;
        border: 1.5px solid var(--border); border-radius: var(--r-card); background: var(--bg-card);
        box-shadow: inset 0 2px 0 var(--c); text-align: left; cursor: pointer; }
.tile:hover { background: var(--row-hover); }
.tile[aria-pressed="true"] { border-color: var(--c); box-shadow: inset 0 2px 0 var(--c), 0 0 0 3px var(--c-tint);
                             background: linear-gradient(180deg, var(--c-bg), transparent 80%), var(--bg-card); }
.tile-label { display: flex; align-items: center; gap: 8px; font-size: var(--fs-meta); color: var(--text-strong-secondary); }
.tile-count { font-family: var(--font-mono); font-size: 28px; font-weight: 500; line-height: 1; color: var(--text); }
.tile[aria-pressed="true"] .tile-label { color: var(--c); font-weight: 500; }
.tile[aria-pressed="true"] .tile-count { color: var(--c); }
.dot { width: 8px; height: 8px; flex-shrink: 0; border-radius: 50%; background: var(--c); }

/* ---------- Segmented control (tabs, filters): quiet inactive, accent underline when pressed ---------- */
.segmented { display: inline-flex; width: fit-content; border: 1px solid var(--border-control); border-radius: var(--r-control); overflow: hidden; }
.segmented-btn { height: var(--h-control); padding: 0 16px; border: 0; border-right: 1px solid var(--border-control);
                 background: var(--bg-raised); font-size: var(--fs-body); font-weight: 500; color: var(--text-secondary); cursor: pointer; }
.segmented-btn:last-child { border-right: 0; }
.segmented-btn:hover { background: var(--row-hover); color: var(--text); }
.segmented-btn[aria-pressed="true"] { background: var(--accent-surface); color: var(--text); box-shadow: inset 0 -2px 0 var(--accent); }
.segmented-btn[aria-pressed="true"] .seg-count { color: var(--accent-hover); }

/* ---------- Data table: fixed layout, sticky header band, group bands, 52 px rows ----------
   12 px between columns (6 px each side), 16 px at the row's outer edges; widths go on <col> in page.css */
.dt { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: var(--fs-body); }
.dt th, .dt td { padding: 0 6px; }
.dt th:first-child, .dt td:first-child { padding-left: 16px; }
.dt th:last-child, .dt td:last-child { padding-right: 16px; }
.dt thead th { position: sticky; top: 0; z-index: 1; height: var(--h-thead); background: var(--bg-subtle); border-bottom: 1px solid var(--border);
               font-size: var(--fs-small); font-weight: 500; color: var(--text-secondary); text-align: left; white-space: nowrap; }
.dt td { height: var(--h-row); border-bottom: 1px solid var(--border-row); vertical-align: middle; overflow: hidden; text-overflow: ellipsis; }
.dt .num { text-align: right; }
.dt .select { text-align: center; }
.dt input[type="checkbox"] { width: 18px; height: 18px; margin: 0; vertical-align: middle; accent-color: var(--accent); }
.dt td .chip { max-width: 100%; white-space: normal; }
.dt tr.group th { height: var(--h-group); background: var(--bg-group); border-bottom: 1px solid var(--border);
                  box-shadow: inset 0 1px 0 var(--border); font-size: var(--fs-meta); font-weight: 400; text-align: left; }
.dt tbody tr.row:not(.is-selected):hover td { background: var(--row-hover); }
.dt tr.row.is-selected td { background: var(--accent-tint); }
.dt tr.row.is-selected td:first-child { box-shadow: inset 1.5px 0 0 var(--accent); }
.dt tr.is-muted { color: var(--text-muted); }
.dt .row-btn { all: unset; display: block; width: 100%; cursor: pointer; }
.dt .row-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Note row under a row: a 3 px tone stripe on the left */
.dt tr.note td { height: auto; padding-block: 8px; font-size: var(--fs-meta); box-shadow: inset 3px 0 0 var(--border-control); }
.dt tr.note--info td, .dt tr.note--busy td { box-shadow: inset 3px 0 0 var(--info-border); color: var(--info-text); }
.dt tr.note--warn td { box-shadow: inset 3px 0 0 var(--warn-border); color: var(--warn-note-text);
                       background: linear-gradient(90deg, var(--st-warn-bg), transparent 55%); }
.dt tr.note--error td { box-shadow: inset 3px 0 0 var(--error-border); color: var(--error-text); background: var(--error-row-tint); }
.dt tr.note--busy .note-text::before { content: ""; display: inline-block; width: 10px; height: 10px; margin-right: 8px; vertical-align: -1px;
  border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .dt tr.note--busy .note-text::before { animation-duration: 2.4s; } }
.dt .note-actions { display: inline-flex; gap: 8px; margin-left: 12px; }
.dt tr.empty td { height: 96px; text-align: center; color: var(--text-secondary); }
.dt .skeleton-bar { display: block; height: 12px; border-radius: 6px; background: var(--bg-group); }
/* Cell content helpers: group band, two-line cell */
.band { display: flex; align-items: center; gap: 10px; font-size: var(--fs-meta); }
.band__title { font-weight: 600; color: var(--text); white-space: nowrap; }
.band__meta { color: var(--text-secondary); white-space: nowrap; }
.band__end { margin-left: auto; }
.band__count { min-width: 52px; text-align: right; font-family: var(--font-mono); font-size: var(--fs-small); color: var(--text-secondary); }
.cell2 { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.cell2__main, .cell2__sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cell2__sub { font-family: var(--font-mono); font-size: var(--fs-small); color: var(--text-secondary); }
.cell2__sub.is-unset { color: var(--warn-note-text); }

/* ---------- Main + side panel: table card left, 400 px sticky panel right; stacks under 1280 px ---------- */
.split { display: flex; gap: var(--gap-section); align-items: flex-start; min-height: 0; }
.split__main { flex: 1 1 auto; min-width: 0; }
.split__side { width: var(--w-panel); flex-shrink: 0; display: flex; flex-direction: column; gap: 12px;
               position: sticky; top: var(--pad-top); max-height: calc(100vh - 2 * var(--pad-top)); overflow: auto; }
@media (max-width: 1279px) {
  .split { flex-direction: column; align-items: stretch; }
  .split__side { width: auto; position: static; max-height: none; }
}

/* ---------- Detail panel: head → key block → error card → sections (timeline, payloads) → action bar ---------- */
.dp { display: flex; flex-direction: column; }
.dp-empty { margin: 0; padding: 20px; color: var(--text-secondary); font-size: var(--fs-body); }
.dp-head { display: flex; flex-direction: column; gap: 8px; padding: 20px 20px 16px; border-bottom: 1px solid var(--border-row); }
.dp-meta { display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: var(--fs-small); color: var(--text-secondary); }
.dp-head h2 { margin: 0; font-size: var(--fs-panel-title); font-weight: 600; }
.dp-sub { font-size: var(--fs-body); color: var(--text-strong-secondary); }
.keyblock { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 10px 12px; margin: 0; padding: 16px 20px;
            border-bottom: 1px solid var(--border-row); font-size: var(--fs-meta); }
.keyblock dt { color: var(--text-secondary); }
.keyblock dd { display: flex; flex-direction: column; gap: 2px; margin: 0; min-width: 0; }
.kb-note { font-size: var(--fs-small); color: var(--text-muted); }
.kb-edited { color: var(--info-text); }
.errcard { display: flex; flex-direction: column; gap: 8px; margin: 16px 20px 0; padding: 14px 16px; border-radius: var(--r-inner);
           border: 1px solid var(--warn-border); background: var(--warn-bg); }
.errcard--error { border-color: var(--error-border); background: var(--error-bg); }
.errcard-head { display: flex; justify-content: space-between; gap: 12px; font-size: var(--fs-small); font-weight: 500; color: var(--warn-text); }
.errcard-code { font-family: var(--font-mono); font-size: var(--fs-body); font-weight: 500; color: var(--warn-text); }
.errcard--error .errcard-head, .errcard--error .errcard-code { color: var(--error-text); }
.errcard-msg { font-size: var(--fs-body); line-height: 1.45; }
.errcard-field { font-family: var(--font-mono); font-size: var(--fs-small); color: var(--text-strong-secondary); }
.errcard-hint { padding-top: 8px; border-top: 1px solid var(--warn-border); font-size: var(--fs-meta); line-height: 1.45; color: var(--text-strong-secondary); }
.errcard--error .errcard-hint { border-top-color: var(--error-border); }
.dp-section { padding: 16px 20px 4px; }
.dp-section + .dp-section { border-top: 1px solid var(--border-row); }
.dp-section h3 { margin: 0 0 12px; font-size: var(--fs-small); font-weight: 400; color: var(--text-secondary); }
.dp details { margin: 0 0 10px; font-size: var(--fs-meta); }
.dp summary { cursor: pointer; color: var(--text-strong-secondary); }
.pre { max-height: 24em; margin: 6px 0 0; padding: 8px 10px; overflow: auto; font-family: var(--font-mono); font-size: var(--fs-small);
       white-space: pre-wrap; overflow-wrap: anywhere; background: var(--bg-subtle); border: 1px solid var(--border-row); border-radius: var(--r-inner); }
.timeline { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
.tl { display: grid; grid-template-columns: 14px minmax(0, 1fr) 44px; column-gap: 12px; padding-bottom: 14px; }
.tl-dot { width: 12px; height: 12px; margin-top: 4px; border-radius: 50%; border: 2px solid var(--track-done); background: var(--track-done); }
.tl--active .tl-dot { border-color: var(--track-active); background: var(--track-active); }
.tl--warn .tl-dot { border-color: var(--track-warn); background: var(--track-warn); }
.tl--fail .tl-dot { border-color: var(--track-fail); background: var(--track-fail); }
.tl--todo .tl-dot { border-color: var(--timeline-todo-ring); background: var(--bg-card); }
.tl-text { display: flex; flex-direction: column; gap: 2px; }
.tl-label { font-size: var(--fs-body); }
.tl--todo .tl-label { color: var(--text-muted); }
.tl-detail { font-size: var(--fs-small); color: var(--text-muted); }
.tl-time { font-family: var(--font-mono); font-size: var(--fs-small); color: var(--text-secondary); text-align: right; }
.actionbar { display: flex; flex-direction: column; gap: 8px; padding: 16px 20px 20px; border-top: 1px solid var(--border-row); }
.ab-buttons { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.ab-logs { margin-left: auto; font-weight: 500; text-decoration: none; }
.ab-result { font-size: var(--fs-meta); }
.ab-result:empty { display: none; }
.ab-result[role="alert"] { color: var(--error-text); }

/* ---------- Dialogs: native <dialog> + showModal(). Drawer = right-hand sheet, modal = centred ---------- */
.drawer { margin: 0 0 0 auto; padding: 0; width: min(560px, 100vw); max-width: 100vw; height: 100vh; max-height: 100vh;
          border: 0; border-left: 1px solid var(--border); background: var(--bg-page); color: var(--text); }
.drawer[open] { display: flex; flex-direction: column; }
.drawer__head { display: flex; justify-content: flex-end; padding: 8px 12px; }
.drawer .dp { margin: 0 12px 12px; }
.modal { width: min(760px, calc(100vw - 32px)); max-height: 85vh; padding: 16px 20px; border: 1px solid var(--border);
         border-radius: var(--r-card); background: var(--bg-card); color: var(--text); }
.drawer::backdrop, .modal::backdrop { background: var(--backdrop); }

/* ---------- Sign-in page ---------- */
.login-body { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 16px; }
.login { width: 100%; max-width: 360px; display: flex; flex-direction: column; gap: 20px; }
.login__brand { display: flex; align-items: center; justify-content: center; gap: 10px; font-size: var(--fs-panel-title); font-weight: 600; }
.login__brand .icon { width: 22px; height: 22px; color: var(--accent); }
.login__card { display: flex; flex-direction: column; gap: 16px; padding: 24px; border: 1px solid var(--border);
               border-radius: var(--r-card); background: var(--bg-card); }
.login__title { margin: 0; font-size: var(--fs-panel-title); font-weight: 600; }
.login__card .btn { width: 100%; }

/* ---------- Tones (keep last): neutral · active · ready · warn · ok · fail; all = the accent "All" tile ---------- */
.pill--neutral, .tile--neutral, .dot--neutral, .chip--neutral { --c: var(--st-neutral); --c-bg: var(--st-neutral-bg); --c-tint: var(--st-neutral-tint); --c-fg: var(--st-neutral-fg); }
.pill--active,  .tile--active,  .dot--active,  .chip--active  { --c: var(--st-active);  --c-bg: var(--st-active-bg);  --c-tint: var(--st-active-tint);  --c-fg: var(--st-active-fg); }
.pill--ready,   .tile--ready,   .dot--ready,   .chip--ready   { --c: var(--st-ready);   --c-bg: var(--st-ready-bg);   --c-tint: var(--st-ready-tint);   --c-fg: var(--st-ready-fg); }
.pill--warn,    .tile--warn,    .dot--warn,    .chip--warn    { --c: var(--st-warn);    --c-bg: var(--st-warn-bg);    --c-tint: var(--st-warn-tint);    --c-fg: var(--st-warn-fg); }
.pill--ok,      .tile--ok,      .dot--ok,      .chip--ok      { --c: var(--st-ok);      --c-bg: var(--st-ok-bg);      --c-tint: var(--st-ok-tint);      --c-fg: var(--st-ok-fg); }
.pill--fail,    .tile--fail,    .dot--fail,    .chip--fail    { --c: var(--st-fail);    --c-bg: var(--st-fail-bg);    --c-tint: var(--st-fail-tint);    --c-fg: var(--st-fail-fg); }
.tile--all, .dot--all { --c: var(--accent); --c-bg: var(--accent-wash); --c-tint: var(--accent-tint); --c-fg: var(--accent-hover); }
```

### 5.3 `page.css` (example: the overview table)

Put page-specific layout here, using `var(--…)` only. For a fixed-width column, the width is the content width
plus 12 px for the gap. The first and last columns also carry the 16 px row edge, so add 10 px more for those. A
column without a width takes up the rest of the row.

```css
/* page.css: overview table column widths (content 44 / 84 / flexible / 112 / 112 / 44 px) */
.dt--overview col.c-id     { width: 66px; }
.dt--overview col.c-type   { width: 96px; }
.dt--overview col.c-track  { width: 124px; }
.dt--overview col.c-status { width: 124px; }
.dt--overview col.c-at     { width: 66px; }
```

### 5.4 Fonts

IBM Plex is free (SIL Open Font License 1.1). Pick one of these ways to load it:

- **Any web page (the default):** the Google Fonts links already in the §6 `<head>`:
  ```html
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap">
  ```
- **Bundled apps (Vite, webpack, Next.js and similar):** install
  `npm install @fontsource/ibm-plex-sans @fontsource/ibm-plex-mono`, then import the fonts once at the app entry:
  ```js
  import "@fontsource/ibm-plex-sans/400.css";
  import "@fontsource/ibm-plex-sans/500.css";
  import "@fontsource/ibm-plex-sans/600.css";
  import "@fontsource/ibm-plex-mono/400.css";
  import "@fontsource/ibm-plex-mono/500.css";
  ```
- **No external requests allowed (strict CSP, offline):**
  1. Download the woff2 files from the official IBM Plex release (https://github.com/IBM/plex/releases, also on
     npm as `@ibm/plex`).
  2. Put them in `static/fonts/`.
  3. Load this `fonts.css` before `theme.css`.

  ```css
  /* fonts.css: self-hosted IBM Plex (use instead of the Google Fonts links) */
  @font-face { font-family: "IBM Plex Sans"; src: url(../fonts/IBMPlexSans-Regular.woff2) format("woff2"); font-weight: 400; font-display: swap; }
  @font-face { font-family: "IBM Plex Sans"; src: url(../fonts/IBMPlexSans-Medium.woff2) format("woff2"); font-weight: 500; font-display: swap; }
  @font-face { font-family: "IBM Plex Sans"; src: url(../fonts/IBMPlexSans-SemiBold.woff2) format("woff2"); font-weight: 600; font-display: swap; }
  @font-face { font-family: "IBM Plex Mono"; src: url(../fonts/IBMPlexMono-Regular.woff2) format("woff2"); font-weight: 400; font-display: swap; }
  @font-face { font-family: "IBM Plex Mono"; src: url(../fonts/IBMPlexMono-Medium.woff2) format("woff2"); font-weight: 500; font-display: swap; }
  ```

## 6. Page skeleton

This is the app shell plus an overview page: the reference layout. Replace the labels, nav items, tiles and rows
with your own. If your pages run under a CSP that blocks inline styles, the skeleton is already compliant: it has no
inline `style` attributes and no inline scripts.

```html
<!-- page.html: app shell + overview page (the reference layout). Replace the labels, nav, tiles and rows. -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Overview · AppName</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap">
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/page.css">
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<div class="app">
  <nav class="sidebar" aria-label="Main">
    <a class="brand" href="/"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 12h4l3-8 4 16 3-8h4"/></svg><span>AppName</span></a>
    <a class="nav-link" href="/" aria-current="page"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg><span>Overview</span></a>
    <div class="nav-section">Pipeline</div>
    <a class="nav-link" href="/load"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 3v12"/><path d="M7 10l5 5 5-5"/><path d="M4 21h16"/></svg><span>Load</span></a>
    <a class="nav-link" href="/build" aria-label="Build, 1 needs action"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2l9 5v10l-9 5-9-5V7z"/><path d="M12 22V12"/><path d="M21 7l-9 5-9-5"/></svg><span>Build</span><span class="nav-badge">1</span></a>
    <a class="nav-link" href="/dispatch"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4z"/></svg><span>Dispatch</span></a>
    <div class="nav-section">Setup</div>
    <a class="nav-link" href="/templates"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2l10 5-10 5L2 7z"/><path d="M2 12l10 5 10-5"/><path d="M2 17l10 5 10-5"/></svg><span>Templates</span></a>
    <a class="nav-link" href="/parsers"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M16 18l6-6-6-6"/><path d="M8 6l-6 6 6 6"/></svg><span>Parsers</span></a>
    <a class="nav-link" href="/sources"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5"/><path d="M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3"/></svg><span>Sources</span></a>
    <div class="nav-section">Help</div>
    <a class="nav-link" href="/help"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 1 1 3.5 2.3c-.6.3-1 .9-1 1.6V14"/><path d="M12 17h.01"/></svg><span>How to use</span></a>
    <section class="services" aria-labelledby="services-title">
      <h2 class="services__title" id="services-title">Services</h2>
      <a class="svc" href="/status#worker" data-state="ok"><span class="svc__dot" aria-hidden="true"></span><span class="svc__name">Worker</span><span class="svc__text">Idle</span></a>
      <a class="svc" href="/status#api" data-state="ok"><span class="svc__dot" aria-hidden="true"></span><span class="svc__name">API</span><span class="svc__text">OK</span></a>
      <a class="svc" href="/status#upstream" data-state="warn"><span class="svc__dot" aria-hidden="true"></span><span class="svc__name">Upstream</span><span class="svc__text">503 at 08:31</span></a>
    </section>
  </nav>

  <main class="main" id="main" tabindex="-1">
    <header class="page-header">
      <div class="page-header__titles">
        <h1>Overview</h1>
        <div class="subnav">
          <button class="btn btn--icon" type="button" aria-label="Previous day"><svg class="chev" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15 18l-6-6 6-6"/></svg></button>
          <span class="subnav__title">Monday 28 September 2026</span>
          <button class="btn btn--icon" type="button" aria-label="Next day"><svg class="chev" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 18l6-6-6-6"/></svg></button>
          <span class="subnav__meta">1 group · 1 item · 38 skipped · <button class="btn btn--link" type="button" aria-pressed="false">Show</button></span>
        </div>
      </div>
      <div class="page-header__tools">
        <label class="tool-label" for="source">Source</label>
        <select class="control" id="source"><option>All sources</option></select>
        <input class="control control--search" type="search" placeholder="Search items" aria-label="Search items">
        <span class="updated" data-tz="GMT+5:30">Updated 11:37:26 GMT+5:30</span>
        <button class="btn btn--secondary" type="button">Refresh</button>
      </div>
    </header>

    <div class="page-root">
      <div class="tiles" role="group" aria-label="Filter by stage">
        <button class="tile tile--all" type="button" aria-pressed="true"><span class="tile-label"><span class="dot" aria-hidden="true"></span>All items</span><span class="tile-count">1</span></button>
        <button class="tile tile--neutral" type="button" aria-pressed="false"><span class="tile-label"><span class="dot" aria-hidden="true"></span>Loaded</span><span class="tile-count">1</span></button>
        <button class="tile tile--ready" type="button" aria-pressed="false"><span class="tile-label"><span class="dot" aria-hidden="true"></span>Built</span><span class="tile-count">0</span></button>
        <button class="tile tile--active" type="button" aria-pressed="false"><span class="tile-label"><span class="dot" aria-hidden="true"></span>Dispatching</span><span class="tile-count">0</span></button>
        <button class="tile tile--ok" type="button" aria-pressed="false"><span class="tile-label"><span class="dot" aria-hidden="true"></span>Done</span><span class="tile-count">0</span></button>
        <button class="tile tile--warn" type="button" aria-pressed="false"><span class="tile-label"><span class="dot" aria-hidden="true"></span>Needs review</span><span class="tile-count">0</span></button>
        <button class="tile tile--fail" type="button" aria-pressed="false"><span class="tile-label"><span class="dot" aria-hidden="true"></span>Rejected</span><span class="tile-count">0</span></button>
      </div>

      <div class="split">
        <section class="card card--clip split__main" aria-label="Items">
          <table class="dt dt--overview" aria-label="Items">
            <colgroup><col class="c-id"><col class="c-type"><col><col class="c-track"><col class="c-status"><col class="c-at"></colgroup>
            <thead><tr><th scope="col">Item</th><th scope="col">Type</th><th scope="col">Document and preprocess</th><th scope="col">Pipeline</th><th scope="col">Status</th><th scope="col" class="num">At</th></tr></thead>
            <tbody>
              <tr class="group"><th colspan="6" scope="rowgroup"><span class="band"><span class="band__title">Group A</span><span class="band__meta">Feed A · AU · Daily import</span><span class="band__end"><span class="chip chip--dashed">No template</span></span><span class="band__count">1 item</span></span></th></tr>
              <tr class="row">
                <td class="mono"><button class="row-btn" type="button" aria-pressed="false" aria-label="Group A, item A-1, Loaded">A-1</button></td>
                <td></td>
                <td><span class="cell2"><span class="cell2__main">Transcript<span class="dim"> · TXT</span></span><span class="cell2__sub is-unset">Not set · choose in Build</span></span></td>
                <td><span class="track" role="img" aria-label="Pipeline: Loaded done, Built not started, Queued not started, Sent not started, Done not started"><span class="track__seg track__seg--done" title="Loaded"></span><span class="track__seg" title="Built"></span><span class="track__seg" title="Queued"></span><span class="track__seg" title="Sent"></span><span class="track__seg" title="Done"></span></span></td>
                <td><span class="status"><span class="pill pill--neutral">Loaded</span><span class="pill-sub">To set in Build</span></span></td>
                <td class="num mono meta">09:10</td>
              </tr>
            </tbody>
          </table>
        </section>
        <aside class="split__side" aria-label="Details">
          <div class="dp"><p class="dp-empty">Select an item to see its key, template, errors and timeline.</p></div>
        </aside>
      </div>
    </div>
  </main>
</div>
</body>
</html>
```

## 7. Component snippets

**Buttons.** This row has one primary button. A disabled button carries its reason beside it.

```html
<div class="cluster">
  <button class="btn btn--primary" type="button">Send</button>
  <button class="btn btn--secondary" type="button">Rebuild</button>
  <button class="btn btn--info" type="button">Review 3 items</button>
  <button class="btn btn--link" type="button">Edit rule</button>
  <span class="btn-wrap"><button class="btn btn--secondary" type="button" disabled aria-describedby="why-1">Resend</button><span class="why" id="why-1">Nothing to resend yet</span></span>
</div>
```

**Pills (with an optional mono sub-line), tracks and chips:**

```html
<div class="cluster">
  <span class="pill pill--neutral">Loaded</span>
  <span class="pill pill--active">Sending</span>
  <span class="pill pill--ready">Built</span>
  <span class="status"><span class="pill pill--warn">Retrying 2/6</span><span class="pill-sub">Next 08:35</span></span>
  <span class="pill pill--ok">Accepted</span>
  <span class="status"><span class="pill pill--fail">Rejected</span><span class="pill-sub">UNSUPPORTED_TYPE</span></span>
  <span class="track" role="img" aria-label="Pipeline: Loaded done, Built done, Queued done, Sent needs attention, Done not started"><span class="track__seg track__seg--done"></span><span class="track__seg track__seg--done"></span><span class="track__seg track__seg--done"></span><span class="track__seg track__seg--warn"></span><span class="track__seg"></span></span>
  <span class="track" role="img" aria-label="Pipeline: all stages done except the last, which failed"><span class="track__seg track__seg--done"></span><span class="track__seg track__seg--done"></span><span class="track__seg track__seg--done"></span><span class="track__seg track__seg--done"></span><span class="track__seg track__seg--fail"></span></span>
</div>
<div class="cluster">
  <span class="chip">plain</span>
  <span class="chip chip--mono">type = transcript</span>
  <span class="chip chip--outline">default-template v1</span>
  <span class="chip chip--accent">Template</span>
  <span class="chip chip--dashed">No template</span>
  <span class="chip chip--neutral">Step 1</span>
  <span class="chip chip--ok">Already loaded</span>
  <span class="chip chip--warn">No step</span>
  <span class="chip chip--note">Source changed</span>
  <span class="chip chip--info">Edited</span>
  <span class="chip chip--muted">Skipped</span>
</div>
```

**Fields, the segmented control and notices.** A notice has `role="status"`; an error notice has `role="alert"`.

```html
<div class="card card__body form-grid">
  <div class="field"><label for="f1">Feed</label><select id="f1"><option>Feed A</option></select></div>
  <div class="field field--edited"><label for="f2">Preprocess (edited)</label><select id="f2"><option>Parser v2</option></select></div>
  <div class="field field--missing"><label for="f3">Template (missing)</label><select id="f3"><option>Choose…</option></select><span class="field-hint">Pick one before Build</span></div>
</div>
<div class="segmented" role="group" aria-label="Show">
  <button class="segmented-btn" type="button" aria-pressed="true">Waiting<span class="seg-count"> · 5</span></button>
  <button class="segmented-btn" type="button" aria-pressed="false">Built<span class="seg-count"> · 2</span></button>
  <button class="segmented-btn" type="button" aria-pressed="false">All</button>
</div>
<div class="notice" role="status"><div class="notice-title">Dispatch is live</div><div class="notice-text">Items are sent when you press Send.</div></div>
<div class="notice notice--warn" role="status"><div class="notice-title">API key expires 14 Oct</div><div class="notice-text">Rotate it before then.</div></div>
<div class="notice notice--error" role="alert"><div class="notice-title">HTTP 502</div><div class="notice-text">Cannot reach the API right now; the page keeps retrying.</div><div class="notice-actions"><button class="btn btn--secondary" type="button">Retry</button></div></div>
<div class="notice notice--ok" role="status">Sent 3 items.</div>
```

**Table states.** This table shows a selected row; note rows under a row (warn, error, busy); a muted (skipped) row;
an empty state; and a skeleton (loading) row.

```html
<section class="card card--clip">
  <table class="dt dt--overview" aria-label="Examples">
    <colgroup><col class="c-id"><col class="c-type"><col><col class="c-track"><col class="c-status"><col class="c-at"></colgroup>
    <thead><tr><th scope="col">Item</th><th scope="col">Type</th><th scope="col">Document</th><th scope="col">Pipeline</th><th scope="col">Status</th><th scope="col" class="num">At</th></tr></thead>
    <tbody>
      <tr class="group"><th colspan="6" scope="rowgroup"><span class="band"><span class="band__title">Group B</span><span class="band__meta">Feed B · UK · Hourly import</span><span class="band__end"><span class="chip chip--outline">default-template v1</span></span><span class="band__count">4 items</span></span></th></tr>
      <tr class="row is-selected">
        <td class="mono"><button class="row-btn" type="button" aria-pressed="true">B-1</button></td><td>Report</td>
        <td><span class="cell2"><span class="cell2__main">Transcript<span class="dim"> · TXT</span></span><span class="cell2__sub">Parser v2</span></span></td>
        <td><span class="track" role="img" aria-label="Pipeline: Loaded done, Built needs attention, the rest not started"><span class="track__seg track__seg--done"></span><span class="track__seg track__seg--warn"></span><span class="track__seg"></span><span class="track__seg"></span><span class="track__seg"></span></span></td>
        <td><span class="status"><span class="pill pill--warn">Needs review</span><span class="pill-sub">FIELD_NOT_FOUND</span></span></td>
        <td class="num mono meta">09:12</td>
      </tr>
      <tr class="note note--warn"><td colspan="6"><span class="note-text">"Acme Ltd" is not in the reference list</span><span class="note-actions"><button class="btn btn--table btn--secondary" type="button">Edit</button></span></td></tr>
      <tr class="row">
        <td class="mono"><button class="row-btn" type="button" aria-pressed="false">B-2</button></td><td>Report</td>
        <td><span class="cell2"><span class="cell2__main">Transcript<span class="dim"> · TXT</span></span><span class="cell2__sub">Parser v2</span></span></td>
        <td><span class="track" role="img" aria-label="Pipeline: all stages done except the last, which failed"><span class="track__seg track__seg--done"></span><span class="track__seg track__seg--done"></span><span class="track__seg track__seg--done"></span><span class="track__seg track__seg--done"></span><span class="track__seg track__seg--fail"></span></span></td>
        <td><span class="status"><span class="pill pill--fail">Rejected</span><span class="pill-sub">422</span></span></td>
        <td class="num mono meta">09:20</td>
      </tr>
      <tr class="note note--error"><td colspan="6"><span class="note-text">UNSUPPORTED_TYPE · summary</span></td></tr>
      <tr class="row">
        <td class="mono"><button class="row-btn" type="button" aria-pressed="false">B-3</button></td><td>Report</td>
        <td><span class="cell2"><span class="cell2__main">Transcript<span class="dim"> · TXT</span></span><span class="cell2__sub">Parser v2</span></span></td>
        <td><span class="track" role="img" aria-label="Pipeline: Loaded done, Built in progress, the rest not started"><span class="track__seg track__seg--done"></span><span class="track__seg track__seg--active"></span><span class="track__seg"></span><span class="track__seg"></span><span class="track__seg"></span></span></td>
        <td><span class="status"><span class="pill pill--active">Building</span><span class="pill-sub">Queue #2</span></span></td>
        <td class="num mono meta">09:31</td>
      </tr>
      <tr class="note note--busy"><td colspan="6"><span class="note-text">Queue #2 · about 2 min</span></td></tr>
      <tr class="row is-muted">
        <td class="mono"><button class="row-btn" type="button" aria-pressed="false">B-4</button></td><td>Report</td>
        <td><span class="cell2"><span class="cell2__main">Reference record<span class="dim"> · JSON</span></span><span class="cell2__sub">By template · context only</span></span></td>
        <td><span class="track" role="img" aria-label="Pipeline: skipped"><span class="track__seg"></span><span class="track__seg"></span><span class="track__seg"></span><span class="track__seg"></span><span class="track__seg"></span></span></td>
        <td><span class="pill pill--neutral">Skipped</span></td>
        <td class="num mono meta">09:31</td>
      </tr>
      <tr class="empty"><td colspan="6">No items on this day. <a href="/load">Load a feed →</a></td></tr>
      <tr class="skeleton"><td colspan="6"><span class="skeleton-bar"></span></td></tr>
    </tbody>
  </table>
</section>
```

**Detail panel (with an item selected).** It goes in `.split__side`. On pages without a side panel, put it in a
`.drawer` dialog instead.

```html
<aside class="dp" aria-label="Details">
  <header class="dp-head">
    <div class="dp-meta"><span>Feed B · UK</span><span class="status"><span class="pill pill--warn">Needs review</span><span class="pill-sub">FIELD_NOT_FOUND</span></span></div>
    <h2>Group B · B-1</h2>
    <div class="dp-sub">Report · Transcript · TXT</div>
  </header>
  <dl class="keyblock">
    <dt>Key</dt><dd><span class="mono">group-b-2026-09-28-1</span><span class="kb-note">As published</span></dd>
    <dt>Template</dt><dd><span>default-template v1 <span class="kb-edited">· edited: preprocess</span></span></dd>
    <dt>Payload</dt><dd><span class="mono">pl_4f2a9c · v2</span></dd>
  </dl>
  <section class="errcard errcard--warn" aria-label="Error">
    <div class="errcard-head"><span>Validation</span><span class="mono">Retry 2/6 · next 08:35</span></div>
    <div class="errcard-code">FIELD_NOT_FOUND</div>
    <div class="errcard-msg">"Acme Ltd" is not in the reference list.</div>
    <div class="errcard-field">field: items[3].name</div>
    <div class="errcard-hint">Fix the value in Build, then rebuild.</div>
  </section>
  <section class="dp-section">
    <h3>Timeline</h3>
    <ol class="timeline">
      <li class="tl tl--done"><span class="tl-dot"></span><span class="tl-text"><span class="tl-label">Loaded</span><span class="tl-detail">Feed B</span></span><span class="tl-time">09:10</span></li>
      <li class="tl tl--warn"><span class="tl-dot"></span><span class="tl-text"><span class="tl-label">Built</span><span class="tl-detail mono">FIELD_NOT_FOUND</span></span><span class="tl-time">09:12</span></li>
      <li class="tl tl--todo"><span class="tl-dot"></span><span class="tl-text"><span class="tl-label">Sent</span></span><span class="tl-time"></span></li>
    </ol>
  </section>
  <section class="dp-section">
    <h3>Payload</h3>
    <details><summary>Request JSON</summary><pre class="pre">{ "item": "B-1", "rows": 9 }</pre></details>
  </section>
  <div class="actionbar">
    <div class="ab-buttons"><button class="btn btn--primary" type="button">Rebuild</button><button class="btn btn--secondary" type="button">Skip…</button><a class="ab-logs" href="/logs">Open logs →</a></div>
    <div class="ab-result" role="status"></div>
  </div>
</aside>
```

**Dialogs and the sign-in page.** Open a dialog with `dialog.showModal()`. When it closes, return focus to the
button that opened it.

```html
<dialog class="drawer" id="item-drawer" aria-label="Item">
  <div class="drawer__head"><button class="btn btn--icon" type="button" aria-label="Close">×</button></div>
  <!-- an <aside class="dp"> goes here -->
</dialog>
<dialog class="modal" id="confirm" aria-label="Confirm">…at most one primary button inside…</dialog>

<!-- Sign-in page: <body class="login-body"> -->
<main class="login">
  <div class="login__brand"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 12h4l3-8 4 16 3-8h4"/></svg><span>AppName</span></div>
  <form class="login__card" method="post" action="/login">
    <h1 class="login__title">Sign in</h1>
    <div class="field"><label for="username">Username</label><input id="username" name="username" autocomplete="username" required></div>
    <div class="field"><label for="password">Password</label><input id="password" name="password" type="password" autocomplete="current-password" required></div>
    <button class="btn btn--primary" type="submit">Sign in</button>
  </form>
</main>
```

## 8. Other stacks

### 8.1 React, Vue, Svelte, Angular

- Import `theme.css`, `components.css` and the fonts once, at the app entry. Use the same class names
  (`className` in React).
- Write small components that output the §6 and §7 markup: `Sidebar`, `PageHeader`, `StageTiles`, `DataTable`,
  `StatusPill`, `PipelineTrack` and `DetailPanel`. Each takes a tone or state as a prop and picks the modifier
  class from it.
- Keep the token names. Never copy hex values into components or inline styles.

### 8.2 Tailwind

Keep `theme.css` loaded and point the Tailwind colours at its variables. The tone colours are named `tone-*` so
they do not override Tailwind's built-in `neutral` palette.

Tailwind v3 (`tailwind.config.js`):

```js
module.exports = {
  theme: {
    extend: {
      colors: {
        page: "var(--bg-page)", card: "var(--bg-card)", subtle: "var(--bg-subtle)", raised: "var(--bg-raised)",
        group: "var(--bg-group)", chip: "var(--bg-chip)", well: "var(--bg-control)", "row-hover": "var(--row-hover)",
        line: "var(--border)", "line-row": "var(--border-row)", "line-control": "var(--border-control)",
        ink: "var(--text)", "ink-strong": "var(--text-strong-secondary)", "ink-2": "var(--text-secondary)", "ink-3": "var(--text-muted)",
        accent: "var(--accent)", "accent-hover": "var(--accent-hover)",
        "tone-neutral": "var(--st-neutral)", "tone-active": "var(--st-active)", "tone-ready": "var(--st-ready)",
        "tone-warn": "var(--st-warn)", "tone-ok": "var(--st-ok)", "tone-fail": "var(--st-fail)",
      },
      fontFamily: { sans: ['"IBM Plex Sans"', "system-ui", "sans-serif"], mono: ['"IBM Plex Mono"', "ui-monospace", "monospace"] },
      borderRadius: { control: "8px", inner: "10px", card: "12px", chip: "6px" },
    },
  },
};
```

In Tailwind v4, declare the same mapping in CSS, after importing `theme.css`. Use
`@theme inline { --color-page: var(--bg-page); --color-card: var(--bg-card); --color-accent: var(--accent); … }`.

### 8.3 Component libraries (MUI, Chakra, Ant Design, shadcn/ui, Vuetify and others)

Set the library's dark theme from the tokens:

| Theme slot | Value |
|---|---|
| Mode | Dark |
| Primary | `#22B5DC` (hover `#4FCBEA`); text on primary `#111A2B` |
| Background: default / paper (surface) | `#0A101C` / `#111A2B` |
| Text: primary / secondary / disabled | `#E8EEF7` / `#A3B1C6` / `#8C9BB2` |
| Divider / border | `#1F2B42` (controls `#3A4A68`) |
| Success / warning / error / info | `#34D399` / `#F2B33D` / `#F87171` / `#6B9BFF` |
| Radius | 8 px for controls, 12 px for cards |
| Typography | IBM Plex Sans, 14 px base, h1 28 / 600; code in IBM Plex Mono |
| Elevation | Off, except the card shadow (§3.8) |
| Density | Controls 40 px, table rows 52 px |

Then turn off ripples, gradients and glass effects, and restyle tables, chips and tags to match §4.

### 8.4 Charts (an extension: the original dashboard has no charts)

- **Series colours,** in this order: `#22B5DC`, `#6B9BFF`, `#A78BFA`, `#34D399`, `#F2B33D`, `#F87171`,
  `#94A3B8`. When a series *is* a status (failures, for example), use that status tone instead.
- **Plot area:** draw on the card background with a transparent canvas.
  - Grid lines `#19233A`, axis lines `#1F2B42`.
  - Tick labels: mono 11 to 12 px, `#8C9BB2`.
  - Axis titles: 12 px, `#A3B1C6`.
- **Tooltip:** background `#16223A`, 1 px `#3A4A68` border, radius 8, 12 px `#E8EEF7` text, values in mono.
- **Lines** are 1.5 to 2 px. Area fills use the series colour at about 15 % opacity. No 3D, no gradients, no drop
  shadows.

### 8.5 Tools that are not web apps

- For tools with their own theming (Grafana, Superset, Metabase, Streamlit, Qt, Flutter, SwiftUI and others),
  set:
  - dark mode;
  - the page background `#0A101C` and panel/card background `#111A2B`;
  - text `#E8EEF7` and secondary text `#A3B1C6`;
  - primary `#22B5DC`;
  - status thresholds from the tones: ok `#34D399`, warn `#F2B33D`, fail `#F87171`;
  - fonts IBM Plex Sans and Mono;
  - radii 8 / 12;
  - the §8.4 chart colours.
- Native sizes: read the CSS px values as dp (Android) or pt (iOS).
- Streamlit example (`.streamlit/config.toml`):

  ```toml
  [theme]
  base = "dark"
  primaryColor = "#22B5DC"
  backgroundColor = "#0A101C"
  secondaryBackgroundColor = "#111A2B"
  textColor = "#E8EEF7"
  font = "sans serif"
  ```

## 9. Behaviour conventions

- **Polling:** refresh about every 30 s and show `Updated HH:MM:SS <zone>`.
  - If a refresh fails, keep the last data on screen, change the text to `Last refresh failed at HH:MM:SS`, and
    turn it amber (`.updated.is-stale`).
  - The Refresh button polls immediately.
- **No flicker, no stolen focus:** key rows by id. Re-render a row only when its data changed, and never while it
  holds keyboard focus. Update tile counts in place.
- **Tiles are filters:** exactly one tile is pressed (`aria-pressed`), and "All" comes first. A count of 0 keeps
  the normal look.
- **Row selection:** a click anywhere on a row clicks the `.row-btn` in its first cell. The row gets
  `.is-selected` and the detail panel shows that item. Under 1280 px, the panel moves below the table.
- **Nothing is hidden silently:** the summary counts skipped or filtered-out items ("38 skipped · Show", a
  `.btn--link` with `aria-pressed`). When they are shown, they appear as muted rows (`tr.is-muted`).
- **States:**
  - *Loading:* three skeleton rows, with `aria-busy="true"` on the table.
  - *Empty:* one sentence plus a link to the next step.
  - *Error:* a `.notice--error` with the code, the message and a Retry button.
- **Sidebar badges** count items that need action. A badge turns amber (`.nav-badge--warn`) when something is
  retrying, and is hidden at 0. Put the count in the link's `aria-label` too.
- **Services box:** the server decides each row's state, and the right-hand text always names it ("Idle", "OK",
  "503 at 08:31").
  - Before the first answer, a row shows "Checking…" in grey.
  - If the backend cannot be reached, rows show "Unknown" and a banner says the page keeps retrying.
- **Density:** `body.compact` makes rows 44 px high.
- **Screen width:** pages are designed at 1440 px and work down to about 1280 px. Below that, the detail panel
  stacks under the table, and the summary under the title wraps before the toolbar moves.

## 10. Done checklist

- [ ] The body background is `#0A101C` (computed `rgb(10, 16, 28)`). The sidebar is 232 px wide on `#070C16`.
      `document.fonts.check('14px "IBM Plex Sans"')` returns `true`.
- [ ] There are no colour literals outside the tokens (run the grep in §2).
- [ ] There is at most one primary button per view.
- [ ] Every state maps to one of the six tones. Every pill and tile has text. Every track has an `aria-label`.
- [ ] Ids, codes, counts and times are mono, and the time zone is shown once.
- [ ] When you press Tab through the page, the azure focus ring shows everywhere (light azure inside the sidebar).
- [ ] Labels are in sentence case. Large areas are navy, and status colour sits only on small elements.
- [ ] Render the §6 skeleton unchanged at 1440 × 900 next to your page. The sidebar, header, tiles, table and
      panel geometry match.
