# Talaria Design System v2.1: implementation report

**Audience:** a Cursor agent working in the real `new_talaria_flutter` / `new_talaria_ui` repos.
**Owner:** Heath Dunlop · **Date:** 11 Oct 2026 (v2.1) · **Copy:** NZ English (colour, behaviour, organisation, licence as a noun).

The reference files sit next to this report:

| File | What it is |
|---|---|
| [`talaria-design-system-v2.html`](./talaria-design-system-v2.html) | The visual spec. One self-contained file; only Google Fonts load from outside. Use the Light/Dark/System toggle; `?theme=dark`, `?solo=<section-id>`, `?split=<section-id>\|all` (light and dark side by side), `?vp=phone\|tablet\|desktop\|wide` (or `390`/`820`/`1280`/`1440`) to set every viewport switcher, `?sheet=issues\|issue\|overview\|settings` for the responsive overview, `?full=1` for full-bleed. The guide is itself responsive (sections drawer below 1100px). |
| [`tokens.json`](./tokens.json) | Source-of-truth tokens: primitive ramps, semantic light/dark (with refs and computed contrast), tones, chart, type, spacing, radius, layout, elevation, motion, focus, opacity, rules. |
| [`talaria_tokens.dart`](./talaria_tokens.dart) | Drop-in Dart: 11 ThemeExtensions (v2.1 adds `TalariaBreakpoints`) with `light`/`dark` consts, `copyWith`, `lerp`, plus `talariaExtensions()`, `talariaColorScheme()` and `context.tColors` and the other accessors. v2.1 adds `TalariaSizeClass`, `TalariaLayout`, `context.sizeClass` / `tLayout` / `tTextAdaptive` / `responsive<T>()`, `TalariaAdaptiveBuilder`, `TalariaAdaptiveShell`, `showTalariaPicker` and `showTalariaFormDialog`. Checked with `dart analyze` against Flutter API stubs, not yet compiled against real Flutter. Needs Flutter ≥ 3.27 (`Color.withValues`). |
| [`previews/`](./previews/) | PNGs in both themes: guide sections (`{theme}-{section}.png`), device frames at 2x (`{theme}-{page}-mobile.png`, `-tablet.png`, plus events, AI drawer, filters sheet and More drawer on mobile), `responsive-sheet-{theme}.png` (Issues at 390 / 820 / 1280 / 1440 at true scale) and `responsive-sheet-overview-{theme}.png`, `guide-on-phone-*.png`, `split-*.png`. |
| [`source/`](./source/) | The generator and source of truth: `tokens.py` → `build_tokens.py`, `build_dart.py` (+ `dart_responsive.py`), `build_html.py` (+ `pages.py` responsive page markup, `resp_sections.py`, `charts.py`, `resp.css`, `guide.css`, `guide.js`, `body1–4.html`), `previews.sh` / `shot.js` (headless Chrome), and `dart_stubs/` (`check.sh` runs `dart analyze` against Flutter API stubs). Edit `tokens.py` and regenerate rather than hand-editing outputs. |

> **Provenance warning.** v1 values came from the compiled production bundle (`main.dart.js`), where class names are minified. Names like `TalariaColors.elevated` are *inferred*. **Grep for the hex values and v1 class names below to find the real identifiers** before renaming anything.

> **Data hygiene.** Golden tests, screenshots and fixtures must use fictional data (the guide uses "Northline" / "Talaria Labs" / "Alex Chen"). Never capture real issue titles that contain secrets, real user-picker emails or real customer org names.

---

## v2.1 changelog

**Responsive (new).** Four size classes as tokens (compact < 600, medium 600–899, expanded 900–1439, large ≥ 1440) with grid, margins, navigation, density, input mode, table mode, overlays, pagination and chart ticks for each (§2.7). Adaptive shell: sidebar 240 / 56 → rail 72 → bottom bar (4 + More) with a modal drawer. Every component in the guide has a compact/medium/expanded spec, and all page mockups are live container-query layouts with a Mobile 390 / Tablet 820 / Desktop 1280 / Wide 1440 switcher in device frames.

**Polish.**
- Optical icon strokes: sm 2, default 1.75, lg 1.5, xl 1.4, in badges 2.25.
- Strict 4 pt rhythm: the 14 px paddings (card header, stat, banner, frame, insights strip) are now 12 or 16. Section sub-heads are 48 above and 16 below.
- Sidebar items are 32 (was 30), group gaps are 16, and the `[` shortcut chip is hidden when collapsed.
- Dark mode: e1 is quieter (`0 1px 2px rgba(0,0,0,.5)` + 3% top highlight), overlay borders use `neutral.750`, and cards get a 2.5% inner top highlight instead of heavier shadows.
- Charts: HTML axes at a true 12 px (they were scaling down to about 6 px with the SVG), 1 px non-scaling gridlines and strokes, hover tooltip with a crosshair and dimmed siblings, and a dashed prior-period ghost.
- New empty-state art (halo, tilted cards, glyph badge; replay variant with faint blades), also used on the 404.
- Toast stacking: newer toasts sit above older ones, so shadows no longer smear.
- Micro-interaction spec table (`tokens.json → interactions`) on top of the motion tokens.
- Copy: "Filters (n)", "Load 16 more", "Swipe for earlier days" and consistent sentence case. The issue rail is 300 (340 at wide), and the lede now says so.

---

## 1. Summary and goals

v2 keeps the brand people already recognise: near-black and white neutrals, `#0070F3`, Inter and JetBrains Mono, and Vercel-grade restraint. It fixes the inconsistencies that slow triage down.

| Goal | v1 problem | v2 answer |
|---|---|---|
| One red | Three red systems (`#E5484D`, `#DC2626`/`#F87171`, `#BE123C`/`#FF4D6D`) | One `red` ramp. `danger` = `#D13438` fill (white text 4.93:1), `dangerFg` `#C42B30` / `#FF7B7E`. `#E5484D` lives on as `red.500` (charts). |
| Warning keeps its hue | `#F5A623` dark vs `#ED5F00` light (orange, close to red) | `warning` = `#F5A623` in **both** themes. Text `#9A5B00` / `#F7B541`; solid fills carry **dark** text (`warningOnFill`). |
| Error ≠ fatal | Both pinkish-red tints | Error = red tint + ⊗ glyph. Fatal = **solid magenta `#C0267A`** + bolt, white text. |
| Real type scale | 16/20/24 at 600, weak hierarchy | 13 styles: Display 32, Headline 24, Heading 18, Title 15, Body 14 … 12px minimum. |
| One badge casing | `uppercase` flag → `ERROR` vs `error` | Sentence case for semantic labels, mono verbatim for literal values, never uppercase. 12/500 (`captionStrong`). |
| State layers | Ad hoc | Hover 5%/6%, pressed 9%/10%, selected accent 10%/14%, focus ring 2px + 2px gap, disabled tokens, loading state that keeps width and label. |
| AI tokens | Markdown used Material `#2196F3`/`#BBDEFB` | `ai*` (iris `#6A45F0`), `#0070F3→#7C5CFF` gradient, `code*` syntax tokens. |
| Coloured environments | All neutral | `EnvironmentTones`: production teal + solid dot, staging plum + half dot, development neutral + hollow dot, test dashed diamond, other neutral. |
| Honest deltas | Up = red/green by direction | `MetricPolarity` decides the colour; "—" when baseline is empty, absolute change when the baseline is tiny. |
| AA everywhere | Light `fgMuted #737373` (4.6 on `#FAFAFA` borderline), `fgSubtle #A3A3A3` (2.5) | Light `fgMuted #525252` (7.49), `fgSubtle #6B6B6B` (≥4.85 on every surface); dark `fgSubtle #8A8A8A` (≥4.94). |
| Colour-blind-safe charts | Severity tints | Okabe–Ito-derived 8-series `ChartPalette` per theme, sequential + diverging. |
| Density, widths, patterns | Varied per page | Density rules, 4 content widths, one page header, one back link, one filter bar, save bar, danger zone, empty/error/404 with actions, skeleton rules, motion and elevation tokens, labelled nav. |

Brand motif: the logo's **fanned blades**. Use it sparingly: the AI loader ("blade loader"), the hero watermark in the guide, and the 404 illustration. Never as a pattern fill, and never on data surfaces.

---

## 2. Token reference (v2.1)

### 2.1 Colour: semantic (light / dark)

| Token (`tokens.json` → Dart field) | Light | Dark |
|---|---|---|
| `bg.canvas` → `bgCanvas` | `#FAFAFA` | `#0A0A0A` |
| `bg.sidebar` → `bgSidebar` | `#FAFAFA` | `#0A0A0A` |
| `bg.surface` → `bgSurface` | `#FFFFFF` | `#111111` |
| `bg.raised` → `bgRaised` | `#FFFFFF` | `#171717` |
| `bg.muted` → `bgMuted` | `#F4F4F4` | `#1C1C1C` |
| `bg.emphasis` → `bgEmphasis` | `#EDEDED` | `#262626` |
| `bg.inverse` → `bgInverse` | `#171717` | `#FAFAFA` |
| `border.default` → `border` | `#E5E5E5` | `#262626` |
| `border.strong` → `borderStrong` | `#D4D4D4` | `#404040` |
| `border.subtle` → `borderSubtle` | `#EDEDED` | `#1F1F1F` |
| `fg.default` → `fg` | `#171717` | `#FAFAFA` |
| `fg.muted` → `fgMuted` | `#525252` | `#A3A3A3` |
| `fg.subtle` → `fgSubtle` | `#6B6B6B` | `#8A8A8A` |
| `fg.disabled` → `fgDisabled` | `#A3A3A3` | `#525252` |
| `fg.onAccent` → `fgOnAccent` | `#FFFFFF` | `#FFFFFF` |
| `fg.inverse` → `fgInverse` | `#FAFAFA` | `#171717` |
| `accent.default` → `accent` | `#0070F3` | `#0070F3` |
| `accent.hover` / `accent.pressed` | `#0062D6` / `#0052B4` | `#0062D6` / `#0052B4` |
| `accent.fg` → `accentFg` (links, selected nav text) | `#0062D6` | `#4D9BFF` |
| `accent.soft` / `accent.border` | `#EFF6FF` / `#BCD7FF` | `#0A1A33` / `#0B3A7A` |
| `focus.ring` → `focusRing` | `#0070F3` | `#4D9BFF` |
| `danger.default` / `danger.hover` | `#D13438` / `#C42B30` | `#D13438` / `#C42B30` |
| `danger.fg` / `danger.soft` / `danger.border` | `#C42B30` / `#FFF2F2` / `#FFC9CA` | `#FF7B7E` / `#2A0F11` / `#5C1518` |
| `warning.default` / `warning.fg` | `#F5A623` / `#9A5B00` | `#F5A623` / `#F7B541` |
| `warning.soft` / `warning.border` / `warning.onFill` | `#FFF8EB` / `#FFDD99` / `#171717` | `#241703` / `#4A2C00` / `#171717` |
| `success.default` / `success.fg` | `#1E9A5E` / `#157A4A` | `#3DD68C` / `#3DD68C` |
| `success.soft` / `success.border` | `#EDFCF4` / `#A6ECC8` | `#061F14` / `#0B3B25` |
| `fatal.default` / `fatal.fg` | `#C0267A` / `#A01F67` | `#C0267A` / `#F472B6` |
| `ai.default` / `ai.fg` | `#6A45F0` / `#5A34D6` | `#6A45F0` / `#B7A8FF` |
| `ai.soft` / `ai.border` | `#F4F2FF` / `#D5CCFF` | `#160F38` / `#2C1B6E` |
| `ai.gradientStart` → `End` | `#0070F3` → `#7C5CFF` | `#4D9BFF` → `#9A86FF` |
| `code.keyword/string/number/function/type/comment` | `#5A34D6` `#157A4A` `#9A5B00` `#0062D6` `#0F766E` `#6B6B6B` | `#B7A8FF` `#6FDDA7` `#FFC95C` `#8DBBFF` `#5FDDC8` `#8A8A8A` |
| `state.hover` / `pressed` / `selected` / `dragged` | fg 5% / 9% / accent 10% / fg 14% | fg 6% / 10% / accentFg 14% / fg 16% |
| `scrim` / `selection` | `#0A0A0A` 40% / accent 20% | `#000000` 64% / `#4D9BFF` 28% |
| `skeleton.base` / `highlight` | `#EDEDED` / `#F7F7F7` | `#1C1C1C` / `#262626` |
| `scrollbar` | fg 28% | fg 24% |

All text tokens pass AA (≥4.5:1) on canvas, surface, raised and muted in both themes. The exception is `fgDisabled`: disabled controls are exempt, and it is kept at ≥2.2:1. Contrast numbers are in `tokens.json` under each token's `contrast`.

### 2.2 Tones
`TalariaTone { bg, fg, border, glyph, solid, dashed }`. Every badge pairs a glyph with a label, so meaning never depends on colour alone.
- **SeverityTones:** trace, debug (neutral), info (blue), warning (amber, △), error (red tint, ⊗), fatal (**solid** magenta, bolt). Severity tones now **lerp**; v1 snapped at t=0.5.
- **StatusTones:** open (blue ○), regressed (red ↻, new), resolved (green ✓), ignored (neutral ⊘), healthy, degraded, down, unknown, processing (spinner). `byName(String)`.
- **EnvironmentTones:** production, staging, development, test, other. `forEnvironment(String)` maps case-insensitively and trims whitespace: `production`/`prod`/`live`, `staging`/`stage`/`preprod`/`uat`, `development`/`dev`/`local`, `test`/`testing`/`ci`. Anything else maps to `other`. Values are shown in **mono, verbatim**.
- Rule: a row carries **at most one tinted badge**. The others become quiet text with a glyph.

### 2.3 Charts (`ChartPalette`)
- Categorical, light: `#0070F3 #D46B08 #0D9488 #C0267A #A68A00 #6A45F0 #C2410C #737373`
- Categorical, dark: `#4D9BFF #FF9F43 #2DD4BF #F472B6 #E8D44D #9A86FF #FF7B54 #A3A3A3`
- Use series in order. Series 1–4 stay pairwise distinguishable under protanopia, deuteranopia and tritanopia. Beyond five series, group the rest as "Other" (grey).
- Severity series use `ChartPalette.trace…fatal` (not tone.fg). Area fill opacity 16%. Lines 2px. 5–6 ticks max, no repeated date labels, tabular numbers. Each chart has an aria summary and a "View as table" toggle.

### 2.4 Typography (`TalariaTextStyles`)
Inter for UI, JetBrains Mono for code and identifiers. 12px minimum. No uppercase anywhere. Numbers use `FontFeature.tabularFigures()`.

| Style | Size/line | Weight | Tracking |
|---|---|---|---|
| display | 32/40 | 600 | −0.022em |
| headline | 24/32 | 600 | −0.019em |
| heading | 18/26 | 600 | −0.012em |
| title | 15/22 | 600 | −0.006em |
| body | 14/20 | 400 | 0 |
| bodyStrong | 14/20 | 500 | 0 |
| label | 13/18 | 500 | 0 |
| caption | 12/16 | 400 | 0 |
| captionStrong | 12/16 | 500 | 0 |
| metric | 28/32 | 600, tnum | −0.02em |
| metricSm | 20/28 | 600, tnum | −0.01em |
| mono | 13/20 | 400 | 0 |
| monoSm | 12/16 | 400 | 0 |

### 2.5 Spacing, radii, layout, density
- **Spacing:** `s0 0, s0_5 2, s1 4, s2 8, s3 12, s4 16, s5 20, s6 24, s8 32, s10 40, s12 48, s16 64`.
- **Radii:** `xs 4, sm 6, md 8, lg 12, xl 16, full 999`. ⚠ v1 `sm` (4) is now `xs`, and v1 `md` (6) is now `sm`. Badges, chips and tooltips use xs. Buttons and inputs use sm. Cards, menus and dialogs use md. Sheets and drawers use lg. Marketing panels use xl.
- **Controls:** heights sm 28, md 32 (default), lg 40. Hit target ≥ 44 via `MaterialTapTargetSize`/padding on touch; on desktop pointer 32 is fine, but keep icon-only buttons at 32 visual / 44 hit.
- **Rows (`rowHeight(TalariaDensity)`):** compact 36 (logs, events, tables), standard 48 (issues list), comfortable 56–64 (cards, project lists). Data tables default to compact on desktop and standard on touch.
- **Content widths (`TalariaContentWidth`):** narrow 720 (settings forms, docs), standard 1200 (most pages), wide 1440 (dashboards, issue detail), fluid (tables, replays, traces). Page margin 32 desktop, 40 wide, 24 tablet, 16 phone (§2.7). Sidebar 56 collapsed / 240 expanded. Top bar 48.

### 2.6 Elevation and motion
- **Elevation `e0–e4`:** e0 flat (cards use a border, no shadow). e1 sticky headers. e2 menus, popovers and select. e3 dialogs and drawers. e4 toasts and command palette. Dark adds a 1px top inner highlight (`topHighlight`) because shadows read poorly on black.
- **Motion (`TalariaMotion`):**
  - Durations: `fast 100` (hover, colour, focus), `base 160` (press, toggle, tabs), `moderate 240` (menus, popovers, drawer enter, toasts), `slow 320` (dialogs, page and focus-trail transitions), `deliberate 480` (chart draw-in), all in ms.
  - Curves: standard `(0.2,0,0,1)`, enter `(0.05,0.7,0.1,1)`, exit `(0.3,0,1,1)`.
  - Other timings: `shimmerPeriod 1200`, `tooltipWait 400`, `skeletonDelay 150`, `skeletonMinShow 300`.
  - `TalariaMotion.reduced` uses linear curves, no shimmer and no chart draw-in, and caps everything else at a 100ms fade. `context.tMotion` returns it automatically when `MediaQuery.disableAnimations` is true.

### 2.7 Breakpoints, layout and adaptive type (v2.1)

| Class | Width | Alias / preview | M3 window class | Columns · gutter · margin | Navigation | Top bar | Input | Content |
|---|---|---|---|---|---|---|---|---|
| `compact` | 0–599 | mobile · 390 | Compact | 4 · 16 · 16 | Bottom bar 64 (4 + More) + modal drawer | 56: back or project, title, env, search | touch | fluid |
| `medium` | 600–899 | tablet · 820 | Medium (+ low Expanded) | 8 · 16 · 24 | Rail 72, labels on + modal drawer | 56: crumbs (no org), env, search icon | touch | fluid |
| `expanded` | 900–1439 | desktop · 1280 | Expanded / Large | 12 · 24 · 32 | Sidebar 240, collapses to 56 with tooltips | 48 | pointer | max 1200 |
| `large` | ≥ 1440 | wide · 1440 | Large / Extra-large | 12 · 24 · 40 | Sidebar 240 / 56 | 48 | pointer | max 1440 |

Other thresholds (`TalariaBreakpoints`): `drawerFullscreenBelow 720`, `replaySplitAt 960`, `sheetMaxWidth 640`.

**Component behaviour per class** (`TalariaLayout`):

| | compact | medium | expanded / large |
|---|---|---|---|
| Issue / data tables | stacked cards (title, message, wrapping meta, count with label) | sparkline column dropped | all columns |
| Filter bar | search + `Filters (n)` → bottom sheet; status scrolls under it | search + status + `Filters (n)` | inline chips |
| Page header | primary action fills the row, 1 action + ⋯ | 2 + ⋯ | 3 + ⋯ |
| Tabs | scroll with edge fade | scroll | fixed |
| Detail rail | status + assignee, then "More details" (collapsible) | 2-column card above content | side 300 / 340 |
| Pagination | load more | load more | numbered pages |
| Dialogs | forms: full screen; confirms and pickers: bottom sheet | centred; pickers in a sheet | centred |
| Drawers (AI, event) | full screen (below 720) | side 400–420 | side 420–480 |
| Env / project switcher | bottom sheet | bottom sheet | menu |
| Toast | full width, 12 above the bottom bar | bottom-centre | bottom-centre |
| Charts | 3 y-ticks, horizontal swipe (newest pinned right), no hover tooltip | 5 x / 4 y | 6–8 x / 5 y |
| Billing plans | stacked | stacked | 3 columns |

**Adaptive type** (`adaptiveType`, `TalariaTextStyles.forSizeClass`). Only these styles change:

| Style | compact | medium | expanded | large |
|---|---|---|---|---|
| display | 28/36 | 30/38 | 32/40 | 36/44 |
| headline | 22/28 | 24/32 | 24/32 | 24/32 |
| heading | 17/24 | 18/26 | 18/26 | 18/26 |
| metric | 24/28 | 26/32 | 28/32 | 28/32 |

**Touch** (`touch`): min target 48 (44 iOS); control heights pointer 28/32/40, touch 36/44/48; row min pointer 48 / touch 56 / card 72; nav item sidebar 32 / rail 56 / bottom bar 64; 16 px input text on touch (stops iOS Safari zoom). Hover styles only apply under `(hover: hover) and (pointer: fine)`.

**Safe areas** (`safeArea`): pad the top app bar by the top inset; pad the bottom bar, sheets, the save bar and the AI composer by the bottom inset; landscape phones also pad left and right.

---

## 3. Migration map (v1 → v2)

Grep for the hex values (both `0xFF…` and `#…`) and replace each usage with the semantic token. Never paste the new hex values into widgets.

### 3.1 TalariaColors (v1 values from the compiled bundle)

| v1 name (inferred) | v1 dark / light | v2 token | Notes |
|---|---|---|---|
| `bg` | `0xFF0A0A0A` / `0xFFFFFFFF` | `bgCanvas` | Light canvas is now `#FAFAFA` with white cards; this gives cards a visible edge. |
| `elevated` | `0xFF111111` / `0xFFFAFAFA` | `bgSurface` (cards, panels) or `bgRaised` (menus, dialogs) | Split by role. |
| `muted` | `0xFF171717` / `0xFFF5F5F5` | `bgMuted` | Dark muted `#1C1C1C` so inputs separate from raised `#171717`. |
| `border` | `0xFF262626` / `0xFFE5E5E5` | `border` | Unchanged. |
| `borderStrong` | `0xFF404040` / `0xFFD4D4D4` | `borderStrong` | Unchanged. |
| `fg` | `0xFFFAFAFA` / `0xFF171717` | `fg` | Unchanged. |
| `fgMuted` | `0xFFA3A3A3` / `0xFF737373` | `fgMuted` | **Light changes to `#525252`.** |
| `fgSubtle` | `0xFF737373` / `0xFFA3A3A3` | `fgSubtle` | **Dark `#8A8A8A`, light `#6B6B6B`.** v1 light failed AA. |
| (disabled used fgMuted) | — | `fgDisabled` | New. |
| `accent` | `0xFF0070F3` | `accent` (fills), `accentFg` (text, links) | Never use `accent` for text on dark; use `accentFg #4D9BFF`. |
| `onAccent`, `onDanger` | `0xFFFFFFFF` | `fgOnAccent` | Deprecated getters kept for one release. |
| `accentSoft` | `#0070F3` @15% | `accentSoft` (opaque) / `stateSelected` (overlay) | Opaque so text on it has fixed contrast. |
| `danger` | `0xFFE5484D` | `danger` `#D13438` (fills), `dangerFg` (text) | `#E5484D` → `TalariaPalette.red500`, charts only. |
| `success` | `0xFF3DD68C` / `0xFF30A46C` | `success` (icons), `successFg` (text) | Light text `#157A4A` (v1 `#30A46C` was 3.2:1). |
| `warning` | `0xFFF5A623` / `0xFFED5F00` | `warning`, `warningFg`, `warningOnFill` | **Remove `#ED5F00`.** |
| tooltip/snackbar inverse (`0xFFFFFFFF`+`0xFF171717` / `0xFF0A0A0A`+`0xFFFAFAFA`) | — | `bgInverse` / `fgInverse` | |
| modal barrier black 54% / 35% | — | `scrim` | |
| text selection accent @24% | — | `selection` | |
| scrollbar fgSubtle @55% | — | `scrollbar` | |
| shimmer extension (muted → border, 1.2s) | — | `skeletonBase` / `skeletonHighlight`, `TalariaMotion.shimmerPeriod` (1.2s) | Fold the 7th extension into these. |
| Markdown `0xFF2196F3`, `0xFFBBDEFB`, `"monospace"` 0.85× | — | `codeFunction`/`accentFg` for links, `bgMuted` for code bg, `TalariaTextStyles.monoSm` | Also apply `code*` syntax colours. |

### 3.2 Zinc and off-palette hex values to delete
Every one of these should disappear from `lib/` (except tests):
`#18181B #1A1A1F #A1A1AA #3F3F46 #52525B #71717A #D4D4D8 #E4E4E7 #F4F4F5` (zinc → neutral),
`#60A5FA #1D4ED8 #BFDBFE #0C1929 #1E3A5F` (info and open → `SeverityTones.info` / `StatusTones.open`),
`#F87171 #DC2626 #FECACA #FEF2F2 #2A1215 #7F1D1D` (error and down → `.error` / `.down`),
`#FF4D6D #BE123C #FFF1F2 #FECDD3 #2D0A14 #9F1239` (fatal → `SeverityTones.fatal`),
`#C2410C #FFF7ED #FED7AA #271C0B #5C3D0E` (warning and degraded → `.warning` / `.degraded`),
`#047857 #ECFDF5 #A7F3D0 #0D2818 #14532D` (resolved and healthy → `.resolved` / `.healthy`),
`#ED5F00 #E5484D #30A46C #2196F3 #BBDEFB`.

Run: `rg -n -i "0xFF(18181B|1A1A1F|A1A1AA|3F3F46|52525B|71717A|D4D4D8|E4E4E7|F4F4F5|60A5FA|1D4ED8|BFDBFE|0C1929|1E3A5F|F87171|DC2626|FECACA|FEF2F2|2A1215|7F1D1D|FF4D6D|BE123C|FFF1F2|FECDD3|2D0A14|9F1239|C2410C|FFF7ED|FED7AA|271C0B|5C3D0E|047857|ECFDF5|A7F3D0|0D2818|14532D|ED5F00|E5484D|30A46C|2196F3|BBDEFB)" lib packages`

### 3.3 Text variants (`AppTextVariant`)

| v1 | v2 |
|---|---|
| display 24/30 | `headline` 24/32 (page titles). New `display` 32 is for empty or hero states only. |
| heading 20/26 | `heading` 18/26 (section titles) |
| title 16/22 | `title` 15/22 (card titles) |
| body, bodyStrong | unchanged |
| labelSm 12/500 | `captionStrong` (badges, table headers) or `caption` 12/400 (metadata) |
| (none) | `label` 13/500: buttons sm, form labels, tabs, nav items |
| mono 13/19.5 | `mono` 13/20 |
| monoSm 12/18 | `monoSm` 12/16 |
| Badge 11px, 0.04 tracking, `uppercase` | `captionStrong` 12/16, Sentence case. **Delete the `uppercase` parameter** from `AppBadge` (or deprecate it as a no-op). |

### 3.4 Spacing, radii and others
- Spacing values are kept; `s0_5` (2), `s5` (20), `s10` (40) and `s16` (64) are new. Map v1 fields by value.
- Radii: v1 `4 → xs`, `6 → sm`, `8 → md`, `12 → lg`. **Watch the name shift.** Grep `radii.sm` and check each one.
- `AppButton` min height 36 → md 32 (sm 28, lg 40). `AppIconButton` 44 → 32 visual with a 44 hit area.
- Sidebar 72 → 56 collapsed, and 240 expanded with **labels**.
- Input focus border 1.5px accent → 1px `focusRing` border plus the 2px focus ring for keyboard focus only.

### 3.5 Navigation widths (v2.1)
- v1's 72 px collapsed sidebar becomes **56** on desktop (`sidebarCollapsed`). 72 is now reserved for the **tablet NavigationRail**, which always shows labels, so don't reuse the old collapsed widget for tablet.
- The old "mobile = desktop sidebar in a drawer" pattern is replaced by the bottom bar (Overview, Issues, Events, Replays, More). Everything else lives in the More drawer.
- The env switcher moves from top-bar dropdown to a bottom sheet below 900. Keep one `EnvironmentController` so both surfaces share state.

---

## 4. Phased tasks

Each phase is a separate PR. Don't start the next phase until the current phase's acceptance criteria pass in **both themes** and in **custom seed** mode.

### Phase 0: style-guide infrastructure (`/_dev/style-guide`)
Do this first, so every later change can be checked on one screen.

| # | Task | Acceptance criteria |
|---|---|---|
| 0.1 | **Live token tables.** Render every `TalariaColors` field, each tone, `ChartPalette`, the type scale, spacing, radii, elevation and motion, *reading the live ThemeExtension* (not hard-coded lists). | Changing a value in `talaria_tokens.dart` changes the table on hot reload. Each colour row shows name, hex and contrast vs canvas, surface and muted (see 0.7). Custom seed mode shows derived values. |
| 0.2 | **State matrix.** For each interactive widget (button × 5 variants × 3 sizes, icon button, input, select, checkbox, radio, switch, chip, segmented, tab, nav item, row), show rest, hover, pressed, focus, selected, disabled, loading and error. Force states with `WidgetStatesController` / a `debugForcedStates` parameter. | The matrix matches the "Buttons" and "Inputs" sections of the HTML guide. Loading keeps width and label. |
| 0.3 | **Per-section light/dark side-by-side toggle.** Wrap a section in `Row(children:[Theme(data: light, child: s), Theme(data: dark, child: s)])`. | Toggling doesn't change the global appearance. Works for every section. |
| 0.4 | **Page-template gallery.** Static, fixture-driven pages: Issues list, Issue detail, Project overview, Settings (General), empty, error, 404 and loading skeleton for each. | Each template is reachable by deep link (see 0.9). No network calls. Fictional data only. |
| 0.5 | **Golden tests in both themes.** `flutter test --update-goldens` per section and template, at 1440×900 and 390×844, light and dark. Bundle Inter and JetBrains Mono as test assets and set `GoogleFonts.config.allowRuntimeFetching = false` in `flutter_test_config.dart`. | CI fails on a pixel diff. Goldens are stored under `test/goldens/{light,dark}/`. |
| 0.6 | **Tab title fix.** The style guide tab reads "Talaria". Use `Title(title: 'Design system · Talaria')` and the breadcrumb "Design system". Apply the pattern `<Page> · <Project> · Talaria` app-wide. | Every route sets a distinct title. Verify via `document.title` in an integration test. |
| 0.7 | **Contrast checker.** A debug helper `contrastRatio(Color a, Color b)` plus a test that asserts every `*Fg`, `fg`, `fgMuted`, `fgSubtle`, tone fg on tone bg, and `fgOnAccent` on fills is ≥ 4.5 (`fgDisabled` ≥ 2.2), for light, dark, and 3 sample seeds in custom mode. | Test passes. The table in 0.1 flags any pair below AA in red. |
| 0.8 | **A11y semantics default-on review.** Call `SemanticsBinding.instance.ensureSemantics()` in debug/profile web builds (and behind a `?a11y=1` flag in release) so the semantics tree is always built for review. Then audit: every icon button has a tooltip/label, badges expose "Severity: Error", charts expose a summary, and focus order follows visual order. | Use axe or Chrome's accessibility tree on the style-guide route: no unlabelled buttons and no missing names. |
| 0.9 | **Isolated widget routes and deep links.** `/_dev/style-guide/<section>?theme=dark&state=hover&density=compact`. | Each section and template opens directly, which makes screenshots scriptable. |

### Phase 1: tokens and ThemeData

| # | Task | Acceptance criteria |
|---|---|---|
| 1.1 | Copy `talaria_tokens.dart` into `new_talaria_ui/lib/src/theme/`. Remove its duplicate class names from the v1 file, and keep v1 field names as `@Deprecated` getters (already provided for `bg`, `elevated`, `muted`, `onAccent`, `onDanger`). | `flutter analyze` is clean, apart from deprecation infos. |
| 1.2 | Update `buildTheme(Brightness, ColorScheme? custom, TalariaColors tokens)`: `extensions: talariaExtensions(b, custom: custom, reduceMotion: …)`; `colorScheme: custom ?? talariaColorScheme(b)`; `scaffoldBackgroundColor: c.bgCanvas`; `textTheme` mapped from `TalariaTextStyles` (displaySmall=headline, titleLarge=heading, titleMedium=title, bodyMedium=body, labelLarge=label, bodySmall=caption, labelSmall=captionStrong). | Stock Material widgets (DatePicker, Dialog, Menu) match Talaria widgets in both themes. |
| 1.3 | Component themes: Input (bgMuted fill, border 1px, hover borderStrong, focus focusRing, error dangerFg message + danger border), Card (bgSurface, e0, md, border), Dialog (bgRaised, e3, md), Menu/Popover (bgRaised, e2), Tooltip (bgInverse/fgInverse, xs, caption, `tooltipWait` 400ms), SnackBar (bgInverse, e4, sm), Divider (border), Scrollbar (scrollbar, r full), TextSelection (selection, cursor accent), NavigationBar indicator (accentSoft). Overlays use `overlayColor: TalariaStates.overlay(c)`, which is a ready-made `WidgetStateProperty`. | Matches the "Overlays" and "Inputs" sections. |
| 1.4 | Replace every hex listed in §3.2. Replace SeverityTones and StatusTones values with v2, and register `EnvironmentTones` and `ChartPalette`. | The `rg` in §3.2 returns no hits in `lib/`. |
| 1.5 | Type: map `AppTextVariant` per §3.3. Remove the badge `uppercase` flag. | No `toUpperCase()` in badge, label or tab code (`rg -n "toUpperCase\(\)" lib`). |
| 1.6 | **Custom seed mapping.** `TalariaColors.fromColorScheme(cs)` (provided) starts from the v2 light or dark base and overrides only the neutrals and accent. Light: bgCanvas = surfaceContainerLow, bgSurface and bgRaised = surfaceContainerLowest. Dark: bgCanvas = surfaceContainerLowest, bgSurface = surfaceContainerLow, bgRaised = surfaceContainer. Both: bgMuted = surfaceContainerHigh, bgEmphasis = surfaceContainerHighest, border = outlineVariant, borderStrong = outline blended 55% over outlineVariant, fg = onSurface, fgMuted = onSurfaceVariant, fgSubtle = onSurfaceVariant lerped 18% toward surface, fgDisabled = onSurface @38%, accent and focusRing = primary, accentFg = primary (dark) or primary lerped 20% toward onPrimaryContainer (light), accentSoft = primaryContainer @60% / @35%, inverse = inverseSurface. **danger, warning, success, fatal, ai, code and all severity, status, environment and chart tones stay fixed**, so meaning doesn't drift with a seed. | The 0.7 contrast test passes for seeds `#0070F3`, `#E5484D` and `#2E7D32` at contrast 0 and 1. If `fgSubtle` fails for a seed, fall back to `fgMuted`. |
| 1.7 | **Fonts.** Keep `google_fonts`, but pin `GoogleFonts.inter`/`jetBrainsMono` through `TalariaTextStyles.withGoogleFonts()`. Consider bundling the two font files (weights 400/500/600) as assets and setting `allowRuntimeFetching=false` in release, to remove the font swap flash and the dependency on fonts.gstatic.com. Use `fontFeatures: [FontFeature.tabularFigures()]` on metric and number styles. | No layout shift after first paint (Lighthouse CLS < 0.02 on /issues). |

### Phase 2: core widgets

| # | Widget | Acceptance criteria |
|---|---|---|
| 2.1 | `AppButton`: primary, secondary, ghost, danger, **ai** × sm/md/lg; leading and trailing icon; split button; `loading` keeps width (spinner replaces the leading icon, label stays, `onPressed` ignored, `Semantics(liveRegion)` "Loading"). | State matrix (0.2) complete. Focus ring visible only for keyboard focus. Disabled uses `fgDisabled` on `bgMuted`. |
| 2.2 | `AppIconButton`: 32 visual / 44 hit. A tooltip is **required** (assert). | Assert fires without a tooltip. |
| 2.3 | `AppBadge(label, tone, size: sm/lg, quiet)`: glyph + Sentence-case text, captionStrong, 20px high (lg 24). Solid when `tone.solid`. Dashed border when `tone.dashed`. `SeverityBadge`, `StatusBadge`, `EnvironmentBadge` (mono), `HealthBadge`, `LivePill`. | The 4 sections in "Badges & chips" match. Semantics label "Severity: Fatal". |
| 2.4 | Inputs: text, search (⌘K hint), select, multiselect, date range, number, textarea, with helper, error and counter states. | Error message uses dangerFg plus an icon, and is announced. |
| 2.5 | Selection: checkbox, radio, switch, segmented (no overflow; disable options rather than hide), tabs with counts, filter chip. | Keyboard: arrows move within groups, Space toggles. |
| 2.6 | `MetricTile(label, value, delta, polarity, comparison)` with a fixed height; use `deltaColor(c, change:, polarity:, baseline:)`. Show "—" when there's no baseline, and the absolute change when the baseline is below 20. | BUG-004: 0 sessions shows "—", not "0.0%" in red. |
| 2.7 | `Skeleton` primitives (line, block, circle, row). Shimmer at `shimmerPeriod` (1.2s) from skeletonBase to skeletonHighlight, with no shimmer under reduced motion. Show skeletons only after `skeletonDelay` (150ms), and keep them for at least `skeletonMinShow` (300ms) so they don't flicker. | Each page skeleton matches its loaded layout box-for-box, so nothing fills in piecemeal. |
| 2.8 | Charts: apply `ChartPalette`, gridlines from `grid`, max 6 ticks, de-duplicated dates, "View as table" toggle, empty state inside the chart frame. | Severity chart colours match the HTML guide. |
| 2.9 | `BladeLoader` (AI only): 6 blades fade in sequence, 1.2s, static under reduced motion. | Only used in AI surfaces. |

### Phase 3: patterns

| # | Pattern | Acceptance criteria |
|---|---|---|
| 3.1 | **App shell + labelled sidebar** (56/240, `[` toggles, state persisted). Groups and icon map: Use the `_outlined` variants. **Observe**: Overview `home`, Dashboards `space_dashboard`, Issues `bug_report`, Events `bolt`, Replays `play_circle_outline`, Whiteboards `draw`. **Analyse**: Performance `speed`, Analytics `bar_chart`, Releases `rocket_launch`, Runtime `memory`. **Configure**: Alerts `notifications_active`, Monitors `monitor_heart`, Flags `flag`, Settings `settings`. **Workspace**: Projects `folder`, Groups `folder_copy`, Team `group`, Billing `credit_card`, Integrations `power`, AI chat `auto_awesome`, Account `account_circle`. The plain bell is only the top-bar inbox. Tag is reserved for tags, and sparkle for AI. **No icon is used twice.** Collapsed mode shows tooltips with the name and shortcut. | Unique-icon test over the nav config. Tooltips present when collapsed. The full map, with what each destination used before, is in the guide's "Iconography" section (`?solo=icons`). |
| 3.2 | **Top bar:** named breadcrumbs (`Org / Project / Section / Item`; never raw IDs; truncate the middle segments), global environment switcher, ⌘K search, notifications, avatar. | BUG-003 fixed on releases, alerts, monitors, whiteboards, runtime and dashboards. |
| 3.3 | **One page header:** optional back link ("← Issues", restores the previous list URL including filters), title (headline), subtitle, meta badges, actions on the right (max 1 primary), optional tabs. The duplicate crumb and the close X are removed on detail pages. | One `AppPageHeader` widget used by every route (`rg "AppPageHeader" lib/features | wc -l` ≈ number of pages). |
| 3.4 | **One filter bar:** search, then chips (Level, Status, Environment, Assignee, Tag, +Filter), date range, sort, and "Clear all". It reads and writes **query parameters** with `router.replace` (not push). Mobile uses ListFilterChrome with the same model. | BUG-009, BUG-011, BUG-012 and BUG-016 fixed: reload keeps filters, Back leaves the page in one step, search text survives reload, defaults aren't written to the URL. |
| 3.5 | **Content widths:** wrap page bodies in `TalariaContentLayout(width: TalariaContentWidth.x)`. Settings = narrow, lists = standard, overview and detail = wide, tables, traces and replays = fluid. | No page stacks gutters (one gutter owned by scroll constraints). |
| 3.6 | **Settings save bar and danger zone:** a sticky bottom bar appears when the form is dirty ("You have unsaved changes · Discard · Save changes"), with ⌘S, a route-leave guard, and inline validation. The danger zone is a red-bordered card at the end, with typed-name confirmation for delete. | Leaving with unsaved changes prompts. Delete requires typing the project slug. |
| 3.7 | **Empty, error and 404:** icon/motif, a heading that says *why* (filter vs time range, BUG-015), and 1 primary + 1 secondary action (e.g. "Clear filters", "Widen to 30 days"). Error states show human copy, a code-styled detail toggle, Retry, and "Copy error". Invalid IDs (BUG-008) show "We couldn't find that issue" + "Back to issues". | No raw exception text by default. Retry is offered only when retrying can help (network or 5xx). |
| 3.8 | **Focus trail:** after closing a drawer, dialog or detail page, focus returns to the invoking row, and the row shows a 2px accent left trail for 2s ("you were here"). Keyboard `j/k` moves between rows, `Enter` opens, `Esc` closes. | Integration test: open issue, Esc, and focus is back on the same row. |
| 3.9 | **Drawers:** right side, 480 default / 720 wide, e3, scrim, Esc closes, focus trapped, URL reflects `?panel=…`. The AI drawer goes fullscreen below 720. | Deep link opens the drawer. Browser Back closes it. |
| 3.10 | **Density:** a `TalariaDensity` provider (compact, standard, comfortable), set per surface, with a user preference only for tables. | Row heights per §2.5. |

### Phase 4: page fixes

| # | Fix | Acceptance criteria |
|---|---|---|
| 4.1 | **Triage-first Issues list:** the list starts above the fold. Insights fold into a single strip (New · Regressed · Unassigned · Escalating, each a filter link). Default sort is Last seen, with bulk actions. | At 1440×900 at least 8 rows are visible without scrolling. |
| 4.2 | **Environment defaulting:** the global env switcher defaults to **All environments** (or the last value remembered per project). Project cards link with the same env used for their counts. Unknown `env` values show a warning chip "Unknown environment 'x'" with a reset action. | BUG-001 and BUG-010: a card's count matches the destination page's count. |
| 4.3 | **Named breadcrumbs** everywhere (see 3.2). | No 8-hex IDs in crumbs. |
| 4.4 | **Filters in the URL** on Issues, Events, Performance, Replays and Releases (see 3.4). | Copying the URL into a new tab reproduces the view. |
| 4.5 | **Honest metrics:** polarity-based deltas, "—" for N/A, no "0.0% crash-free" with 0 sessions (BUG-004), and insight counts that respect the active filters and range (BUG-012, BUG-014). | Unit tests for `deltaColor` and the N/A rules. |
| 4.6 | **404 inside the shell:** unknown routes and unknown sub-pages (BUG-005, BUG-007) render the shell, "Page not found", the path in mono, "Go to overview" + "Search Talaria". | `/nonsense` and `/projects/{id}/doesnotexist` keep the sidebar. |
| 4.7 | **index.html background respects the saved theme.** Add an inline script in `<head>` that, before Flutter boots, reads the saved appearance from `localStorage` and sets `document.documentElement.style.background` and `<meta name="theme-color">`. shared_preferences on web uses keys prefixed `flutter.`. Find the real key, e.g. `rg -n "appearance|themeMode|TalariaAppearanceKind" lib` and then `flutter.<key>`. Values: light `#FAFAFA`, dark `#0A0A0A`, system → `matchMedia`, custom → the persisted seed's surface (store a precomputed `bg` hex alongside the seed JSON to keep this trivial). | Hard reload with a saved Dark theme on an OS in Light mode shows no white flash (record at 60fps). |

Sketch for 4.7:
```html
<script>
(function(){try{
  var raw=localStorage.getItem('flutter.talaria.appearance'); // ← replace with the real key
  var v=raw?JSON.parse(raw):null, kind=(v&&v.kind)||v||'system';
  var dark = kind==='dark' || (kind==='system'&&matchMedia('(prefers-color-scheme: dark)').matches);
  var bg = (v&&v.bg) || (dark?'#0A0A0A':'#FAFAFA');
  document.documentElement.style.background=bg;
  document.querySelector('meta[name=theme-color]')?.setAttribute('content',bg);
}catch(e){}})();
</script>
```

---

### Phase 5: responsive (v2.1)

**5.1 Size class plumbing.**
- Add `TalariaBreakpoints.standard` to `talariaExtensions()` (done in the generated file).
- Use `context.sizeClass` / `context.tLayout` for **window-level** decisions: the shell, navigation, overlays and dialogs. It reads `MediaQuery.sizeOf(context)`, which only rebuilds on size changes, so never use `MediaQuery.of`.
- Use `TalariaAdaptiveBuilder` (a `LayoutBuilder`) for **pane-level** decisions such as tables in a split view, the issue rail or cards inside a drawer, because the pane can be compact while the window is expanded.
- Never branch on `Platform.isIOS` for layout. Use `context.isTouchPlatform` only for sizing and hover rules.

**5.2 Adaptive shell** (`TalariaAdaptiveShell`).
- Expanded and large: the existing sidebar (240 / 56; `[` toggles) plus the 48 top bar.
- Medium: `NavigationRail(labelType: NavigationRailLabelType.all, minWidth: 72)` with the project avatar at the top and More plus Settings pinned at the bottom. The top bar is 56 and drops the org crumb.
- Compact: `NavigationBar` (height 64 + bottom inset) with 4 destinations plus More, which opens a modal `Drawer` holding every group. The `AppBar` is 56 with back (or the project avatar), the title with the project as subtitle, the env chip and search.
- Keep the selected index in the router (go_router `StatefulShellRoute`) so switching class keeps state.
- Preserve scroll position per destination.

**5.3 Overlays.**
- `showTalariaPicker` gives a modal bottom sheet (drag handle, `showDragHandle: true`, `useSafeArea: true`, max width 640) below 900, and a menu or dialog above.
- `showTalariaFormDialog` gives `Dialog.fullscreen` on compact.
- Confirms are bottom sheets on compact with the primary button on top.
- The AI and event drawers are full screen below 720.
- Toasts: a `SnackBar` with `behavior: floating` and a margin that clears the `NavigationBar`.

**5.4 Table → list.** One `IssueRow` widget, three layouts chosen by `TalariaAdaptiveBuilder`:
- full grid;
- no sparkline;
- a 2-line card with the counts kept and labelled ("412 events").

Selection checkboxes move to long-press plus a contextual action bar on touch. Swipe actions (resolve or ignore) are optional, and the overflow menu remains.

**5.5 Filters and pagination.**
- Below 900 the chip row becomes a `Filters (n)` button opening a sheet that edits the same URL-backed filter state, with "Show 41 issues" applying it.
- Status segments scroll horizontally on compact.
- Below 900, swap the pager for `Load 25 more` with cursor pagination. Keep `page=` in the URL on desktop only.

**5.6 Safe areas and touch.**
- `SafeArea` around the compact app bar and bottom bar.
- `MediaQuery.paddingOf(context).bottom` for sheets, the floating save bar and the AI composer.
- Set `materialTapTargetSize: MaterialTapTargetSize.padded` and `visualDensity: VisualDensity.standard` when `isTouchPlatform`, and use `TalariaLayout.controlHeight(size, touch:)` for custom controls.
- Input text is 16 on touch.
- Gate hover-only affordances behind a `MouseRegion` check, and give every icon button a tooltip with a long-press equivalent.

**5.7 Charts on compact.**
- Use the tick counts from `TalariaLayout` (`chartXTicks` and `chartYTicks`).
- Wrap the plot in a horizontal `SingleChildScrollView(reverse: true)` with a minimum width of 560, so the newest day is pinned right. Keep the y-axis fixed outside the scroller.
- Tap shows the tooltip; there is no hover.

**5.8 Testing.**
- Golden tests per size class at **390×844, 820×1180, 1280×800 and 1440×900**, for every page template and the key components (filter bar, issue list, page header with tabs, detail rail, chart, plans, confirm, toast, AI drawer), in light and dark.
- Use `tester.view.physicalSize` and `devicePixelRatio = 1`, plus a 3x phone variant to catch hairlines.
- Add widget tests asserting:
  - no `RenderFlex` overflow at each size;
  - every tappable element is ≥ 48×48 on touch (`meetsGuideline(androidTapTargetGuideline)`, `iOSTapTargetGuideline`);
  - text contrast passes (`textContrastGuideline`).
- Run the web build at 200% zoom at 1280. It must behave like the medium layout, not overflow.

**Acceptance.**
- Every page matches the guide's 390, 820 and 1280 frames.
- There is no horizontal scroll at any width from 320 to 2560, apart from the intentional scrollers: tabs, status segments, chart and code.
- The bottom bar never covers content (the scroll view gets the bottom padding).
- Rotating a tablet keeps state.

---

## 5. Flutter notes

- **ThemeExtension wiring.** `ThemeData(extensions: talariaExtensions(brightness, custom: customScheme, reduceMotion: …))`. Read with `context.tColors`, `context.tText`, `context.tSpace`, `context.tRadii`, `context.tSeverity`, `context.tStatus`, `context.tEnv`, `context.tChart`, `context.tElevation`, `context.tMotion`. Alias these to the existing helpers (v1 compiled as `A.a0/A.ag/A.bn/A.cg/A.wL/A.d1h`), or keep the old names as thin wrappers. All extensions implement `lerp`, so `AnimatedTheme` crossfades work, and tones now interpolate too.
- **Custom seed.** `ColorScheme.fromSeed(seedColor, brightness, dynamicSchemeVariant, contrastLevel)`, then `TalariaColors.fromColorScheme`. Keep semantic tones fixed. Persist `{seed, brightness, variant, contrast, bg}` so index.html can paint the right background (4.7).
- **google_fonts.** `TalariaTextStyles.light/dark` are const, with `fontFamily: 'Inter'` / `'JetBrains Mono'`. `withGoogleFonts()` swaps in `GoogleFonts.inter(...)` at runtime. In tests, bundle fonts and disable runtime fetching.
- **State layers.** Use `overlayColor: TalariaStates.overlay(colors)` (a `WidgetStateProperty` covering hover, pressed, dragged and selected), and `boxShadow: TalariaStates.focusRing(colors)` for the 2px ring with a 2px gap. Disabled opacity is `TalariaStates.disabledOpacity` (0.48), for icons only; text uses `fgDisabled`. Draw the focus ring only when `FocusManager.instance.highlightMode == FocusHighlightMode.traditional`.
- **Semantics.** Badges: `Semantics(label: '${kind}: $label', excludeSemantics: true)`. Icon buttons: tooltip = label. Charts: `Semantics(label: summary)` plus a table toggle. Live regions for toasts, loading and the save bar. `ensureSemantics()` per 0.8.
- **Reduced motion.** `context.tMotion` already returns `TalariaMotion.reduced` when `MediaQuery.disableAnimationsOf(context)` is true.
- **Responsive helpers (v2.1).**
  - `context.sizeClass` returns a `TalariaSizeClass` (`isCompact`, `isMedium`, `isDesktop`, `atLeast(c)`, `below(c)`).
  - `context.tLayout` returns a `TalariaLayout` with columns, margin, nav kind and widths, density, `touchFirst`, table, detail-rail and overlay modes, and tick counts. `TalariaLayout.columnWidth(width)` and `controlHeight(size)` are on it too.
  - `context.responsive<T>(compact: …, medium: …, expanded: …, large: …)` falls back to the next smaller value.
  - `context.tTextAdaptive` applies the adaptive type scale.
  - Prefer `TalariaAdaptiveBuilder` inside panes.
  - The generated `TalariaAdaptiveShell`, `showTalariaPicker` and `showTalariaFormDialog` are reference implementations. Adapt them to the real router and sidebar widget rather than shipping them unchanged.
- **Deltas.** `deltaColor(colors, change: d, polarity: p, baseline: b)` returns `deltaGood`, `deltaBad` or `deltaNeutral` (`successFg` / `dangerFg` / `fgMuted`). `MetricPolarity { higherIsBetter, lowerIsBetter, neutral }`. When `baseline < minBaseline` (20), the caller renders the absolute change ("↑ 5 from 3") instead of a percentage.

---

## 6. Testing checklist

- [ ] `flutter analyze` is clean. `dart format` has been applied.
- [ ] The `rg` in §3.2 returns 0 hits in `lib/`. There is no `toUpperCase()` in badge, label or tab code.
- [ ] Contrast test (0.7) passes for light, dark and 3 custom seeds.
- [ ] Goldens (0.5) pass for every style-guide section and page template, light and dark, desktop and mobile.
- [ ] State matrix: every interactive widget has hover, pressed, focus, selected, disabled and loading (where relevant) and an error state.
- [ ] Keyboard only: Tab order, visible focus ring, Esc closes overlays, focus returns to the invoker (focus trail).
- [ ] Screen reader (VoiceOver + Chrome): nav items, icon buttons, badges and charts are all named.
- [ ] Reduced motion: no shimmer, no slide animations, and the blade loader is static.
- [ ] 200% browser zoom: no clipped text. The segmented control and filter bar wrap rather than overflow.
- [ ] Theme: no white flash on reload with Dark saved (4.7). The System theme follows OS changes live.
- [ ] Environment: All by default, the count on a project card matches the destination, and an unknown env shows a warning.
- [ ] URL: filters round-trip, Back leaves the page in one step, and drawers are deep-linkable.
- [ ] 404 and invalid IDs render inside the shell with actions.
- [ ] Metrics: "—" for N/A, polarity-correct colours.
- [ ] All copy is NZ English and Sentence case.
- [ ] All fixtures use fictional data.
- [ ] Responsive goldens at 390 / 820 / 1280 / 1440, light and dark, for every page template and the components listed in 5.8.
- [ ] No `RenderFlex` overflow and no unintended horizontal scroll from 320 to 2560 wide; tap targets ≥ 48 (44 iOS) on touch.
- [ ] Rail on tablet, bottom bar + More on phone, sheets for env/project/filters, full-screen forms on compact, safe-area insets respected (notch, home indicator, landscape).

---

## 7. Things that would make the designer's job easier

1. **Live token tables** in the style guide, reading from the ThemeExtensions with hex and contrast values, copyable. These are the source of truth during review.
2. **State matrices** for every interactive widget, with forced states, so hover, pressed, focus and disabled can be reviewed without a mouse.
3. **Theme toggles everywhere:** a global switcher in the style guide, plus per-section light/dark side by side, plus a custom seed picker with contrast slider.
4. **Screenshot and golden export:** a script (`tool/export_previews.dart` or the provided `source/shot.js`) that writes PNGs per section and theme to a known folder, and CI artefacts with golden diffs attached to PRs.
5. **Isolated widget routes:** `/_dev/style-guide/<section>`, with fixture data and no backend, so a single widget can be opened, recorded or compared.
6. **Deep links for everything:** theme, density, state, drawer, tab and filter all in the URL, so a design review comment can be a link.
7. A **page-template gallery** with empty, error, loading and 404 variants, so new pages start from a pattern, not a blank screen.
8. A per-route **document title**, so browser tabs and screen recordings are identifiable.
9. **Fictional fixture data** checked in, safe for screenshots and marketing.
10. **Breakpoint preview for any widget (v2.1).** In `/_dev/style-guide`, a toolbar with Mobile 390 / Tablet 820 / Desktop 1280 / Wide 1440 that renders the selected widget or page inside a `MediaQuery` override (size, padding for safe areas, `platform` for touch) and a device frame, with a "show all four" mode side by side. Deep link with `?vp=390` the way the HTML guide does.
11. **Device-frame screenshot export** per size class and theme from that tool (or CI goldens), so responsive reviews are a folder of PNGs rather than a resize-the-browser session.
12. A **size-class overlay** toggle that draws the 4/8/12 column grid and margins over any route, plus a readout of the current size class and insets.

---

## 8. Where to look in the HTML guide

Section ids (open with `talaria-design-system-v2.html?solo=<id>`, or `?split=<id>` for side by side):
`overview`, `v21` (what's new), `principles`, `changes` (old vs new), `colour`, `type`, `foundations` (spacing, radii, density, widths, elevation, motion), `icons`, `responsive` (breakpoints, layout table, grid, adaptive type, touch, components at each breakpoint), `rsheet` (one page at four sizes), `buttons`, `inputs`, `selection`, `badges`, `cards`, `rows`, `filters`, `header`, `nav`, `feedback` (alerts, toasts, empty, error, 404), `states` (loading, skeletons, focus trail), `data`, `billing`, `ai`, `overlays` (menus, dialogs, drawers, tooltips), `page-issues`, `page-issue`, `page-overview`, `page-settings`, `page-mobile` (all pages on phone and tablet plus phone flows: events, AI drawer, More drawer, filters/env/project/confirm sheets, full-screen dialog, toast), `a11y`.

Page sections have a viewport switcher; `?solo=page-issue&vp=phone` opens Issue detail in the phone frame.

Regenerate everything: `cd source && python3 build_tokens.py && python3 build_dart.py && python3 build_html.py`. The scripts write to fixed `/workspace/talaria/deliverables/` paths; adjust these when moving to the repo. Previews: `bash previews.sh`, which needs puppeteer-core and Chrome. Dart check: `DART=/path/to/dart bash source/dart_stubs/check.sh`.
