diff --git a/data/repo-awareness-snapshot.json b/data/repo-awareness-snapshot.json
index eda565017d..5d9b7f11d9 100644
--- a/data/repo-awareness-snapshot.json
+++ b/data/repo-awareness-snapshot.json
@@ -1,8 +1,8 @@
{
"version": "repo-awareness-snapshot-v1",
"captured_revision": {
- "sha": "59bb01b3e5c6b5c846053d2916d3e780ea38bbb1",
- "committed_at": "2026-08-27T05:20:05+00:00"
+ "sha": "284454e511a503567e9e984dbc01ae0390c37153",
+ "committed_at": "2026-08-27T18:45:24+08:00"
},
"routes": {
"modes": [
@@ -2386,6 +2386,11 @@
"section": "design-system",
"catalogued": true
},
+ {
+ "path": "docs/design-system/FIX-GUIDE.md",
+ "section": "design-system",
+ "catalogued": true
+ },
{
"path": "docs/design-system/GATES.md",
"section": "design-system",
@@ -2399,7 +2404,7 @@
{
"path": "docs/design-system/README.md",
"section": "design-system",
- "catalogued": false
+ "catalogued": true
},
{
"path": "docs/design-system/SPEC.md",
@@ -3867,9 +3872,9 @@
}
],
"counts": {
- "documents": 480,
- "catalogued": 106,
- "uncatalogued": 374,
+ "documents": 481,
+ "catalogued": 108,
+ "uncatalogued": 373,
"sections": 19
}
},
diff --git a/docs/README.md b/docs/README.md
index 6098437cc7..aa9dc406e3 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -30,13 +30,15 @@ npm run docs:check-links
- [search-results-bar-decisions.md](search-results-bar-decisions.md) — shared results-bar anatomy, why the filter shelf is scoped to two modes, and what is deliberately not done
- [deployment-architecture.md](deployment-architecture.md) — app/worker/Supabase deployment topology
- [ingestion-state-machine.md](ingestion-state-machine.md) — ingestion job lifecycle and states
-- [design-system.md](design-system.md) — tokens, primitives, styling conventions
+- [design-system/README.md](design-system/README.md) — front door for the v2 design system (tokens, components, gates)
+- [design-system.md](design-system.md) — live-layer notes during the v1→v2 transition (superseded as spec)
- [design-system/SPEC.md](design-system/SPEC.md) — the complete v2 design system: roles, rules, rationale (never values)
- [design-system/TOKENS.md](design-system/TOKENS.md) — reconciled token inventory: every role, winning name, owner, and what it replaces
- [design-system/COMPONENTS.md](design-system/COMPONENTS.md) — the eight safety-component specifications plus the maturity matrix
- [design-system/DECISIONS.md](design-system/DECISIONS.md) — conflicts C1–C5 resolved, clinical Q&A record, assumptions, blocked items
- [design-system/GATES.md](design-system/GATES.md) — every design-system rule paired with its enforcement status
- [design-system/ADOPTION.md](design-system/ADOPTION.md) — PR 13 registration record: adoption order, per-surface file allowlists, exclusions, pins, proof shots
+- [design-system/FIX-GUIDE.md](design-system/FIX-GUIDE.md) — Hazard 1–2 sweep dispositions (Fixed / Documented / Deferred / Out-of-scope)
- [comparison-behaviour.md](comparison-behaviour.md) — shared selection, state, responsive, and accessibility contract for comparison surfaces
- [clinical-chat-ui-component-map.md](clinical-chat-ui-component-map.md) — chat UI component inventory
- [clinical-badge-system-guide.md](clinical-badge-system-guide.md) — clinical badge semantics
diff --git a/docs/branch-review-records/be0d35ad96adbaa5219303ff8595fd560ba330b3fc03c79f10d2dabdf35b4921.record.md b/docs/branch-review-records/be0d35ad96adbaa5219303ff8595fd560ba330b3fc03c79f10d2dabdf35b4921.record.md
new file mode 100644
index 0000000000..7fcbf174b9
--- /dev/null
+++ b/docs/branch-review-records/be0d35ad96adbaa5219303ff8595fd560ba330b3fc03c79f10d2dabdf35b4921.record.md
@@ -0,0 +1 @@
+| 2026-08-27 | ds-hazard-1-2-sweep | 19ee085b1cc952e8c40e893fb2fce99538415259 | design-system hazard 1-2 | P2 SPEC.md:217-218 still requires per-step -lh/-tr against gated deletion; P3 GATES evidence header still not-re-run; no P0/P1 | test:focused --files token,adoption,responsive,reachability: fail-closed exit 2 (tests/** unsafe); run-vitest same files blocked by exclusive coordinator (browser-test-gate-handoff-d5c1db); check:design-system-contract not run; verify:cheap/ui/pr-local and providers skipped |
diff --git a/docs/codebase-index.md b/docs/codebase-index.md
index 8b06bdefb1..72fe2f8b50 100644
--- a/docs/codebase-index.md
+++ b/docs/codebase-index.md
@@ -9,28 +9,28 @@ Structured map for AI agents and onboarding. For live routes, see `docs/site-map
## Quick start
-| Step | Command |
-| --------------------------------- | -------------------------------- |
-| Confirm Supabase target | `npm run check:supabase-project` |
-| Start app (project-specific port) | `npm run ensure` |
-| Start ingestion worker | `npm run worker` |
-| Cheap verification gate | `npm run verify:cheap` |
-| UI verification gate | `npm run verify:ui` |
+| Step | Command |
+| --------------------------------- | -------------------------------------------------------------------------------- |
+| Confirm Supabase target | `npm run check:supabase-project` (provider-backed — needs explicit confirmation) |
+| Start app (project-specific port) | `npm run ensure` |
+| Start ingestion worker | `npm run worker` |
+| Cheap verification gate | `npm run verify:cheap` |
+| UI verification gate | `npm run verify:ui` |
---
## Top-level layout
-| Path | Purpose |
-| ----------- | ---------------------------------------------------------------- |
-| `src/` | Next.js App Router UI, API routes, shared lib, components |
-| `supabase/` | SQL migrations, schema mirror, Edge Functions, CLI config |
-| `worker/` | Local ingestion worker (parse, OCR, chunk, embed, DB writes) |
-| `scripts/` | CLI ops: reindex, eval, backfill, governance, dev-server helpers |
-| `tests/` | Vitest unit (`*.test.ts`) + Playwright E2E (`ui-*.spec.ts`) |
-| `docs/` | Runbooks, governance, search/RAG plans, generated sitemap |
-| `public/` | Static assets (`public/llms.txt`) |
-| `.github/` | CI workflows, PR template (clinical governance preflight) |
+| Path | Purpose |
+| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `src/` | Next.js App Router UI, API routes, shared lib, components |
+| `supabase/` | SQL migrations, schema mirror, Edge Functions, CLI config |
+| `worker/` | Local ingestion worker (parse, OCR, chunk, embed, DB writes) |
+| `scripts/` | CLI ops: reindex, eval, backfill, governance, dev-server helpers |
+| `tests/` | Vitest unit (`*.test.ts`) + Playwright E2E (`ui-*.spec.ts`) |
+| `docs/` | Runbooks, governance, search/RAG plans, generated sitemap; design-system system of record is [`docs/design-system/README.md`](design-system/README.md) |
+| `public/` | Static assets (`public/llms.txt`) |
+| `.github/` | CI workflows, PR template (clinical governance preflight) |
Smaller top-level directories that are easy to miss:
diff --git a/docs/design-system-contract.md b/docs/design-system-contract.md
index ce96a3b554..aa7e2d9d73 100644
--- a/docs/design-system-contract.md
+++ b/docs/design-system-contract.md
@@ -25,11 +25,11 @@ npm run check:icon-scale
- **Tokens Only**: Raw CSS hex codes (e.g. `#007a78`, `#ffffff`), RGB/RGBA, HSL, and un-tokenized Tailwind color classes (e.g. `bg-white`, `text-slate-900`, `border-red-200`) are prohibited in components.
- **Variable Syntax**: All colors must use CSS custom properties defined in `src/app/globals.css` with semantic purpose:
- **Brand & Clinical Accent**: `var(--clinical-accent)`, `var(--clinical-accent-hover)`, `var(--clinical-accent-soft)`, `var(--clinical-accent-border)`
- - **Surfaces & Borders**: `var(--surface)`, `var(--surface-subtle)`, `var(--surface-wash)`, `var(--surface-lux)`, `var(--border)`, `var(--border-subtle)`
- - **Text Roles**: `var(--text)`, `var(--text-muted)`, `var(--text-heading)`, `var(--text-soft)`
+ - **Surfaces & Borders**: `var(--surface)`, `var(--surface-subtle)`, `var(--surface-wash)`, `var(--surface-lux)`, `var(--border)`, `var(--border-strong)`, `var(--border-lux)`
+ - **Text Roles**: `var(--text)`, `var(--text-muted)`, `var(--text-heading)`. `--text-soft` is decoration (`--decoration-soft`), not a text role.
- **Status & Safety Triads**: `--success-*`, `--warning-*`, `--danger-*`, `--info-*` (reserved exclusively for clinical/system status).
- **Focus Ring & Outlines**: `var(--focus)` for all keyboard and visible focus rings.
-- **Raw Color Exemptions**: Strict and enumerated in `RAW_COLOR_EXEMPTIONS` in `scripts/design-system-contract-utils.mjs` (e.g., globals token definitions, brand mark SVG builder, diagnostic visualizations, OpenGraph art, printable patient/factsheet paper).
+- **Raw Color Exemptions**: Strict and enumerated in `RAW_COLOR_EXEMPTIONS` in `scripts/design-system-contract-utils.mjs` (e.g., globals token definitions, brand mark SVG builder, diagnostic visualizations, OpenGraph art, printable patient/factsheet paper). Medication record accent defaults (`#0f766e` in `src/lib/medications.ts` and `src/lib/medication-records.ts`) are a **scoped** exemption for the Postgres `accent` column default only — not a whole-file blank cheque, and not a mapping onto `--clinical-accent`.
### 2.2 Typography Scale
@@ -47,7 +47,7 @@ npm run check:icon-scale
- **Elevation**: Monotonic numeric scale `var(--e0)` through `var(--e4)`. No raw `box-shadow` values.
- **Edge Ownership**: Prohibits simultaneous `border-*` and `ring-*` styling on the same surface to prevent clipped or competing boundaries.
-- **Motion Durations**: Transitions and animations must use standardized duration tokens (`var(--duration-fast)`, `var(--duration-normal)`) and respect `motion-reduce:`. Layout-property animation (e.g., width, height, padding) is disallowed except for explicitly audited phone-chrome transitions.
+- **Motion Durations**: Transitions and animations must use standardized duration tokens (`var(--duration-fast)`, `var(--duration-base)`) and respect `motion-reduce:`. Layout-property animation (e.g., width, height, padding) is disallowed except for explicitly audited phone-chrome transitions.
---
diff --git a/docs/design-system/ADOPTION.md b/docs/design-system/ADOPTION.md
index 48c1667001..1a5a3fcb67 100644
--- a/docs/design-system/ADOPTION.md
+++ b/docs/design-system/ADOPTION.md
@@ -5,7 +5,7 @@ of truth is `adoption-contract.json`; the generated manifest and marked tables i
must match it exactly.
- **Date:** 12 August 2026
-- **Current state:** 54 visual references are locally registered; all 51 production page routes
+- **Current state:** 55 visual references are locally registered; all 51 production page routes
are owned across 14 surface families, with 59 route/component roots scanned; every declared
root uses the v2 shell and has declared proof with no committed visual baseline.
- **Phase 1 blockers resolved first, in their own commits:** `#207` ungrounded `AnswerState`,
diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md
index 9e79fed409..ee117a0394 100644
--- a/docs/design-system/COMPONENTS.md
+++ b/docs/design-system/COMPONENTS.md
@@ -20,7 +20,7 @@ derived from source, `.design-sync/config.json`, previews, direct contract cover
production imports. It intentionally does not claim dark, forced-colours, 320px, print, remote
publication, or product adoption without separate evidence.
-### 0.1 Registered visual components (54 — local contract; remote status unverified)
+### 0.1 Registered visual components (55 — local contract; remote status unverified)
Every visual export has one source map entry, a source-derived public `*Props` contract (except
the two zero-prop roots), a reference preview, and a direct static publication test. The generated
@@ -37,7 +37,7 @@ table under **Generated maturity snapshot** is the canonical list and product-im
_Remaining from the original eight in this document:_ `DocumentFrame` is built locally
(`src/components/ui/document-frame.tsx`, shell-only in `DocumentViewer`) but is not yet among
-the 54 design-sync registered visual exports — registration and full controls remain follow-up.
+the 55 design-sync registered visual exports — registration and full controls remain follow-up.
`OverlayRoot`, `SegmentedControl`, and the PR 6–8 components are built and represented by the
local publication contract. The generated snapshot records their current product-import counts;
@@ -47,8 +47,9 @@ design-project publication or browser acceptance.
_P1 reusable (specified in outline only):_ `Menu`/`Popover` · `KeyValue` ·
`AppliedFilters`/`FilterSheet` · `ResponsiveActionGroup` · `ScrollableStrip`/
`ScrollAffordance` · `SourceLink` · `Banner` · `CopyButton`/`CopyField` ·
-state family (`ErrorState`, `OfflineState`, `PermissionDeniedState`, `NotFoundState`,
-`UnavailableState`).
+state family (`OfflineState`, `PermissionDeniedState`, `NotFoundState`,
+`UnavailableState`). `ErrorState` is built (`src/components/ui/error-state.tsx`) and
+locally registered — it is not outline-only.
`FilterBar` and `DataTable` are retired names, not future component contracts. Use a
surface-owned filter pattern or the canonical `AccessibleTable`; do not revive either name.
@@ -86,7 +87,7 @@ print primitives (`PrintHeader`, `PrintFooter`, `CitationFootnote`, `PrintOnly`,
| Progress/StageList | all four resolved: `scaleX`, the theme `animate-shimmer` in place of a hardcoded `1.4s`, a step index clamped to ≥1, and an sr-only `role="status"` sibling in place of `aria-live` on the whole `
` | **done** |
| EmptyState | static live-off default with explicit polite/assertive opt-in | **done** (PR 8) |
| AccessibleTable | semantic caption, `MissingValue` cells, dense headers (clipped header keeps its full string as `title`) and the expander (now the registered `Button`, off the local ring-focus recipe) landed; content-role widths remain | PR 6/PR 12 remainder |
-| ui-primitives.tsx | 698-line module mixing recipes/actions/feedback/clinical — split. Re-measured 2026-08-12: **157** production files import it (202 including mockups), against the 54 registered components' **31** with product imports; this module, not the registry, is what the product actually runs on. Per-primitive breakdown added 2026-08-21: `Button` **12** importers against 157, with **112** production files still holding a raw ``; `Sheet` **26** — the one genuinely adopted primitive. The split is not uniform, and the pattern explains it: `Sheet` owns focus trap, portal and overlay stacking, which a className cannot fake, while `Button`/`Chip`/field shells own visual convention a recipe string approximates. Deciding between "give `Button` behaviour worth importing" and "promote `ui-primitives` to the documented layer" is the actual PR 12 question | PR 12 |
+| ui-primitives.tsx | 698-line module mixing recipes/actions/feedback/clinical — split. Re-measured 2026-08-12: **157** production files import it (202 including mockups), against the 55 registered components' **31** with product imports; this module, not the registry, is what the product actually runs on. Per-primitive breakdown added 2026-08-21: `Button` **12** importers against 157, with **112** production files still holding a raw ``; `Sheet` **26** — the one genuinely adopted primitive. The split is not uniform, and the pattern explains it: `Sheet` owns focus trap, portal and overlay stacking, which a className cannot fake, while `Button`/`Chip`/field shells own visual convention a recipe string approximates. Deciding between "give `Button` behaviour worth importing" and "promote `ui-primitives` to the documented layer" is the actual PR 12 question | PR 12 |
---
diff --git a/docs/design-system/DECISIONS.md b/docs/design-system/DECISIONS.md
index 0ba432d847..dd8a28e5d7 100644
--- a/docs/design-system/DECISIONS.md
+++ b/docs/design-system/DECISIONS.md
@@ -147,7 +147,7 @@ source), flagged in SPEC §3 and cheap to veto mode-by-mode.
## C6 · Publication truth is source-derived; adoption truth is route-complete
-**Chose.** The local design-sync registry contains 53 visual exports. Each row is derived from
+**Chose.** The local design-sync registry contains 55 visual exports. Each row is derived from
one real source file and requires an entry export, an exact TypeScript-checker-derived public
`*Props` contract (or an explicit zero-prop root), a reference preview, and a direct publication
test. `OverlayPortal`, `ToastProvider`, `useToast`, `AnswerState`, the answer helpers, and the
diff --git a/docs/design-system/FIX-GUIDE.md b/docs/design-system/FIX-GUIDE.md
new file mode 100644
index 0000000000..5814e3bd30
--- /dev/null
+++ b/docs/design-system/FIX-GUIDE.md
@@ -0,0 +1,94 @@
+# Clinical KB design system — Hazard 1–2 fix guide
+
+Single appendix for the closed Hazard 1–2 sweep. **Rules and roles stay in SPEC / TOKENS /
+GATES; this file only records dispositions.** A Documented row is not a licence to change
+product UI, `globals.css`, or `ckb-v2-tokens.css`.
+
+- **Date:** 27 August 2026
+- **Companions:** [README.md](README.md) · [TOKENS.md](TOKENS.md) · [GATES.md](GATES.md) ·
+ [COMPONENTS.md](COMPONENTS.md) · [`docs/design-system-contract.md`](../design-system-contract.md) ·
+ [`docs/design-system.md`](../design-system.md) (live-layer Geist notes)
+
+Statuses: **Fixed** (code in this sweep) · **Documented** (intentional or already true; no
+product change) · **Deferred** (later, named successor) · **Out-of-scope** (explicitly not
+this sweep).
+
+Do not invent token values here. Do not snap Geist interpolations to 600/700. Do not copy
+mockup `min-h-11` or `--text-soft` body into production.
+
+---
+
+## Batch A — code this sweep
+
+Agents 1–3 landed these. Parent verifies after merge; do not treat this table as a second
+measurement of `globals.css` / `ckb-v2-tokens.css`.
+
+| ID | Status | Rationale |
+| -------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| DS-P1-16 | Fixed | Therapy-compass decorative `size={15}` maps to `size-icon-sm` (14px); Lucide `size` prop removed so SVG attrs cannot override the class. |
+| DS-P2-03 | Fixed | Unused `--text-{xs,sm,body,md,lg,xl}-lh` / `-tr` companions deleted; keep `--text-hero--line-height` and `--text-hero-tr`. Do not re-require per-step orphans. |
+| DS-P2-07 | Fixed | Two `1px` shadow-spread layers dropped in `globals.css` (compare FAB already has a border; disabled send uses a border, not inset+border). |
+| DS-P2-12 | Fixed | Three raw `line-height` literals in `globals.css` map to existing named tokens (`--text-hero--line-height`, `--leading-prose`). Not `--leading-tight`. |
+| DS-P2-13 | Fixed | Specifier surfaces that already own the edge with `border` lose `shadow-inset`. True wells, segmented tracks, field-controls, and Ward stay. |
+| DS-P2-17 | Fixed | `differential-detail-page` uses `resolveScrollBehavior()`; `settings-dialog` already branches on reduce-motion and is left alone. |
+| DS-P2-25 | Fixed | `#0f766e` medication accent defaults stay data, not `--clinical-accent`. Scoped `RAW_COLOR_EXEMPTIONS` entry; `rawColorLiterals` 2→0. See below. |
+
+---
+
+## Batch B — docs dispositions
+
+| ID | Status | Rationale |
+| -------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| DS-P2-04 | Documented | Intentional Geist variable interpolation. Already explained in [`docs/design-system.md`](../design-system.md) (intermediate weights; do **not** snap to 600/700). |
+| DS-P2-22 | Documented | `SectionHeading` exists (`src/components/ui/section-heading.tsx`); 14 production call sites. No opportunistic migration this sweep. |
+| DS-P2-23 | Documented | `AnswerCard` `frame?: "raised" \| "bare"` — both valid. Production answer surface uses **bare**. Future visual baselines must include both. Do not commit screenshots. |
+| DS-P2-27 | Documented | 15-mode load: recommend first-run / Tools prominence later. No IA redesign this sweep. |
+| DS-P2-28 | Documented | Settings **Motion** already has a per-control description. **Recent searches on home** does not — docs recommendation only; no extra intro copy in product this sweep. |
+| DS-P2-29 | Deferred | Phone document-viewer density waits on the tap-floor (Hazard ≥3). |
+| DS-P2-30 | Documented | Toast primitive exists; there is no **universal network-error** toast. Do not build toast infra here. `OverlayRoot` remains the overlay owner. |
+| DS-P2-31 | Documented | Already in TOKENS §4 / SPEC §3: add `--kind-*` **with the first call site**. No values in this sweep. |
+| DS-P2-32 | Documented | `--text-placeholder` is **live** (globals + v2 + `placeholder:` consumers), not planned. TOKENS §5 corrected. No further role migration. |
+| DS-P2-33 | Documented | Contract `textSoftConsumers=0` in production. Keep the `--text-soft` alias until zero `var(--text-soft)` remain **including mockups**. Do not rename; do not delete this sweep. |
+| DS-P3-01 | Deferred | PR-9 plan. `--shadow-focus` and dead springs are already gone. `--shadow-lift` still has live `mode-nav` and `document-search-results` consumers — do **not** delete it. |
+| DS-P3-02 | Documented | TOKENS already: `--tone-*` frozen, not deleted. |
+| DS-P3-03 | Documented | TOKENS already: design-side `--quantity-unit-scale` never lands. |
+| DS-P3-04 | Documented | TOKENS already: ConfidenceMeter deferred; no token without a call site. |
+| DS-P3-05 | Documented | Registry has **55** registered visual exports. `DocumentFrame` is built shell-only and still unregistered. Register when print primitives ship (PR 11). |
+| DS-P3-07 | Documented | Full visual state matrix grows incrementally with new adoptions. |
+| DS-P3-08 | Documented | QA checklist: inspect the filter band at 414–430px. No code this sweep. |
+| DS-P3-09 | Documented | `#TYZK23` PWA install CLS already fixed (PR #2253). Regression-check note only; no speculative CSS. |
+| DS-P3-10 | Documented | Mockup `min-h-11` and `--text-soft` body must not leak into production. |
+
+---
+
+## Medication accent literals (DS-P2-25)
+
+The two remaining production raw-colour hits are `#0f766e` defaults in
+`src/lib/medications.ts` and `src/lib/medication-records.ts`. They restate a Postgres column
+default (`accent text not null default '#0f766e'`) for a per-record, user-chosen colour.
+SPEC already records this as **data, not a token**.
+
+Do **not** map them to `--clinical-accent` (that role is `--primary-500` and would recolour
+medication records). The contract exemption is **scoped to those accent defaults**, not a
+whole-file blank cheque on either module. Enumerated in `RAW_COLOR_EXEMPTIONS` in
+`scripts/design-system-contract-utils.mjs`; described for operators in
+[`docs/design-system-contract.md`](../design-system-contract.md).
+
+---
+
+## Out of scope this sweep (do not implement)
+
+P0/P1 tap-floor · Button / PageHeader / v2 full adoption · Ward family · type-step
+retirement · toast infrastructure · `--kind-*` values · `--text-soft` global rename ·
+deleting live `--shadow-lift` · ConfidenceMeter · `DocumentFrame` registry · screenshot
+capture · `npm run ensure` unless a visual defect is proven.
+
+---
+
+## Residual risk (unchanged by docs)
+
+- Remaining production `border` + `inset` outside the specifier surfaces edited in DS-P2-13.
+- Lucide `size={N}` remains ungated except the DS-P1-16 `size={15}` sites already converted (`check:icon-scale` only bans `*-4.5`).
+- `--shadow-lift` still live (`mode-nav`, `document-search-results`).
+- Mockup `--text-soft` still blocks alias deletion.
+- Mockup `min-h-11` must stay mockup-only.
diff --git a/docs/design-system/GATES.md b/docs/design-system/GATES.md
index af1231bb60..5972ac802c 100644
--- a/docs/design-system/GATES.md
+++ b/docs/design-system/GATES.md
@@ -19,7 +19,7 @@ than CI, two of them by the second reader; review does not scale past two carefu
| Check | What it enforces | Label |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ |
| `npm run check:design-system-contract` (token baseline + adoption + design-sync contracts) | Raw colour literals (ratchet against `design-system-contract-baseline.json`), literal `shadow-[…]` classes, the legacy tap class, interactive tap-size literals, therapy-compass `tc-btn` base-class use; border+ring edge conflicts, 1px shadow spreads, layout-property transitions and legacy shadow aliases (all ratcheted **per path**, so new use fails anywhere); status-coloured numerals and colour-only status indicators; raw padding, radius, gap and line-height literals in both classes and CSS declarations (ratcheted **per path**; named utilities and Tailwind arbitrary-property forms such as `[padding:22px]` both count; values computed with `var()`/`env()`/`calc()`/`clamp()`/`max()`, zero with any CSS unit identifier, and the CSS-wide keywords are exempt); declared-but-unselected type steps (**hard rule** with a gated exemption list); `dark:` colour overrides, legacy palette utilities and image-inversion filters (all pinned at **zero**, not ratcheted); deterministic adoption truth; local source/export/preview/design-sync parity. Mockups exempt. | implemented-blocking |
-| `tests/ckb-v2-token-contract.test.ts` | v2 layer stays class-scoped (no `:root` leak) · `--border-lux` solid and no lighter than `--border`, both themes · dark surface ramp monotonic and separated · `--surface-subtle` aliases up · text/muted/heading ≥4.5:1 on the light shell, muted ≥4.5:1 dark · `--text-soft` pinned **below** 4.5:1 and ≥3:1 (both sides, so the tier cannot be "fixed" away) · command pair ≥4.5:1 both themes · `--shadow-inset` true inset · elevation ladder carries no baked 1px hairline · tap floor ≠ chip height ≠ compact row · reduced-motion zeroes durations · every type step has its own line-height and tracking | implemented-blocking |
+| `tests/ckb-v2-token-contract.test.ts` | v2 layer stays class-scoped (no `:root` leak) · `--border-lux` solid and no lighter than `--border`, both themes · dark surface ramp monotonic and separated · `--surface-subtle` aliases up · text/muted/heading ≥4.5:1 on the light shell, muted ≥4.5:1 dark · `--text-soft` pinned **below** 4.5:1 and ≥3:1 (both sides, so the tier cannot be "fixed" away) · command pair ≥4.5:1 both themes · `--shadow-inset` true inset · elevation ladder carries no baked 1px hairline · tap floor ≠ chip height ≠ compact row · reduced-motion zeroes durations · type-scale pins `--leading-prose` plus `--text-hero--line-height` / `--text-hero-tr` (no per-step `-lh`/`-tr` orphans) | implemented-blocking |
| `tests/design-token-contract.test.ts` | Live-layer token relationships (companion to the v2 contract) | implemented-blocking |
| `tests/source-badges-off-vocab.dom.test.tsx` | Enum resilience: off-vocabulary `clinical_validation_status` degrades to the neutral triad, logs once, never throws | implemented-blocking |
| `tests/source-metadata-browser-safety.test.ts` | The `process is not defined` client-bundle crash stays fixed (server logger never ships to the browser) | implemented-blocking |
@@ -73,24 +73,24 @@ because it contributed nothing.
| # | Gate | Status | Evidence / path to blocking |
| --- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Contrast ≥4.5:1 for every text/background pair, both themes, live **and** v2; `--decoration-soft` asserted below 4.5 and never on a text node | **implemented-blocking** | v2 pairs + `--text-placeholder` + `--decoration-soft` tier + recipe AST (`decoration-on-text.contract.test.ts`); live full matrix remains follow-on. |
-| 2 | Tap targets ≥48px interactive; static pills never carry `min-h-tap`; no production target reduced | **implemented-blocking for new use** | Enforced today: `--spacing-tap` is 48px and pinned from both sides (`ckb-v2-token-contract` asserts the `@theme` knob is ≥48 and that `--tap-min` is its alias), `ui-style-contract` measures the rendered floor for declared `min-height` carriers in Chromium, and the legacy-class / literal ratchets still run. **Still open for the `h-10` case, and 9 Aug 2026 measured why.** An enumeration of _rendered interactive_ elements was written, shown to find genuine defects, and then **reverted rather than landed**: it is not deterministic on this route. Six runs against one production build returned 6, 5, 4, 3, 3 and 9 distinct sub-floor shapes, largely disjoint — one run saw the answer-suggestion chips and a sort band, another the settled results list. `waitForLoadState("networkidle")` plus deduplication to distinct shapes did not fix it, and two consecutive agreeing runs turned out to be coincidence. This spec runs in the required `Production UI` job, so an intermittent version of it would block every merge in the repo; that is a worse outcome than the gap it closes. Path to blocking: give the audit a deterministic surface — a static route or a fixed seeded state — before re-attempting it. **What the enumeration did establish, in every one of the six runs, is a live defect:** controls that carry `min-h-tap` compute `min-height: 0px` and render at 16–36px, six distinct shapes in total. The declared-carrier audit cannot report them by construction — it only measures elements already computing at or above the floor, so a floor overridden downward is skipped rather than flagged. Tracked as `#293`. **Correction, same date — "`test:e2e:style-contract` is not part of `verify:cheap`" was true and badly misleading, and it sent one session looking for a wiring bug that does not exist.** The npm script is only a convenience alias for running this one spec; the spec matches `productionSpecPattern` in `playwright.config.ts` and is listed explicitly in `scripts/playwright-pr-shards.mjs`, so it already runs in the `Production UI` job that `pr-required` demands on any UI-scoped PR. It must **not** be added to `verify:cheap:internal`: `check:gate-manifest` requires every gate in that chain to also run in `static-pr`, which has no browser and no server. Per-surface geometry stays in the held visual harness. **Both named gaps are now closed, 15 Aug 2026.** The rendered enumeration landed first: `885c613` (PR #1962) added the deterministic phone-viewport `min-h-tap` audit this row was waiting on — `/forms`'s fixed-array home rather than a live-search route, polled until three consecutive reads agree, explicitly sorted, and asserted identical across three full navigate-and-enumerate cycles. The `h-10` case is closed here by `interactiveTapFloorDeclarations` in `check:design-system-contract`: an interactive element (`a`, `button`, `input`, `select`, `summary`, `textarea`) declaring its own **unprefixed** `min-h-*` below the 48px token. Comparable arbitrary lengths and mutually exclusive/composed class branches are evaluated independently, so one safe branch cannot hide a sub-floor runtime path. Ratcheted at **33** across 14 files with per-path pins, so a new sub-floor control anywhere in `src/**` fails today. Mutation-verified: lowering one shortlist button to `min-h-9` produces both the total and matching per-path regression. **Deliberately scoped to `min-h-*`, not `h-*`/`size-*`:** a short `h-4` on an interactive element is routinely the visible box of a control whose hit area is owned by a tap-sized wrapper (`SelectionCheckbox` in `differentials-home.tsx`, whose label `ui-smoke` asserts still meets the floor), so flagging those would pad the baseline with non-defects — the §5 failure mode. **One known limit, recorded rather than hidden:** the walker sees intrinsic lowercase tags only, so a floor declared on ` ` or another component wrapper is invisible to it (the pre-existing `legacyTapClasses` check has the same blind spot). The 33 recorded sites are real debt still owed, tracked in `#265`. |
+| 2 | Tap targets ≥48px interactive; static pills never carry `min-h-tap`; no production target reduced | **implemented-blocking for new use** | Enforced today: `--spacing-tap` is 48px and pinned from both sides (`ckb-v2-token-contract` asserts the `@theme` knob is ≥48 and that `--tap-min` is its alias), `ui-style-contract` measures the rendered floor for declared `min-height` carriers in Chromium, and the legacy-class / literal ratchets still run. **Still open for the `h-10` case, and 9 Aug 2026 measured why.** An enumeration of _rendered interactive_ elements was written, shown to find genuine defects, and then **reverted rather than landed**: it is not deterministic on this route. Six runs against one production build returned 6, 5, 4, 3, 3 and 9 distinct sub-floor shapes, largely disjoint — one run saw the answer-suggestion chips and a sort band, another the settled results list. `waitForLoadState("networkidle")` plus deduplication to distinct shapes did not fix it, and two consecutive agreeing runs turned out to be coincidence. This spec runs in the required `Production UI` job, so an intermittent version of it would block every merge in the repo; that is a worse outcome than the gap it closes. Path to blocking: give the audit a deterministic surface — a static route or a fixed seeded state — before re-attempting it. **What the enumeration did establish, in every one of the six runs, is a live defect:** controls that carry `min-h-tap` compute `min-height: 0px` and render at 16–36px, six distinct shapes in total. The declared-carrier audit cannot report them by construction — it only measures elements already computing at or above the floor, so a floor overridden downward is skipped rather than flagged. Tracked as `#293`. **Correction, same date — "`test:e2e:style-contract` is not part of `verify:cheap`" was true and badly misleading, and it sent one session looking for a wiring bug that does not exist.** The npm script is only a convenience alias for running this one spec; the spec matches `productionSpecPattern` in `playwright.config.ts` and is listed explicitly in `scripts/playwright-pr-shards.mjs`, so it already runs in the `Production UI` job that `pr-required` demands on any UI-scoped PR. It must **not** be added to `verify:cheap:internal`: `check:gate-manifest` requires every gate in that chain to also run in `static-pr`, which has no browser and no server. Per-surface geometry stays in the held visual harness. **Both named gaps are now closed, 15 Aug 2026.** The rendered enumeration landed first: `885c613` (PR #1962) added the deterministic phone-viewport `min-h-tap` audit this row was waiting on — `/forms`'s fixed-array home rather than a live-search route, polled until three consecutive reads agree, explicitly sorted, and asserted identical across three full navigate-and-enumerate cycles. The `h-10` case is closed here by `interactiveTapFloorDeclarations` in `check:design-system-contract`: an interactive element (`a`, `button`, `input`, `select`, `summary`, `textarea`) declaring its own **unprefixed** `min-h-*` below the 48px token. Comparable arbitrary lengths and mutually exclusive/composed class branches are evaluated independently, so one safe branch cannot hide a sub-floor runtime path. Ratcheted at **32** across 13 files with per-path pins, so a new sub-floor control anywhere in `src/**` fails today. Mutation-verified: lowering one shortlist button to `min-h-9` produces both the total and matching per-path regression. **Deliberately scoped to `min-h-*`, not `h-*`/`size-*`:** a short `h-4` on an interactive element is routinely the visible box of a control whose hit area is owned by a tap-sized wrapper (`SelectionCheckbox` in `differentials-home.tsx`, whose label `ui-smoke` asserts still meets the floor), so flagging those would pad the baseline with non-defects — the §5 failure mode. **One known limit, recorded rather than hidden:** the walker sees intrinsic lowercase tags only, so a floor declared on ` ` or another component wrapper is invisible to it (the pre-existing `legacyTapClasses` check has the same blind spot). The 32 recorded sites are real debt still owed, tracked in `#265`. |
| 3 | Focus outline present, `--focus`, no companion ring | **planned** | Corrected 6 Aug 2026: the previous evidence ("`--focus` is referenced nowhere in the DS export", finding N3) was false — **[verified: grep]** 4 declarations (2 theme, 2 forced-colours) against **273** `var(--focus)` consumers, 260 of them in `.tsx`. The token is adopted; the check is what is missing. Path to blocking: assert a visible focus outline on every interactive role and reject a `ring-*` companion on the same node. A row that understates shipped work costs the document its authority as surely as one that overstates it, and nobody files a bug against pessimism. |
| 4 | Non-colour encoding on every status indicator | **implemented-blocking** | `colourOnlyStatusIndicators` scans production class roots and shared swatch recipes. The remaining calculator spectrum and shared status-marker debt was retired on 22 Aug 2026: severity bands now add distinct neutral edge patterns, ready/review/muted markers differ by outline/fill/geometry, and visible labels remain at call sites. The metric and per-path baseline are pinned at **0**; `StatusMark`, persistent text, and decoration-only `--decoration-soft` retain their documented roles. |
| 5 | Tables: semantic caption, associated headers, `aria-controls` on the expander | **implemented-blocking** | `AccessibleTableProps.caption` is required; DOM and alignment tests prove the semantic ``, associated headers, and expander relationship. |
| 6 | Enum resilience — neutral fallback, never throws | **implemented-blocking** | `source-badges-off-vocab.dom.test.tsx`. |
| 7 | Elevation monotonicity — no child heavier than its parent | **implemented-partial** | Token-ramp monotonicity is tested; the per-surface child/parent rule is planned (needs a render-tree check). |
-| 8 | Edge ownership — no `border-*` + `ring-*` on one surface; no 1px spread in a drop shadow | **implemented-partial** | Corrected 9 Aug 2026: the previous evidence — "the per-surface co-occurrence lint and spread-term check are planned (PR 9)" — was **false**, and had been since before this row was written. Both ship. `edgeOwnershipConflicts` is an AST co-occurrence check over resolved class roots (`BORDER_WIDTH_UTILITY` × `RING_WIDTH_UTILITY`), and `onePixelShadowSpreads` reads the fourth length of every `box-shadow` layer. Both ratchet **per path**, so a new conflict on any surface fails today. What is partial is the recorded debt, not the check: **18 conflicts across 9 files**, and 2 spreads in `globals.css`. (Corrected 15 Aug 2026 — this cell said 27 across 15, which PR #1942 had already paid down to 25 across 12 without updating the prose. The §3 row below and `scripts/design-system-contract-baseline.json` both already said 25, and per `docs/design-system/README.md` the baseline outranks this document, so the prose was the defect. Ledger `#265` carried the same stale figure.) Path to blocking: retire that debt and pin both at zero. The spread form was chosen _because_ it is mechanically lintable, and it duly was. |
-| 9 | No layout-property animation; no hardcoded durations | **implemented-partial** | Corrected 9 Aug 2026: "a layout-property lint is still planned" was **false**, and leaving it would have preserved the exact contradiction this pass exists to remove. `layoutTransitionExceptions` ships and ratchets **per path** against `SAFE_TRANSITION_PROPERTIES`, so a new layout-property transition in any file fails today; the phone chrome's intentional `grid-template-rows` / `height` / `padding-bottom` are among the 11 recorded exceptions rather than a gap. The Tailwind duration form is a **hard zero** (`hardcodedMotionClasses`), on top of PR-Arch tokenising production `duration-*`/`ease-*` literals onto `--duration-*` / `--ease-*` (Gate 10). What stays partial is recorded debt, not a missing check: 11 layout transitions and 41 CSS `hardcodedCssMotionDurations`. Path to blocking: retire those and pin both at zero. |
+| 8 | Edge ownership — no `border-*` + `ring-*` on one surface; no 1px spread in a drop shadow | **implemented-partial** | Corrected 9 Aug 2026: the previous evidence — "the per-surface co-occurrence lint and spread-term check are planned (PR 9)" — was **false**, and had been since before this row was written. Both ship. `edgeOwnershipConflicts` is an AST co-occurrence check over resolved class roots (`BORDER_WIDTH_UTILITY` × `RING_WIDTH_UTILITY`), and `onePixelShadowSpreads` reads the fourth length of every `box-shadow` layer. Both ratchet **per path**, so a new conflict on any surface fails today. What is partial is the recorded debt, not the check: **16 conflicts across 7 files**, and 0 spreads in `globals.css`. (Corrected 15 Aug 2026 — this cell said 27 across 15, which PR #1942 had already paid down to 25 across 12 without updating the prose. The §3 row below and `scripts/design-system-contract-baseline.json` both already said 25, and per `docs/design-system/README.md` the baseline outranks this document, so the prose was the defect. Ledger `#265` carried the same stale figure.) Path to blocking: retire that debt and pin both at zero. The spread form was chosen _because_ it is mechanically lintable, and it duly was. |
+| 9 | No layout-property animation; no hardcoded durations | **implemented-partial** | Corrected 9 Aug 2026: "a layout-property lint is still planned" was **false**, and leaving it would have preserved the exact contradiction this pass exists to remove. `layoutTransitionExceptions` ships and ratchets **per path** against `SAFE_TRANSITION_PROPERTIES`, so a new layout-property transition in any file fails today; the phone chrome's intentional `grid-template-rows` / `height` / `padding-bottom` are among the 11 recorded exceptions rather than a gap. The Tailwind duration form is a **hard zero** (`hardcodedMotionClasses`), on top of PR-Arch tokenising production `duration-*`/`ease-*` literals onto `--duration-*` / `--ease-*` (Gate 10). What stays partial is recorded debt, not a missing check: 11 layout transitions and 42 CSS `hardcodedCssMotionDurations`. Path to blocking: retire those and pin both at zero. |
| 10 | Named z and motion tokens have real consumers; no unapproved `z-` value | **implemented-blocking** | PR-Arch: off-ladder `z-50` retired; toast at `--z-toast` (`z-[110]`, lint allowlist); motion literals map to named duration/ease tokens including phone-chrome hide/reveal pair. Dead `OverlayProvider` deleted. |
| 11 | Published-type conformance — every `dtsPropsFor` entry exactly matches source props | **implemented-blocking** | `generate-design-sync-contract.mjs` uses the TypeScript checker to derive every public property from the exported source `*Props` type; `--check` and `check-design-sync-contract.mjs` reject drift, missing named types, or machine-specific declarations. Zero-prop visual roots are explicit. |
-| 12 | Publication coverage — every registered visual component has source, export, preview, prop contract and direct publication test | **implemented-blocking** | Registry/map/props parity and all 53 direct component rows are pinned by the design-sync contract and `design-sync-visual-exports.test.ts`. Behavioural, full visual-state, print, and browser acceptance remain separate gates; `DocumentFrame` is built shell-only in product but not yet among the 53 registered visual exports (print primitives remain specified-not-built). |
+| 12 | Publication coverage — every registered visual component has source, export, preview, prop contract and direct publication test | **implemented-blocking** | Registry/map/props parity and all 55 direct component rows are pinned by the design-sync contract and `design-sync-visual-exports.test.ts`. Behavioural, full visual-state, print, and browser acceptance remain separate gates; `DocumentFrame` is built shell-only in product but not yet among the 55 registered visual exports (print primitives remain specified-not-built). |
**Computed-style suites (planned, PR 1–2):** Tailwind conflict order · v2 cascade, ancestor
**and** same-node forms · HCM remapping under all three v2 selectors · z-index · target
geometry · contrast · print token reset. Class-string assertions are not accepted for any of
these — computed values only.
-**Reference preview coverage is implemented-blocking for all 53 registered visual exports.**
+**Reference preview coverage is implemented-blocking for all 55 registered visual exports.**
The full runtime visual state matrix remains planned: default · hover · active · focus-visible ·
disabled · busy · invalid · long content · 320px · dark · forced colours · reduced motion ·
print where relevant. 320px is blocking for newly adopted components; print stays manual per
@@ -111,15 +111,15 @@ theme-list parity, and remote design-project publication remain separate concern
| Put `--decoration-soft`/`--text-soft` on a text node | Tier pinned both sides in the token contract; recipe AST (`decoration-on-text.contract.test.ts`) | implemented-blocking (recipes); broader AST follow-on |
| Uppercase a dose unit | `Quantity` pins `normal-case` + unit test | implemented-blocking (branch) |
| Invert a PDF, diagram or clinical image in any theme | `check:design-system-contract` — `imageInversions`, pinned at **zero**, not ratcheted | **implemented-blocking** (9 Aug 2026) — CSS `filter`/`backdrop-filter` plus the Tailwind `invert`/`hue-rotate` utilities; see §5 |
-| Border **and** ring on one surface, or a 1px spread in a drop shadow | `check:design-system-contract` — `edgeOwnershipConflicts` (18) + `onePixelShadowSpreads` (2) | **implemented-blocking for new use** — AST/CSS ratchets with per-path pins; the recorded debt itself is Gate 8's remaining half |
+| Border **and** ring on one surface, or a 1px spread in a drop shadow | `check:design-system-contract` — `edgeOwnershipConflicts` (16) + `onePixelShadowSpreads` (0) | **implemented-blocking for new use** — AST/CSS ratchets with per-path pins; the recorded debt itself is Gate 8's remaining half |
| A child shadow heavier than its parent's | Gate 7 | implemented-partial |
-| Use `--shadow-tight`/any alias in new code | `check:design-system-contract` — `legacyShadowAliases`, ratcheted at 89 with per-path pins | **implemented-blocking for new use** — a new alias in any file fails. `--shadow-tight` itself is retired onto `--e1` and additionally pinned by a tracked-tree sweep in `tests/design-token-contract.test.ts` (it was silently reverted once by the `acf78bf` merge); the remaining 89 are the other six aliases, still `#262` |
-| Raw pixel size, padding, radius, gap or line-height in markup | `check:design-system-contract` — `rawPaddingLiterals` (52), `rawRadiusLiterals` (20), `rawGapLiterals` (25), `rawMarginLiterals` (74), `rawLineHeightLiterals` (3) | **implemented-blocking for new use** (9 Aug 2026) — per-path ratchets over both the utility and the CSS-declaration spelling, so a literal cannot move into `globals.css` to escape. Values containing a CSS function (`env(`, `clamp(`, `max(`, `calc(`) are sanctioned computed forms and exempt. Raw _size_ is still covered only for tap/shadow/colour |
+| Use `--shadow-tight`/any alias in new code | `check:design-system-contract` — `legacyShadowAliases`, ratcheted at 87 with per-path pins | **implemented-blocking for new use** — a new alias in any file fails. `--shadow-tight` itself is retired onto `--e1` and additionally pinned by a tracked-tree sweep in `tests/design-token-contract.test.ts` (it was silently reverted once by the `acf78bf` merge); the remaining 87 are the other six aliases, still `#262` |
+| Raw pixel size, padding, radius, gap or line-height in markup | `check:design-system-contract` — `rawPaddingLiterals` (42), `rawRadiusLiterals` (20), `rawGapLiterals` (20), `rawMarginLiterals` (53), `rawLineHeightLiterals` (0) | **implemented-blocking for new use** (9 Aug 2026) — per-path ratchets over both the utility and the CSS-declaration spelling, so a literal cannot move into `globals.css` to escape. Values containing a CSS function (`env(`, `clamp(`, `max(`, `calc(`) are sanctioned computed forms and exempt. Raw _size_ is still covered only for tap/shadow/colour |
| Animate `width`, `height`, `grid-template-*`, `top`, `left`, `gap` | `check:design-system-contract` — `layoutTransitionExceptions`, ratcheted at 11 with per-path pins | **implemented-blocking for new use** — `SAFE_TRANSITION_PROPERTIES` carries the compositor-only allowlist; phone chrome's deliberate `grid-template-rows` is in the recorded 11 |
-| Hardcode a transition duration | `check:design-system-contract` — `hardcodedMotionClasses` (**zero**) + `hardcodedCssMotionDurations` (41) | **implemented-blocking** for the Tailwind `duration-*`/`delay-*`/`transition-all` form; the CSS form is a ratchet, so its 41 are debt |
+| Hardcode a transition duration | `check:design-system-contract` — `hardcodedMotionClasses` (**zero**) + `hardcodedCssMotionDurations` (42) | **implemented-blocking** for the Tailwind `duration-*`/`delay-*`/`transition-all` form; the CSS form is a ratchet, so its 42 are debt |
| A `z-[N]` literal outside the named rungs, in JS/TS only | `require-z-index-ladder` (ESLint) | implemented-blocking. Scope: `z-[N]` literals in JS/TS AST nodes. Plain `z-30` utilities and every `.css` / `.module.css` declaration are invisible to an ESLint rule - `sidebar-live-shell.module.css` carries an off-ladder `z-index: var(--z-sidebar-flyout, 50)` this cannot see. |
| Ship a registered component without a direct publication test | Gate 12 | implemented-blocking |
-| Lower a production tap target below 48px | `check:design-system-contract` — `interactiveTapFloorDeclarations` (33) + Gate 2's phone-viewport `ui-style-contract` audit + `ui-smoke` flake history | **implemented-blocking for new use** (15 Aug 2026) — a new interactive control declaring a sub-floor `min-h-*`, including comparable arbitrary values and any reachable conditional branch, fails on both the total and its own path; the 33 recorded sites remain debt. Component-wrapper tags such as ` ` are a known blind spot |
+| Lower a production tap target below 48px | `check:design-system-contract` — `interactiveTapFloorDeclarations` (32) + Gate 2's phone-viewport `ui-style-contract` audit + `ui-smoke` flake history | **implemented-blocking for new use** (15 Aug 2026) — a new interactive control declaring a sub-floor `min-h-*`, including comparable arbitrary values and any reachable conditional branch, fails on both the total and its own path; the 32 recorded sites remain debt. Component-wrapper tags such as ` ` are a known blind spot |
| Construct an enabled control with no action, destination or name | PR 4 discriminated unions + DOM contracts; `require-button-wiring` covers raw `` | implemented-blocking (Citation/Chip/ToggleSwitch/RadioGroup) |
| Render `AnswerCard` without a verification and answer state | PR 6 required props (type-level) | implemented-blocking in `AnswerCard`; live answer-shell adoption remains compatibility-layer work |
| Use a bare dash for a missing clinical value | `MissingValue` + review | implemented-partial — `AccessibleTable` composes `MissingValue`; broader legacy call-site convergence remains manual |
@@ -187,7 +187,7 @@ Recorded at export (31 July 2026), **not re-run for this document set**:
`tests/installed-lock-parity.test.ts`, environmental (`brace-expansion` lock drift, issue
`#149`, fixed by `npm ci`), pre-existing and unrelated
- `npm run verify:ui` — **344 passed**, exit 0
-- `check:design-system-contract` — pass: 625 production files, raw colours 2, literal
+- `check:design-system-contract` — pass: 625 production files, raw colours 0, literal
shadows 0, legacy tap classes 0
The honest sentence includes the failure. Exit code 0 alone is never proof — quote the
diff --git a/docs/design-system/README.md b/docs/design-system/README.md
index 98140c8b97..701a879c38 100644
--- a/docs/design-system/README.md
+++ b/docs/design-system/README.md
@@ -28,6 +28,7 @@ Reading order:
assumptions, and the resolution log.
5. [GATES.md](GATES.md) — every rule paired with its enforcement status. A prohibition
with no row there is a suggestion.
+6. [FIX-GUIDE.md](FIX-GUIDE.md) — Hazard 1–2 sweep dispositions (Fixed / Documented / Deferred / Out-of-scope).
Canonical code: `src/app/ckb-v2-tokens.css` is the **v2 target layer** and `globals.css` remains
the compatibility layer. The source now mounts `.ckb-v2` literally on the global ``, so every
diff --git a/docs/design-system/SPEC.md b/docs/design-system/SPEC.md
index 143e140a99..f5cf4097d9 100644
--- a/docs/design-system/SPEC.md
+++ b/docs/design-system/SPEC.md
@@ -665,20 +665,21 @@ and adoption.** Status keys as in the header; "done" entries cite their commit.
### Phase 2 — values, split three ways
-| PR | Contents | Status |
-| --------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| PR 5a · Token values, no geometry | Colour, elevation, ink roles | **done** — `59e4c3dfc` landed the `--shadow-well` rename, the spine and status-mark families, and the dark `--clinical-chat-document` fix; the remaining ink-role deltas landed with PR 3 (`--text-placeholder`, eyebrows and placeholders off the decoration tier, the disabled encoding). The v2 light and dark blocks now declare the same colour roles, and the only raw colour literals left in `src/` are the two `#0f766e` accent defaults, which are **not** design tokens — see the note below |
-| PR 5b · Tap 44→48 in `@theme` | The 426-site tap-call-site migration; contract-test pin update; visual QA pass; `--tap-min` becomes the alias | **done** — `--spacing-tap: 3rem` in `@theme`; `--tap-min` reduced to `var(--spacing-tap)`; 426 `*-tap` call sites moved; three pins flipped in the same commit; the phone composer keeps a written 44px exception below 431px (§4.10) |
-| PR 5c · Radius step | Every `rounded-md` moves; its own visual diff | **done** — live `@theme --radius-md` 8px → 10px, matching the v2 control rung and moving 243 `rounded-md` call sites; the 4px-grid pin now names both half-steps and asserts the two layers agree; 14 arbitrary radius literals absorbed onto the ladder (§4.6) |
+| PR | Contents | Status |
+| --------------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| PR 5a · Token values, no geometry | Colour, elevation, ink roles | **done** — `59e4c3dfc` landed the `--shadow-well` rename, the spine and status-mark families, and the dark `--clinical-chat-document` fix; the remaining ink-role deltas landed with PR 3 (`--text-placeholder`, eyebrows and placeholders off the decoration tier, the disabled encoding). The v2 light and dark blocks now declare the same colour roles. The two `#0f766e` accent defaults are scoped `RAW_COLOR_EXEMPTIONS`, so `rawColorLiterals` is **0** — they are data, not design tokens; see the note below |
+| PR 5b · Tap 44→48 in `@theme` | The 426-site tap-call-site migration; contract-test pin update; visual QA pass; `--tap-min` becomes the alias | **done** — `--spacing-tap: 3rem` in `@theme`; `--tap-min` reduced to `var(--spacing-tap)`; 426 `*-tap` call sites moved; three pins flipped in the same commit; the phone composer keeps a written 44px exception below 431px (§4.10) |
+| PR 5c · Radius step | Every `rounded-md` moves; its own visual diff | **done** — live `@theme --radius-md` 8px → 10px, matching the v2 control rung and moving 243 `rounded-md` call sites; the 4px-grid pin now names both half-steps and asserts the two layers agree; 14 arbitrary radius literals absorbed onto the ladder (§4.6) |
-**`#0f766e` is data, not a token.** The two remaining raw colour literals
+**`#0f766e` is data, not a token.** The two `#0f766e` accent defaults
(`src/lib/medications.ts`, `src/lib/medication-records.ts`) restate a Postgres column
default (`accent text not null default '#0f766e'`) for a per-record, user-chosen accent
colour. Changing the application default without migrating the database default would
-diverge the two, so both stay. The raw-colour ratchet in
-`scripts/design-system-contract-baseline.json` is a **ceiling, not a target**: it exists to
-stop new literals appearing, and a value that is persisted data rather than design intent
-is outside the token system entirely.
+diverge the two, so both stay. They are a **scoped** `RAW_COLOR_EXEMPTIONS` entry, not
+remaining debt: `scripts/design-system-contract-baseline.json` pins `rawColorLiterals`
+at **0**. The ratchet is a **ceiling, not a target**: it exists to stop new literals
+appearing, and a value that is persisted data rather than design intent is outside the
+token system entirely.
### Phase 3 — safety structure
diff --git a/docs/design-system/TOKENS.md b/docs/design-system/TOKENS.md
index ceafcf9cf6..f28e5977cc 100644
--- a/docs/design-system/TOKENS.md
+++ b/docs/design-system/TOKENS.md
@@ -41,27 +41,27 @@ This is the two-sources-of-truth failure C1 exists to kill. One winner per role:
Declared in the `.ckb-v2` structural block **[verified: full read at `ef13a072a`]**.
-| Group | Roles | Notes |
-| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Space scale | `--space-0` … `--space-11` | 4px base. Markup never references the raw scale — semantic tokens only. |
-| Semantic gaps | `--gap-tight` · `--gap-inline` · `--gap-stack` · `--gap-block` · `--gap-section` | The only inter-element spacing markup may use. |
-| Semantic padding | `--pad-chip-x` · `--pad-control-x` · `--pad-cta-x` · `--pad-card` · `--pad-panel` · `--pad-strip` | Heading-inset convention rides on these (SPEC §4.6). |
-| Page | `--page-gutter` · `--page-max` · `--measure` · `--header-h` | `--measure` is the prose measure — never wider. |
-| Type steps | `--text-{xs,sm,body,md,lg,xl}` each with `-lh` and `-tr` · `--text-hero` · `--text-hero--line-height` · `--text-hero-tr` | Seven steps, per-step line-height and tracking (gated). ⚠️ `--text-hero--line-height` is the one double-dash name — keep as-is; renaming is churn without a defect. |
-| Chip sizes | `Chip` `compact` → `--text-2xs` (legacy compat step); `standard` (default) → `--text-xs` | Canonical (#220): dense surfaces opt into `size="compact"`; default adoption keeps `--text-xs`. `--text-2xs` stays a compatibility alias for Chip compact / legacy call sites — not a new v2 type step to invent against. Do not densify every Chip to chase the globals “small chips” note. |
-| Type companions | `--leading-prose` · `--tracking-eyebrow` · `--nums` | `--nums` = numeric variant for all data. |
-| Weights | `--font-weight-{body,label,heading,value}` | One job each; display uses heading weight (SPEC §4.6). |
-| Radius | `--radius-{sm,md,lg,xl,2xl}` | One step per surface role. v2 and live `--radius-md` agreed in PR 5c and are now pinned to each other by the live token contract. |
-| Icons | `--icon-{xs,sm,md,lg}` | Paired to adjacent type steps. |
-| Density | `--tap-min` (alias of `--spacing-tap`, §1) · `--chip-height` · `--row-comfortable` · `--row-compact` · `--cell-pad-{comfortable,compact}` | Tap target is not row height (gated on resolved values, not strings, now that `--tap-min` is an alias). |
-| Accent rules | `--rule-w` · `--rule-accent` · `--rule-warning` | ⚠️ Reference `var(--clinical-accent)` / `var(--warning)` **without declaring them** — v2 depends on the live layer for both (§5). |
-| Evidence gutter | `--gutter-col` · `--gutter-dot` · `--gutter-line-w` | One gutter column owns line and dot (SPEC §7). |
-| Evidence spine | `--spine-w` · `--spine-current` · `--spine-stale` | Authored `59e4c3dfc` (§1); derived from rule/accent/warning roles. |
-| Status mark | `--status-mark-size` · `--status-mark-stroke` | Authored `59e4c3dfc` (§1); codifies `StatusMark`'s inline geometry. |
-| Quantity | `--quantity-unit-tracking` · `--quantity-unit-gap` | Winners per §1. |
-| Dashed edge | `--border-dashed` | Drop targets / "nothing here yet" — distinct from `--border-strong`, which means emphasis. |
-| Stacking | `--z-{base,raised,chrome,overlay,popover,modal,toast}` | Each rung names its `--eN` partner; toast above modal deliberately. |
-| Motion | `--duration-{fast,base,slow}` · `--ease-standard` · `--ease-physical` | Durations zero under `prefers-reduced-motion` (gated). |
+| Group | Roles | Notes |
+| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Space scale | `--space-0` … `--space-11` | 4px base. Markup never references the raw scale — semantic tokens only. |
+| Semantic gaps | `--gap-tight` · `--gap-inline` · `--gap-stack` · `--gap-block` · `--gap-section` | The only inter-element spacing markup may use. |
+| Semantic padding | `--pad-chip-x` · `--pad-control-x` · `--pad-cta-x` · `--pad-card` · `--pad-panel` · `--pad-strip` | Heading-inset convention rides on these (SPEC §4.6). |
+| Page | `--page-gutter` · `--page-max` · `--measure` · `--header-h` | `--measure` is the prose measure — never wider. |
+| Type steps | `--text-{xs,sm,body,md,lg,xl}` · `--text-hero` · `--text-hero--line-height` · `--text-hero-tr` | Seven steps. Shared `--leading-prose`; hero keeps `--text-hero--line-height` / `--text-hero-tr` (gated). Unused xs–xl `-lh`/`-tr` and `--text-{step}--line-height` companions are orphans — do not re-require them. ⚠️ `--text-hero--line-height` is the one double-dash name — keep as-is; renaming is churn without a defect. |
+| Chip sizes | `Chip` `compact` → `--text-2xs` (legacy compat step); `standard` (default) → `--text-xs` | Canonical (#220): dense surfaces opt into `size="compact"`; default adoption keeps `--text-xs`. `--text-2xs` stays a compatibility alias for Chip compact / legacy call sites — not a new v2 type step to invent against. Do not densify every Chip to chase the globals “small chips” note. |
+| Type companions | `--leading-prose` · `--tracking-eyebrow` · `--nums` | `--nums` = numeric variant for all data. |
+| Weights | `--font-weight-{body,label,heading,value}` | One job each; display uses heading weight (SPEC §4.6). |
+| Radius | `--radius-{sm,md,lg,xl,2xl}` | One step per surface role. v2 and live `--radius-md` agreed in PR 5c and are now pinned to each other by the live token contract. |
+| Icons | `--icon-{xs,sm,md,lg}` | Paired to adjacent type steps. |
+| Density | `--tap-min` (alias of `--spacing-tap`, §1) · `--chip-height` · `--row-comfortable` · `--row-compact` · `--cell-pad-{comfortable,compact}` | Tap target is not row height (gated on resolved values, not strings, now that `--tap-min` is an alias). |
+| Accent rules | `--rule-w` · `--rule-accent` · `--rule-warning` | ⚠️ Reference `var(--clinical-accent)` / `var(--warning)` **without declaring them** — v2 depends on the live layer for both (§5). |
+| Evidence gutter | `--gutter-col` · `--gutter-dot` · `--gutter-line-w` | One gutter column owns line and dot (SPEC §7). |
+| Evidence spine | `--spine-w` · `--spine-current` · `--spine-stale` | Authored `59e4c3dfc` (§1); derived from rule/accent/warning roles. |
+| Status mark | `--status-mark-size` · `--status-mark-stroke` | Authored `59e4c3dfc` (§1); codifies `StatusMark`'s inline geometry. |
+| Quantity | `--quantity-unit-tracking` · `--quantity-unit-gap` | Winners per §1. |
+| Dashed edge | `--border-dashed` | Drop targets / "nothing here yet" — distinct from `--border-strong`, which means emphasis. |
+| Stacking | `--z-{base,raised,chrome,overlay,popover,modal,toast}` | Each rung names its `--eN` partner; toast above modal deliberately. |
+| Motion | `--duration-{fast,base,slow}` · `--ease-standard` · `--ease-physical` | Durations zero under `prefers-reduced-motion` (gated). |
## 3 · v2 shell inventory — light and dark blocks
@@ -96,26 +96,26 @@ the full pair-matrix gate lands (GATES §2, gate 1).
The v2 layer _references_ or _depends on_ these; their values stay in `live` / `@theme`:
-| Role | Owner | Rule |
-| ------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------- |
-| `--spacing-tap` | `@theme` | **The** tap knob; generates `min-h-tap`/`h-tap`/`size-tap`/`min-w-tap`/`w-tap` (426 call sites). 44→48 landed here (PR 5b). |
-| `--clinical-accent` | `live` | Consumed by v2 `--rule-accent` without declaration — inherited dependency. |
-| `--warning` | `live` | Consumed by v2 `--rule-warning` — same. |
-| `--danger`, `--danger-solid`, `--danger-solid-contrast` | `live` | Filled danger pairs with `--danger-solid-contrast` (HCM-mapped to `MarkText`); pairing enforced from PR 3. |
-| `--success`, `--info` | `live` | Clinical/status palette — reserved channel. |
-| `--focus` | `live` | The only focus outline colour; no companion ring, ever. |
-| `--ring-highlight`, `--ring-glass` | `live` | Highlight rings on lux surfaces; each carries its own per-theme value, so `dark:ring-*` is gated at zero (GATES §3). |
-| `--text-placeholder` | `planned` (PR 3) | New role ≥4.5:1; until it exists, placeholders use `--text-muted`. |
+| Role | Owner | Rule |
+| ------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
+| `--spacing-tap` | `@theme` | **The** tap knob; generates `min-h-tap`/`h-tap`/`size-tap`/`min-w-tap`/`w-tap` (426 call sites). 44→48 landed here (PR 5b). |
+| `--clinical-accent` | `live` | Consumed by v2 `--rule-accent` without declaration — inherited dependency. |
+| `--warning` | `live` | Consumed by v2 `--rule-warning` — same. |
+| `--danger`, `--danger-solid`, `--danger-solid-contrast` | `live` | Filled danger pairs with `--danger-solid-contrast` (HCM-mapped to `MarkText`); pairing enforced from PR 3. |
+| `--success`, `--info` | `live` | Clinical/status palette — reserved channel. |
+| `--focus` | `live` | The only focus outline colour; no companion ring, ever. |
+| `--ring-highlight`, `--ring-glass` | `live` | Highlight rings on lux surfaces; each carries its own per-theme value, so `dark:ring-*` is gated at zero (GATES §3). |
+| `--text-placeholder` | `live` | Declared in globals and v2; ≥4.5:1 placeholder ink. Not planned. No further role migration this sweep. |
## 6 · Deprecations and deletions
| Token | Disposition | Gate |
| ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `--text-soft` | Deprecated alias of `--decoration-soft`; both resolve identically during the window. Delete when zero references remain outside the alias declaration. | Contract test pins the tier from both sides; a lint for `--text-soft`/`--decoration-soft` on text-bearing nodes is planned (GATES §1). |
-| `--shadow-focus` | **Delete** (PR 9) — encodes a companion focus ring the conventions forbid; a trap for the next person who greps "focus". | Planned lint after deletion. |
-| `--shadow-lift` | Retire into the `--eN` ladder (PR 9). | Planned. |
+| `--shadow-focus` | **Deleted** — encoded a companion focus ring the conventions forbid. Focus is an outline, not a ring. | Gated gone by `tests/design-token-contract.test.ts`. |
+| `--shadow-lift` | Retire into the `--eN` ladder (PR 9). Still has live consumers — do not delete until those move. | Planned. |
| `--shadow-card`, `--shadow-soft` | Aliases of a ladder step; retire **inside the recipes first**, then delete. | Planned. |
-| `--spring-bouncy` + two other dead springs | Delete (PR 9); byte-duplicate and unused curves. | Planned. |
+| `--spring-bouncy` + two other dead springs | **Deleted** (2026-08-21) — unused curves. `--ease-spring` remains. | Done. |
| `--quantity-unit-scale` (design side) | Never lands; superseded per §1. | Next design sync removes it. |
| Legacy type steps (`text-2xs`/`3xs`, `sm-minus`, `base-minus`, `2xl-minus`, `lg-minus`, `3xl-minus`, `3xl/4xl/5xl`) | Retired **last of all** — ≈663 call sites; `--text-md` arrives additively first. ⚠️ `Quantity` currently consumes `text-base-minus` — fix in the retirement tranche. `--text-2xl-compact` left this list early (`#297`): it had zero consumers, so retiring it needed no tranche and rendered identically. | Contract ratchet extension, planned. |
@@ -173,5 +173,6 @@ Two consequences already observed and fixed elsewhere: 44px tap targets survived
named `--z-*` ladder entirely.
**To retire it**, map the spacing scale onto `--gap-*`/`--pad-*`, route `--ward-z-*` onto local
-stacking contexts using the named rungs, and adopt the per-step `--text-*-lh` companions in
-place of `--ward-leading-*`. Each is a visual change and wants its own review.
+stacking contexts using the named rungs, and adopt `--leading-prose` (hero line-height where
+display type applies) in place of `--ward-leading-*`. Each is a visual change and wants its own
+review. Do not reintroduce per-step `--text-*-lh` orphans.
diff --git a/docs/process-hardening.md b/docs/process-hardening.md
index 1124dc6269..3904585a72 100644
--- a/docs/process-hardening.md
+++ b/docs/process-hardening.md
@@ -648,7 +648,7 @@ passes `p_worker_id`. Ordered apply steps, R17 manual `CONCURRENTLY` index, and
## Design convergence & type-scale ratchet (2026-07-06)
-- **`docs/design-system.md` is now the front door** for all UI work: token contract, type-scale rules, z-index ladder, Sheet-only modals, a11y requirements, and the UI Definition of Done. The `docs/redesign/*` documents remain the deep references it links to.
+- **`docs/design-system/README.md` is now the front door** for all UI work: token contract, type-scale rules, z-index ladder, Sheet-only modals, a11y requirements, and the UI Definition of Done. [`docs/design-system.md`](./design-system.md) remains live-layer notes during the v1→v2 transition. The `docs/redesign/*` documents remain the deep references they link to.
- **Type-scale ratchet — backlog cleared, gate now strict:** `node scripts/check-type-scale.mjs --strict` reports **0 hits / 0 files** (this pass retires the last 8 hits in 1 file; the prior recorded baseline was 20/9, originally 168/22). The compact mode-home hero now uses the shared fluid `--text-hero` scale; the temporary mode-home-only aliases were removed after confirming no consumers remained. `check:type-scale --strict` is wired into `verify:cheap` (package.json), so any newly introduced arbitrary `text-[px|rem|em]` size now fails the gate — UI PRs must keep the count at zero. Colour utilities (`text-[color:var(--…)]`) are the sanctioned token form and are not counted.
- **Cleared this pass:** dead launcher mobile detail rows now expand (aria-expanded disclosures); launcher detail dialog migrated to the `Sheet` primitive (focus trap/return-focus restored); launcher filter tablists gained `aria-controls` + a `role="tabpanel"` results region; styled `src/app/not-found.tsx` added (the `notFound()` calls in differentials no longer fall through to the unstyled default); `?page=abc` NaN leak in the document viewer clamped; `/services` off-palette preview deleted (dead export) and the live navigator's residual hardcodes tokenized; launcher icon tones moved from raw Tailwind palette classes to categorical `--type-*` / semantic danger triads (dark-mode + forced-colors correct); mockups layout emits `robots: noindex`.
diff --git a/docs/scripts-index.md b/docs/scripts-index.md
index a508e866f2..c6b9af14f3 100644
--- a/docs/scripts-index.md
+++ b/docs/scripts-index.md
@@ -49,7 +49,12 @@ migration has shipped (see `docs/maturity-backlog-workorders.md` L1).
`check-maintainability-budgets.mjs`, `check-upload-limit-parity.mjs`,
`check-codebase-index-coverage.mjs`, `check-docs-links.mjs`,
`check-docs-script-refs.mjs`, `check-bundle-budget.mjs`, `check-type-scale.mjs`,
-`check-icon-scale.mjs`, `check-design-system-contract.mjs`, `check-function-grants.mjs`,
+`check-icon-scale.mjs`, `check-design-system-contract.mjs`,
+`generate-design-system-adoption.mjs` (`npm run design-system:adoption:update` /
+`check:design-system-adoption`), `generate-design-sync-contract.mjs` +
+`check-design-sync-contract.mjs` (`npm run design-system:design-sync:update` /
+`check:design-sync-contract`), `adopt-visual-baselines.mjs`
+(`npm run design-system:baselines:adopt`), `check-function-grants.mjs`,
`check-owner-scope-api.mjs`, `check-client-bundle-secrets.mjs`, `verify-pr-local.mjs`,
`verify-release-offline.mjs`, `check-codex-cloud-setup.mjs`. `check-gate-manifest.mjs` cross-checks that every gate in the
`verify:cheap:internal` chain also runs in CI's `static-pr` job, so the two lists can't drift.
diff --git a/docs/testing.md b/docs/testing.md
index 44ac18ea5c..7dcda78932 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -430,10 +430,10 @@ The scheduled `release-browser-matrix` workflow provides cross-engine regression
Before opening a UI PR, confirm:
- **Reuse first.** Check `src/components/ui-primitives.tsx` (class recipes plus `IconButton`, `AsyncButton`, `InlineNotice`, `EmptyState`, `LoadingPanel`, `ToggleSwitch`) and `src/components/ui/sheet.tsx` (the only overlay primitive) before hand-rolling. Icon-only buttons use `IconButton` (its `label` is a required prop).
-- **Tokens only.** No raw hex or Tailwind palette classes, no literal shadows, no `text-[Npx]` — see [`docs/design-system.md`](./design-system.md) §1–§5. `check:design-system-contract`, `check:type-scale`, and `check:icon-scale` enforce this.
+- **Tokens only.** No raw hex or Tailwind palette classes, no literal shadows, no `text-[Npx]` — see [`docs/design-system/README.md`](./design-system/README.md). `check:design-system-contract`, `check:type-scale`, and `check:icon-scale` enforce this.
- **States.** Handle loading / empty / error / disabled where they apply; async surfaces expose a retry, not a dead end.
-- **Accessibility** ([design-system §7](./design-system.md)): keyboard operable, visible focus, accessible names on icon controls, live regions for async status, and reduced motion honoured — scripted `scrollTo`/`scrollIntoView` go through `resolveScrollBehavior` (`src/lib/scroll-behavior.ts`), never a hard-coded `behavior: "smooth"`.
+- **Accessibility** ([design-system](./design-system/README.md)): keyboard operable, visible focus, accessible names on icon controls, live regions for async status, and reduced motion honoured — scripted `scrollTo`/`scrollIntoView` go through `resolveScrollBehavior` (`src/lib/scroll-behavior.ts`), never a hard-coded `behavior: "smooth"`.
- **Tests.** Add a `.dom.test.tsx` for changed component behaviour (see "Component tests" above) and update the E2E journeys for changed flows.
- **Unlayered CSS.** If the change adds a class rule outside `@layer` that sets a border, background, colour, shadow or outline, `tests/style-contract-registry.test.ts` will fail until it is registered. Add a rendered-effect contract rather than an exemption where the rule matters visually — see "Visual regression and style contracts".
-- **Verify** ([design-system §9](./design-system.md)): follow the risk tiers in root `AGENTS.md`. Prove changed component behaviour with the focused DOM test first; run `npm run ensure` before browser work and use the narrowest affected journey. Select one appropriate broad handoff gate when the diff crosses owners, cannot be bounded, or applicable PR/handoff policy requires it; do not routinely stack `verify:cheap`, `verify:pr-local`, and `verify:ui`. Add a manual dark-mode + forced-colors spot check when those rendered states can plausibly change.
+- **Verify** ([design-system](./design-system/README.md) and GATES): follow the risk tiers in root `AGENTS.md`. Prove changed component behaviour with the focused DOM test first; run `npm run ensure` before browser work and use the narrowest affected journey. Select one appropriate broad handoff gate when the diff crosses owners, cannot be bounded, or applicable PR/handoff policy requires it; do not routinely stack `verify:cheap`, `verify:pr-local`, and `verify:ui`. Add a manual dark-mode + forced-colors spot check when those rendered states can plausibly change.
- Architecture and state-ownership conventions: [`docs/frontend-architecture.md`](./frontend-architecture.md).
diff --git a/scripts/design-system-contract-baseline.json b/scripts/design-system-contract-baseline.json
index 326282fc01..bd3d6e5e73 100644
--- a/scripts/design-system-contract-baseline.json
+++ b/scripts/design-system-contract-baseline.json
@@ -1,48 +1,44 @@
{
"metrics": {
- "rawColorLiterals": 2,
+ "rawColorLiterals": 0,
"literalShadowClasses": 0,
"legacyTapClasses": 0,
- "interactiveTapFloorDeclarations": 33,
+ "interactiveTapFloorDeclarations": 32,
"colourOnlyStatusIndicators": 0,
"statusColouredNumerals": 0,
- "edgeOwnershipConflicts": 18,
- "onePixelShadowSpreads": 2,
+ "edgeOwnershipConflicts": 16,
+ "onePixelShadowSpreads": 0,
"hardcodedCssMotionDurations": 42,
"rawCssZIndices": 8,
"legacyPaletteUtilities": 0,
"darkColorOverrides": 0,
- "legacyShadowAliases": 89,
+ "legacyShadowAliases": 87,
"arbitraryTracking": 0,
- "rawPaddingLiterals": 58,
- "rawRadiusLiterals": 24,
- "rawGapLiterals": 28,
- "rawMarginLiterals": 76,
- "rawLineHeightLiterals": 3,
+ "rawPaddingLiterals": 42,
+ "rawRadiusLiterals": 20,
+ "rawGapLiterals": 20,
+ "rawMarginLiterals": 53,
+ "rawLineHeightLiterals": 0,
"layoutTransitionExceptions": 11,
"textSoftConsumers": 0,
"errorStateCountProps": 0,
"failedStateResultCounts": 0
},
"debtByPath": {
- "rawColorLiterals": {
- "src/lib/medication-records.ts": 1,
- "src/lib/medications.ts": 1
- },
+ "rawColorLiterals": {},
"literalShadowClasses": {},
"legacyTapClasses": {},
"interactiveTapFloorDeclarations": {
- "src/components/DocumentTagCloud.tsx": 2,
"src/components/calculators/bedside-sheet.tsx": 3,
"src/components/calculators/calculator-ui.tsx": 3,
"src/components/calculators/clinical-console.tsx": 2,
"src/components/calculators/directory-grid.tsx": 1,
- "src/components/clinical-dashboard/DocumentManagerPanel.tsx": 6,
"src/components/clinical-dashboard/answer-content.tsx": 1,
"src/components/clinical-dashboard/document-admin.tsx": 5,
- "src/components/clinical-dashboard/favourites-command-library-page.tsx": 1,
+ "src/components/clinical-dashboard/DocumentManagerPanel.tsx": 6,
"src/components/clinical-dashboard/favourites-hub.tsx": 2,
"src/components/clinical-dashboard/settings-dialog.tsx": 3,
+ "src/components/DocumentTagCloud.tsx": 2,
"src/components/forms/form-detail-page.tsx": 1,
"src/components/forms/forms-search-results-page.tsx": 2,
"src/components/ui/chip.tsx": 1
@@ -50,19 +46,15 @@
"colourOnlyStatusIndicators": {},
"statusColouredNumerals": {},
"edgeOwnershipConflicts": {
- "src/components/DocumentTagCloud.tsx": 3,
"src/components/clinical-dashboard/dashboard-nav.tsx": 1,
"src/components/clinical-dashboard/favourites-command-library-page.tsx": 1,
"src/components/clinical-dashboard/master-search-header.tsx": 5,
+ "src/components/DocumentTagCloud.tsx": 3,
"src/components/formulation/formulation-builder-page.tsx": 3,
- "src/components/formulation/formulation-compare-page.tsx": 1,
"src/components/specifiers/specifier-builder-page.tsx": 1,
- "src/components/specifiers/specifier-compare-page.tsx": 1,
"src/components/ui-primitives.tsx": 2
},
- "onePixelShadowSpreads": {
- "src/app/globals.css": 2
- },
+ "onePixelShadowSpreads": {},
"hardcodedCssMotionDurations": {
"src/app/globals.css": 42
},
@@ -75,7 +67,6 @@
"src/app/globals.css": 1,
"src/app/layout.tsx": 1,
"src/app/not-found.tsx": 1,
- "src/components/DocumentViewer.tsx": 1,
"src/components/applications-launcher-page.tsx": 1,
"src/components/calculators/bedside-sheet.tsx": 1,
"src/components/calculators/calculator-sheet.tsx": 1,
@@ -83,8 +74,8 @@
"src/components/calculators/guided-flow.tsx": 3,
"src/components/calculators/search-detail.tsx": 8,
"src/components/calculators/search-page.tsx": 2,
- "src/components/clinical-dashboard/ClinicalSidebar.tsx": 2,
"src/components/clinical-dashboard/auth-panel.tsx": 1,
+ "src/components/clinical-dashboard/ClinicalSidebar.tsx": 2,
"src/components/clinical-dashboard/dashboard-nav.tsx": 2,
"src/components/clinical-dashboard/differentials-home.tsx": 2,
"src/components/clinical-dashboard/document-search-results.tsx": 4,
@@ -95,13 +86,14 @@
"src/components/clinical-dashboard/medication-prescribing-workspace.tsx": 1,
"src/components/clinical-dashboard/medication-record-page.tsx": 2,
"src/components/clinical-dashboard/mode-action-popup.tsx": 2,
- "src/components/clinical-dashboard/settings-dialog.tsx": 3,
+ "src/components/clinical-dashboard/settings-dialog.tsx": 2,
"src/components/clinical-dashboard/source-preview-popover.tsx": 1,
"src/components/clinical-dashboard/universal-search-command-surface.tsx": 1,
"src/components/differentials/diagnosis-map-panel.tsx": 4,
"src/components/differentials/differential-detail-page.tsx": 2,
"src/components/differentials/differential-presentation-actions.tsx": 1,
"src/components/differentials/differential-presentation-workflow-page.tsx": 2,
+ "src/components/DocumentViewer.tsx": 1,
"src/components/dsm/dsm-comparison-page.tsx": 1,
"src/components/dsm/dsm-diagnosis-page.tsx": 1,
"src/components/dsm/dsm-differential-considerations-page.tsx": 2,
@@ -117,7 +109,7 @@
"src/components/specifiers/specifier-map-page.tsx": 1,
"src/components/specifiers/specifier-ui.tsx": 1,
"src/components/specifiers/specifiers-home-page.tsx": 1,
- "src/components/therapy-compass/screens/compare-screen.tsx": 2,
+ "src/components/therapy-compass/screens/compare-screen.tsx": 1,
"src/components/therapy-compass/screens/sheets-screen.tsx": 1,
"src/components/ui/answer-card.tsx": 1,
"src/components/ui/button.tsx": 1,
@@ -128,52 +120,44 @@
},
"arbitraryTracking": {},
"rawPaddingLiterals": {
- "src/app/globals.css": 23,
+ "src/app/globals.css": 18,
"src/components/clinical-dashboard/result-filter-control.tsx": 1,
"src/components/clinical-record-panels.tsx": 2,
"src/components/differentials/differential-detail-page.tsx": 1,
"src/components/therapy-compass/screens/brief-screen.tsx": 6,
- "src/components/therapy-compass/screens/compare-screen.tsx": 4,
- "src/components/therapy-compass/screens/detail-screen.tsx": 5,
+ "src/components/therapy-compass/screens/compare-screen.tsx": 3,
"src/components/therapy-compass/screens/pathways-screen.tsx": 4,
- "src/components/therapy-compass/screens/recommend-screen.tsx": 5,
"src/components/therapy-compass/screens/sheets-screen.tsx": 7
},
"rawRadiusLiterals": {
- "src/app/globals.css": 22,
+ "src/app/globals.css": 18,
"src/components/clinical-dashboard/search-results-header-band.tsx": 1,
"src/components/mode-nav/mode-nav.tsx": 1
},
"rawGapLiterals": {
- "src/app/globals.css": 14,
+ "src/app/globals.css": 11,
"src/components/therapy-compass/screens/brief-screen.tsx": 4,
"src/components/therapy-compass/screens/compare-screen.tsx": 1,
- "src/components/therapy-compass/screens/detail-screen.tsx": 3,
"src/components/therapy-compass/screens/pathways-screen.tsx": 2,
- "src/components/therapy-compass/screens/recommend-screen.tsx": 2,
"src/components/therapy-compass/screens/sheets-screen.tsx": 2
},
"rawMarginLiterals": {
- "src/app/globals.css": 8,
+ "src/app/globals.css": 6,
"src/components/clinical-dashboard/medication-prescribing-workspace.tsx": 1,
"src/components/document-viewer/document-rail-panels.tsx": 1,
"src/components/document-viewer/source-panels.tsx": 2,
"src/components/sidebar-live-shell.module.css": 2,
"src/components/therapy-compass/screens/brief-screen.tsx": 3,
- "src/components/therapy-compass/screens/compare-screen.tsx": 3,
- "src/components/therapy-compass/screens/detail-screen.tsx": 3,
+ "src/components/therapy-compass/screens/compare-screen.tsx": 2,
"src/components/therapy-compass/screens/other-screen.tsx": 1,
"src/components/therapy-compass/screens/pathways-screen.tsx": 1,
- "src/components/therapy-compass/screens/recommend-screen.tsx": 8,
"src/components/therapy-compass/screens/sheets-screen.tsx": 6,
"src/components/therapy-compass/workspace.tsx": 1,
"src/components/ui/retrieval-state-banner.tsx": 1,
- "src/components/ward-management/ward-management-modes.module.css": 16,
- "src/components/ward-management/ward-management.module.css": 19
- },
- "rawLineHeightLiterals": {
- "src/app/globals.css": 3
+ "src/components/ward-management/ward-management-modes.module.css": 15,
+ "src/components/ward-management/ward-management.module.css": 11
},
+ "rawLineHeightLiterals": {},
"layoutTransitionExceptions": {
"src/app/globals.css": 4,
"src/components/calculators/guided-flow.tsx": 1,
diff --git a/scripts/design-system-contract-utils.mjs b/scripts/design-system-contract-utils.mjs
index 3a96108f5c..9dda570432 100644
--- a/scripts/design-system-contract-utils.mjs
+++ b/scripts/design-system-contract-utils.mjs
@@ -50,6 +50,15 @@ export const RAW_COLOR_EXEMPTIONS = [
pattern: /^src\/components\/factsheets\/factsheet-detail-page\.tsx$/,
scope: "factsheet-print-sheet",
},
+ {
+ // Medication record accent fallback `#0f766e` is a stored-record default,
+ // not `--clinical-accent` (`#1d6fb8` / `--primary-500`). Mapping it would
+ // recolour live medication tiles. Scoped to the `accent:` default only —
+ // any other raw colour in these files stays countable.
+ category: "medication accent default",
+ pattern: /^src\/lib\/(?:medication-records|medications)\.ts$/,
+ scope: "medication-accent-default",
+ },
];
export function hasLegacyTapClass(classText) {
@@ -1835,6 +1844,19 @@ function namedFunctionRange(relativePath, source, functionName) {
return declaration ? { start: declaration.getFullStart(), end: declaration.end } : null;
}
+function medicationAccentDefaultRanges(source) {
+ // Only the `accent:` property default (`??` / `||` / direct) of `#0f766e`.
+ // `accentColor`, comments, and any other teal literal stay visible.
+ const pattern = /(? {}) {
const exemption = RAW_COLOR_EXEMPTIONS.find(({ pattern }) => pattern.test(relativePath));
if (!exemption) return source;
@@ -1860,6 +1882,15 @@ export function rawColorContractSource(relativePath, source, reportFailure = ()
return maskRanges(source, [range]);
}
+ if (exemption.scope === "medication-accent-default") {
+ const ranges = medicationAccentDefaultRanges(source);
+ if (ranges.length === 0) {
+ reportFailure("medication accent default boundary is missing");
+ return source;
+ }
+ return maskRanges(source, ranges);
+ }
+
reportFailure(`unknown raw-color exemption scope for ${relativePath}`);
return source;
}
diff --git a/src/app/ckb-v2-tokens.css b/src/app/ckb-v2-tokens.css
index d00e7aeb5c..4b4f83ca8a 100644
--- a/src/app/ckb-v2-tokens.css
+++ b/src/app/ckb-v2-tokens.css
@@ -62,7 +62,9 @@
--bp-tablet: 768px;
--bp-desktop: 1024px;
- /* Type - 7 steps, each with its own line-height and tracking.
+ /* Type - 7 size steps. xs–xl are size-only; shared leading is
+ * `--leading-prose`. Hero is the only step with dedicated companions
+ * (`--text-hero--line-height`, `--text-hero-tr`).
*
* TWO SCALES ARE LIVE AT ONCE, and this block is the one that wins. Because
* `ckb-v2` is mounted on , every step below overrides both Tailwind's
@@ -83,29 +85,14 @@
* --text-lg 1.25rem = Tailwind default --text-xl
* --text-xl 1.5rem = Tailwind default --text-2xl
*
- * The `-lh` / `-tr` companions below are declared but currently consumed by
- * nothing (0 references across src/ as of 2026-08-21). They are the intended
- * paired scale, not drift - but until a call site adopts them, only the size
- * half of this scale is real. Do not read their presence as evidence that
- * leading and tracking are already handled. */
+ * Do not reintroduce per-step `-lh` / `-tr` orphans for xs–xl. Wiring those
+ * would be type-scale adoption, not a companion-token restore. */
--text-xs: 0.75rem;
- --text-xs-lh: 1rem;
- --text-xs-tr: 0;
--text-sm: 0.8125rem;
- --text-sm-lh: 1.125rem;
- --text-sm-tr: 0;
--text-body: 0.9375rem;
- --text-body-lh: 1.375rem;
- --text-body-tr: -0.005em;
--text-md: 1.0625rem;
- --text-md-lh: 1.75rem;
- --text-md-tr: -0.008em;
--text-lg: 1.25rem;
- --text-lg-lh: 1.625rem;
- --text-lg-tr: -0.012em;
--text-xl: 1.5rem;
- --text-xl-lh: 1.875rem;
- --text-xl-tr: -0.018em;
--text-hero: clamp(1.75rem, 1.2rem + 2vw, 2.25rem);
--text-hero--line-height: 1.12;
--text-hero-tr: -0.022em;
diff --git a/src/app/globals.css b/src/app/globals.css
index 86cc194718..4be8efa107 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -4,8 +4,9 @@
the shared render-blocking stylesheet without changing mockup rendering. */
@source not "./mockups";
@source not "../components/**/*mockup*";
-/* Opt-in v2 design-system token layer. Every rule is scoped to `.ckb-v2`, so
- importing it cannot change any surface that does not carry the class. */
+/* Opt-in v2 design-system token layer. Every rule is scoped to `.ckb-v2`.
+ Production mounts that class on `` (`src/app/layout.tsx`), so the
+ layer is opted in at the document root rather than per subtree. */
@import "./ckb-v2-tokens.css";
@custom-variant dark (&:where(.dark, .dark *));
@@ -241,7 +242,7 @@
--leading-tight/-snug/-normal/-relaxed; these do NOT redefine those (that
would silently retune every existing leading-tight/leading-snug call site).
leading-display 1.05 large display headings — hero/mode-home/detail h1
- leading-prose 1.6 the max-w-[68ch] body measure */
+ leading-prose 1.65 the max-w-[68ch] body measure */
--leading-display: 1.05;
--leading-prose: 1.65;
@@ -1367,9 +1368,7 @@ summary::-webkit-details-marker {
font-size: 0.95rem;
font-weight: 900;
letter-spacing: -0.01em;
- box-shadow:
- 0 18px 42px rgb(0 0 0 / 24%),
- 0 0 0 1px color-mix(in srgb, var(--clinical-accent-contrast) 10%, transparent) inset;
+ box-shadow: 0 18px 42px rgb(0 0 0 / 24%);
touch-action: manipulation;
transition:
transform 160ms var(--ease-spring),
@@ -2178,7 +2177,8 @@ summary::-webkit-details-marker {
padding-inline: 0.35rem;
font-size: 16px;
font-weight: 560;
- line-height: 1.2;
+ /* Hero companion on this 16px field is deliberate coupling (DS-P2-12). */
+ line-height: var(--text-hero--line-height);
letter-spacing: 0;
}
@@ -2226,7 +2226,8 @@ summary::-webkit-details-marker {
opacity: 1;
background: var(--surface-subtle);
color: var(--disabled);
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border-strong) 42%, transparent);
+ border: 1px solid color-mix(in srgb, var(--border-strong) 42%, transparent);
+ box-shadow: none;
}
.answer-footer-search-chip {
@@ -2545,7 +2546,7 @@ summary::-webkit-details-marker {
.answer-suggestion-label-eyebrow {
font-size: 0.625rem;
letter-spacing: 0.06em;
- line-height: 1rem;
+ line-height: var(--leading-prose);
text-transform: uppercase;
}
@@ -2597,7 +2598,7 @@ summary::-webkit-details-marker {
color: var(--text-muted);
font-size: 0.75rem;
font-weight: 600;
- line-height: 1.25rem;
+ line-height: var(--leading-prose);
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
diff --git a/src/components/differentials/differential-detail-page.tsx b/src/components/differentials/differential-detail-page.tsx
index 14ce1b219d..ab4249c696 100644
--- a/src/components/differentials/differential-detail-page.tsx
+++ b/src/components/differentials/differential-detail-page.tsx
@@ -53,6 +53,7 @@ import {
type DifferentialSafetyFact,
} from "@/lib/differential-detail";
import type { DifferentialRecord, DifferentialSection } from "@/lib/differentials";
+import { resolveScrollBehavior } from "@/lib/scroll-behavior";
import { useAccountData } from "@/components/account-data-provider";
const sectionIcons: Record = {
@@ -1060,8 +1061,7 @@ export function DifferentialDetailPage({
? () => {
setSectionOpen("must-not-miss", true);
const target = document.getElementById("differential-section-must-not-miss");
- const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
- target?.scrollIntoView({ behavior: reduceMotion ? "auto" : "smooth", block: "start" });
+ target?.scrollIntoView({ behavior: resolveScrollBehavior(), block: "start" });
}
: null;
const openCompareTab = () => changeTab("compare");
diff --git a/src/components/specifiers/specifier-builder-page.tsx b/src/components/specifiers/specifier-builder-page.tsx
index 89670a17ef..b83d8d3cc1 100644
--- a/src/components/specifiers/specifier-builder-page.tsx
+++ b/src/components/specifiers/specifier-builder-page.tsx
@@ -96,7 +96,7 @@ function StepProgress({
return (
{builderSteps.map((step, index) => {
const isActive = active === step.id;
@@ -295,7 +295,7 @@ export function SpecifierBuilderPage({ initialSpecifiers = [] }: { initialSpecif
changeDiagnosis(event.target.value as SpecifierBuilderDiagnosis)}
- className="min-h-12 w-full max-w-full rounded-lg border border-[color:var(--border-strong)] bg-[color:var(--surface)] px-3 text-sm font-bold text-[color:var(--text-heading)] shadow-[var(--shadow-inset)] outline-none focus:border-[color:var(--focus)] focus:ring-4 focus:ring-[color:var(--focus)]/20"
+ className="min-h-12 w-full max-w-full rounded-lg border border-[color:var(--border-strong)] bg-[color:var(--surface)] px-3 text-sm font-bold text-[color:var(--text-heading)] outline-none focus:border-[color:var(--focus)] focus:ring-4 focus:ring-[color:var(--focus)]/20"
>
{diagnosisPresets.map((item) => (
@@ -565,7 +565,7 @@ export function SpecifierBuilderPage({ initialSpecifiers = [] }: { initialSpecif
) : null}
{activeView !== "review" ? (
-
+
move(-1)}
diff --git a/src/components/specifiers/specifier-ui.tsx b/src/components/specifiers/specifier-ui.tsx
index 5a4d3a3085..ddb59e6fb6 100644
--- a/src/components/specifiers/specifier-ui.tsx
+++ b/src/components/specifiers/specifier-ui.tsx
@@ -39,7 +39,7 @@ export function SpecifierWordingPathway() {
>
{specifierWordingPathwaySteps.map((label, index) => (
-
+
{index + 1}
@@ -70,7 +70,7 @@ export function SpecifierDiagnosisFilter({
@@ -146,7 +146,7 @@ export function SpecifierMatchCard({ record, isTopMatch }: { record: SpecifierRe
Deciding signal
diff --git a/src/components/therapy-compass/screens/brief-screen.tsx b/src/components/therapy-compass/screens/brief-screen.tsx
index 04d5e3cc0b..8f231bc184 100644
--- a/src/components/therapy-compass/screens/brief-screen.tsx
+++ b/src/components/therapy-compass/screens/brief-screen.tsx
@@ -10,11 +10,12 @@ import { Button } from "@/components/ui/button";
import { Tabs } from "@/components/ui/tabs";
import { BrowserPrintButton, PrintOutput } from "@/components/ui/print-output";
import { cn } from "@/components/ui-primitives";
-import { therapyRecordHref } from "@/lib/therapy-compass-navigation";
+import { therapyRecordHref, type TherapyBriefDuration } from "@/lib/therapy-compass-navigation";
import { useTcBindings } from "../bindings";
import { InteractiveRow } from "@/components/ui/interactive-row";
import { parseSteps, summarise } from "../data/select";
+import type { Therapy } from "../data/types";
import { LoadingState } from "../ui";
import { useClipboard } from "../use-clipboard";
import { TherapyCompareAction } from "../record/compare-action";
@@ -29,6 +30,20 @@ const CHECKLIST = [
"Confirm patient-facing language",
];
+const BRIEF_DURATION: Record string | null }> = {
+ "5min": { label: "5-minute", text: (therapy) => therapy.briefVersion },
+ "15min": {
+ label: "15-minute",
+ text: (therapy) => therapy.fifteenMinuteVersion || therapy.fullSessionVersion || therapy.briefVersion,
+ },
+ ground: {
+ label: "Grounding",
+ text: (therapy) =>
+ therapy.clinicianScripts.find((script) => /ground|relax|distress/i.test(`${script.scriptType} ${script.title}`))
+ ?.body || therapy.briefVersion,
+ },
+};
+
export function BriefScreen() {
const b = useTcBindings();
const t = b.selectedTherapy;
@@ -47,14 +62,9 @@ export function BriefScreen() {
const { notice, saved, toggleFavourite } = useTherapyFavourite(t?.slug ?? null);
if (b.loading || !t) return ;
- const durationLabel = b.briefTab === "15min" ? "15-minute" : b.briefTab === "ground" ? "Grounding" : "5-minute";
- const durationText =
- b.briefTab === "15min"
- ? t.fifteenMinuteVersion || t.fullSessionVersion || t.briefVersion
- : b.briefTab === "ground"
- ? t.clinicianScripts.find((c) => /ground|relax|distress/i.test(`${c.scriptType} ${c.title}`))?.body ||
- t.briefVersion
- : t.briefVersion;
+ const duration = BRIEF_DURATION[b.briefTab as TherapyBriefDuration] ?? BRIEF_DURATION["5min"];
+ const durationLabel = duration.label;
+ const durationText = duration.text(t);
const steps = parseSteps(durationText, 6);
const interventionText = [
`${t.name} — ${durationLabel} intervention`,
@@ -135,9 +145,8 @@ export function BriefScreen() {
diff --git a/src/components/therapy-compass/screens/compare-screen.tsx b/src/components/therapy-compass/screens/compare-screen.tsx
index a908b97f05..ad8296c0cd 100644
--- a/src/components/therapy-compass/screens/compare-screen.tsx
+++ b/src/components/therapy-compass/screens/compare-screen.tsx
@@ -233,7 +233,7 @@ export function CompareScreen() {
className="min-w-[160px] border-l border-[color:var(--border)] px-5 py-3.5 align-top"
>
-
+
{t.name}
-
+
{r.label}
@@ -285,7 +285,7 @@ export function CompareScreen() {
-
+
Comparisons are source-grounded. Review status reflects the latest source checks.
@@ -343,7 +343,7 @@ function TherapyCompareStack({
warn ? "text-[color:var(--warning-text)]" : "text-[color:var(--text-muted)]",
)}
>
-
+
{r.label}
diff --git a/src/components/therapy-compass/screens/recommend-screen.tsx b/src/components/therapy-compass/screens/recommend-screen.tsx
index 589704d596..164c00f18e 100644
--- a/src/components/therapy-compass/screens/recommend-screen.tsx
+++ b/src/components/therapy-compass/screens/recommend-screen.tsx
@@ -130,7 +130,7 @@ export function RecommendScreen() {
-
+
Ranking is source-grounded and advisory. Confirm fit, cautions and review status before clinical use.
>
diff --git a/src/components/therapy-compass/screens/sheets-screen.tsx b/src/components/therapy-compass/screens/sheets-screen.tsx
index 82de055b06..55f83a1509 100644
--- a/src/components/therapy-compass/screens/sheets-screen.tsx
+++ b/src/components/therapy-compass/screens/sheets-screen.tsx
@@ -329,16 +329,15 @@ function TherapyPicker() {
onClick={() => setOpen((v) => !v)}
>
-
+
{b.selectedTherapy?.name ?? "Choose a therapy"}
{open ? (
@@ -346,9 +345,8 @@ function TherapyPicker() {
{
}
});
- it("gives every type step its own line-height and tracking (#3, #14, #35, #36)", () => {
+ it("pins shared leading and hero companions, not per-step orphans (#3, #14, #35, #36)", () => {
for (const step of ["xs", "sm", "body", "md", "lg", "xl"]) {
expect(structural.has(`--text-${step}`), `--text-${step} missing`).toBe(true);
- expect(structural.has(`--text-${step}-lh`), `--text-${step}-lh missing`).toBe(true);
- expect(structural.has(`--text-${step}-tr`), `--text-${step}-tr missing`).toBe(true);
+ expect(structural.has(`--text-${step}-lh`), `--text-${step}-lh must stay deleted`).toBe(false);
+ expect(structural.has(`--text-${step}-tr`), `--text-${step}-tr must stay deleted`).toBe(false);
+ expect(structural.has(`--text-${step}--line-height`), `--text-${step}--line-height must stay deleted`).toBe(
+ false,
+ );
+ }
+ expect(structural.get("--leading-prose")).toBe("1.65");
+ expect(structural.get("--text-hero--line-height")).toBe("1.12");
+ expect(structural.get("--text-hero-tr")).toMatch(/^-/);
+
+ // The `^ {2}(--…)` map misses a declaration that is not indented with two
+ // spaces. Orphans must stay gone on the comment-stripped raw sheets too —
+ // including the live hero spelling `--text---line-height`.
+ const rawSheets = [stylesheet.replace(/\/\*[\s\S]*?\*\//g, ""), globalsStylesheet.replace(/\/\*[\s\S]*?\*\//g, "")];
+ for (const raw of rawSheets) {
+ for (const step of ["xs", "sm", "body", "md", "lg", "xl"]) {
+ expect(raw).not.toMatch(new RegExp(`--text-${step}-lh\\s*:`));
+ expect(raw).not.toMatch(new RegExp(`--text-${step}-tr\\s*:`));
+ expect(raw).not.toMatch(new RegExp(`--text-${step}--line-height\\s*:`));
+ }
}
- // Negative tracking only from 15px up — tightening small text hurts legibility.
- expect(structural.get("--text-xs-tr")).toBe("0");
- expect(structural.get("--text-sm-tr")).toBe("0");
- expect(structural.get("--text-body-tr")).toMatch(/^-/);
});
});
diff --git a/tests/design-system-adoption.test.ts b/tests/design-system-adoption.test.ts
index 3360c73407..4ac2c89e7b 100644
--- a/tests/design-system-adoption.test.ts
+++ b/tests/design-system-adoption.test.ts
@@ -1482,4 +1482,19 @@ describe("design-system adoption manifest", () => {
expect(gates).toMatch(/Render `AnswerCard` without[\s\S]*implemented-blocking in `AnswerCard`/);
expect(gates).toMatch(/Use a bare dash[\s\S]*implemented-partial — `AccessibleTable` composes `MissingValue`/);
});
+
+ it("quotes current design-system-contract-baseline totals in GATES.md", () => {
+ const { metrics } = JSON.parse(read("scripts/design-system-contract-baseline.json")) as {
+ metrics: Record;
+ };
+ const gates = read("docs/design-system/GATES.md");
+ expect(gates).toContain(`edgeOwnershipConflicts\` (${metrics.edgeOwnershipConflicts})`);
+ expect(gates).toContain(`legacyShadowAliases\`, ratcheted at ${metrics.legacyShadowAliases}`);
+ expect(gates).toContain(`rawPaddingLiterals\` (${metrics.rawPaddingLiterals})`);
+ expect(gates).toContain(`rawGapLiterals\` (${metrics.rawGapLiterals})`);
+ expect(gates).toContain(`rawMarginLiterals\` (${metrics.rawMarginLiterals})`);
+ expect(gates).toContain(`interactiveTapFloorDeclarations\` (${metrics.interactiveTapFloorDeclarations})`);
+ expect(gates).toContain(`hardcodedCssMotionDurations\` (${metrics.hardcodedCssMotionDurations})`);
+ expect(gates).toMatch(new RegExp(`raw colours ${metrics.rawColorLiterals}\\b`));
+ });
});
diff --git a/tests/design-system-contract-utils.test.ts b/tests/design-system-contract-utils.test.ts
index fbfec867a3..65a7f4391f 100644
--- a/tests/design-system-contract-utils.test.ts
+++ b/tests/design-system-contract-utils.test.ts
@@ -658,4 +658,50 @@ describe("design-system contract helpers", () => {
);
expect(reportFailure).toHaveBeenCalledWith("printable factsheet paper boundary is missing");
});
+
+ it("masks only the medication accent default, not other hex in the same file", () => {
+ const reportFailure = vi.fn();
+ const source = [
+ "export function rowToMedicationRecord(row) {",
+ ' return { accent: row.accent ?? "#0f766e", tag: row.tag };',
+ "}",
+ 'export const UNRELATED = "#123456";',
+ 'export const ALSO_TEAL = "#0f766e";',
+ ].join("\n");
+
+ const records = rawColorContractSource("src/lib/medication-records.ts", source, reportFailure);
+ expect(records).not.toMatch(/accent: row\.accent \?\? "#0f766e"/);
+ expect(records).toContain('export const ALSO_TEAL = "#0f766e"');
+ expect(records).toContain("#123456");
+ expect(reportFailure).not.toHaveBeenCalled();
+
+ const medications = rawColorContractSource(
+ "src/lib/medications.ts",
+ 'export function normalizeRecord(record) { return { accent: record.accent?.trim() || "#0f766e" }; }\nexport const OTHER = "#abcdef";',
+ reportFailure,
+ );
+ expect(medications).not.toContain("#0f766e");
+ expect(medications).toContain("#abcdef");
+ expect(reportFailure).not.toHaveBeenCalled();
+ });
+
+ it("does not treat accentColor or a comment mention as the medication accent default", () => {
+ const reportFailure = vi.fn();
+ const source = [
+ 'export const accentColor = "#0f766e";',
+ '// fallback accent: row.accent ?? "#0f766e"',
+ 'export const UNRELATED = "#123456";',
+ ].join("\n");
+
+ expect(rawColorContractSource("src/lib/medications.ts", source, reportFailure)).toBe(source);
+ expect(reportFailure).toHaveBeenCalledWith("medication accent default boundary is missing");
+ });
+
+ it("fails closed when the medication accent default boundary disappears", () => {
+ const reportFailure = vi.fn();
+ const source = 'export const FALLBACK = "#0f766e";';
+
+ expect(rawColorContractSource("src/lib/medication-records.ts", source, reportFailure)).toBe(source);
+ expect(reportFailure).toHaveBeenCalledWith("medication accent default boundary is missing");
+ });
});
diff --git a/tests/therapy-compass-responsive-contract.test.ts b/tests/therapy-compass-responsive-contract.test.ts
index 7d5d3fd3c3..bf53f29292 100644
--- a/tests/therapy-compass-responsive-contract.test.ts
+++ b/tests/therapy-compass-responsive-contract.test.ts
@@ -264,7 +264,10 @@ describe("Therapy Compass responsive contract", () => {
// the stronger condition: it proves the element really is that component
// AND carries the labelling attribute, and unlike a raw substring it
// cannot be satisfied by matching prose elsewhere in the file.
+ expect(briefSource).toContain("const BRIEF_DURATION");
+ expect(briefSource).not.toMatch(/briefTab === "15min" \? "15-minute"/);
expect(openingTagWith(briefSource, "Tabs", ['label="Brief intervention duration"'])).toBeTruthy();
+
expect(openingTagWith(compareSource, "Tabs", ['label="Comparison fields"'])).toBeTruthy();
expect(openingTagWith(compareSource, "SegmentedControl", ['label="Comparison density"'])).toBeTruthy();
expect(openingTagWith(sheetsSource, "SegmentedControl", ['label="Reading level and tone"'])).toBeTruthy();
@@ -324,6 +327,20 @@ describe("clinical accent contrast contract", () => {
expect(briefSource).not.toContain('? "#fff" : "var(--clinical-accent)"');
});
+ it("maps sweep-touched Lucide size={16} onto size-icon-md", () => {
+ for (const [name, source] of [
+ ["brief", briefSource],
+ ["compare", compareSource],
+ ["recommend", recommendSource],
+ ["sheets", sheetsSource],
+ ] as const) {
+ expect(source, `${name} screen`).not.toMatch(/\bsize=\{16\}/);
+ }
+ expect(briefSource).toContain("size-icon-md");
+ expect(compareSource).toContain("size-icon-md");
+ expect(sheetsSource).toContain("size-icon-md");
+ });
+
it("keeps the current dark accent/foreground token pair above text contrast", () => {
const darkStart = globalsSource.indexOf(".dark {");
const darkEnd = globalsSource.indexOf("\n}", darkStart);