diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md
index ef236e67a3..60cee37d9e 100644
--- a/docs/branch-review-ledger.md
+++ b/docs/branch-review-ledger.md
@@ -880,3 +880,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie
| 2026-08-10 | cursor/smarter-meds-search-9c1b (PR #1785) | 5cb0e11e077a3aaf5b8e4ea37b26ac72b0328997 | PR #1785 unblock/fix | before: Production UI (3) failed on service-detail scroll endpoint (remaining 67px) at 38b3bd0c; GitHub DIRTY behind-but-clean vs #1791. after: merged origin/main + re-scroll toPass fix in ui-tools service-detail test; threads untouched; do not merge | CI Production UI (3) logs; git merge-tree clean; prettier ui-tools; product fix in same tip commit as this row |
| 2026-08-10 | PR #1800 / codex/enhance-search-function-with-fuzzy-matching | 93da84b063c9c3f956da7ef79710d2cd00159735 | PR #1800 babysit | Synced origin/main (merge-tree clean; GitHub DIRTY was staleness). Fixed CodeRabbit SSRI/SNRI fuzzy cross-match (floor 5 chars) in follow-on tip commit. Clinical Governance Preflight required for clinicalRisk body. Codex P2 field-aware/per-token fuzzy deferred. RAG surfaces untouched. | focused catalog-search+consumers 49 pass; pr-policy body local ok; merge-tree clean |
| 2026-08-10 | PR #1803 / claude/codex-m4b-shadow-tight-migration-53a8kn | b778a56e9c3fa7642a783dde85e1130559d71e24 | shadow-tight token migration onto the e1 elevation tier and alias retirement (#262 part 1) | Migrated all 150 var(--shadow-tight) occurrences across 71 files to var(--e1) (90 gated production sites across 48 files, 60 mockup); deleted all three alias declarations (:root, .dark, forced-colors); pinned legacyShadowAliases 220 to 127 with exact per-path counts, closing 3 aliases of re-accumulated slack; added a whole-stylesheet absence assertion (mutation-verified); updated GATES.md section 3 plus a new section 6, TOKENS.md section 6, design-system.md, both redesign direction docs, .design-sync/conventions.md and ledger #262. Verified in Chromium that the ckb-v2 tier override is picked up by the alias substitution, so the change is value-preserving; that check is recorded as a prerequisite for the remaining six aliases. | npm run verify:cheap (30 static gates plus lint plus typecheck green; design-system contract passed, legacy shadow aliases 127; unit suite 553/554 files, 6024 tests passed, 1 pre-existing root-permission failure in tests/pr-handoff-stop.test.ts reproduced on untouched base a16dd26); npm run format:check whole tree; targeted Chromium computed-style measurement. verify:ui not run, Playwright browser revision drift #255, delegated to CI Production UI. No provider-backed gates. |
+| 2026-08-11 | claude/codex-m4c-retire-shadow-nliak3 | 448a0d084c4cd2cda6153dd7f03dcb67c43a8df0 | DS Track A2 (#261): retire --shadow-focus; composer focus onto sanctioned outline; contract guard; baseline ratchet; design-system docs + ledger | Approved — PR #1807. Token deleted in both themes; .chat-composer-shell-delta:focus-within uses outline 2px var(--focus) at offset 2px and no longer overrides box-shadow. Reach premise corrected: 0 of 37 production routes render the class (only /mockups/calculators-search). legacyShadowAliases 127->125, globals.css pin 3->1. | check:design-system-contract PASS; design-token-contract.test.ts PASS + mutation-verified both ways; verify:pr-local PASS except pre-existing tests/pr-handoff-stop.test.ts failure baselined on untouched base e8b61d8; build PASS; check:rag:fixtures PASS (36 cases); Chromium look both themes on the mockup route (inspection only, rev 1194 vs pinned 1234 #255); verify:ui/verify:phone-chrome NOT run — delegated to CI |
diff --git a/docs/design-system.md b/docs/design-system.md
index 498d3977a1..1a715728e2 100644
--- a/docs/design-system.md
+++ b/docs/design-system.md
@@ -153,7 +153,8 @@ Icon **glyphs** use the parallel `--spacing-icon-*` scale in `@theme`:
- The surviving role names are **aliases onto tiers**, not independent values:
`--shadow-card` / `--shadow-soft` → `--e2`; `--shadow-hover` → `--e3`; `--shadow-elevated` /
`--shadow-lux` → `--e4`. `--shadow-tight` is retired — the resting hairline is `--e1` at the
- call site. `--shadow-inset`, `--shadow-rail-active`, `--shadow-focus` and
+ call site. `--shadow-focus` is retired too — focus is an `outline: 2px solid var(--focus)`,
+ never a companion shadow ring. `--shadow-inset`, `--shadow-rail-active` and
`--glow-primary/soft` stay bespoke. All are removed under forced-colors, ladder included.
- No literal `box-shadow` values in components — reach for a tier
(`shadow-[var(--e2)]`, `hover:shadow-[var(--e3)]`) or a role alias.
diff --git a/docs/design-system/GATES.md b/docs/design-system/GATES.md
index 8db669ac5f..0bd0191588 100644
--- a/docs/design-system/GATES.md
+++ b/docs/design-system/GATES.md
@@ -74,7 +74,7 @@ because it contributed nothing.
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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 (token + declared carriers today; fixed-height `h-10` controls and full interactive enumeration not yet blocked); static pills never carry `min-h-tap`; no production target reduced | **implemented-partial** | 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. |
-| 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. |
+| 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. 11 Aug 2026 (`#261`): the one companion ring this rule knew about is gone — `--shadow-focus` is deleted and `.chat-composer-shell-delta:focus-within` uses the outline. That removes the known violation, not the need for the check; nothing yet stops the next one. |
| 4 | Non-colour encoding on every status indicator | **implemented-partial** | Blocked today by `ui-v2-answer-safety.dom.test.tsx`: an overdue `DoseLine` row is asserted to carry all three channels (amber inset rule **plus** the words "Source review overdue" **plus** a `StatusMark` shape), `MissingValue` is asserted never to contract to a dash at any density, `FieldError` is asserted to pair its text with an icon, and `RetrievalStateBanner` is asserted to carry its state in the headline text rather than the tone alone. Off-vocabulary status still degrades to a phrase (`source-badges-off-vocab.dom.test.tsx`, Gate 6). **Not blocked today:** there is no repository-wide enumeration of status indicators, so a _new_ colour-only indicator elsewhere in `src/components/**` — the bare `statusDot*` recipes are the obvious candidates — would not fail anything. **Closed 9 Aug 2026 — the repository-wide enumeration now ships.** `colourOnlyStatusIndicators` (`check:design-system-contract`) flags a status hue on a box that says nothing: no children, no `aria-label`/`aria-labelledby`/`title` on it or any ancestor, no text sibling, and not a `StatusMark`. It also flags shared _swatch recipes_ — a status hue plus a tiny round box and no text utility — because the analyzer is per-file and cannot follow an imported `statusDotReady` to its call sites, so the recipe is where the defect is catchable. Ratcheted at **4** with per-path pins: the two bare `statusDot*` recipes GATES.md named, plus a calculator risk band and a therapy meter fill. A _new_ colour-only indicator anywhere in `src/**` now fails. Still partial: those 4 recorded sites, and `--decoration-soft` is deliberately out of scope (it carries no state). |
| 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`. |
@@ -273,11 +273,14 @@ with 17 paths dropped. The pre-existing baseline also carried 3 aliases of stale
the other six roles (measured 217 against a 220 ceiling); per-path counts are now exact, so
that headroom is closed too. This is the same slack `#264` found on 9 August, re-accumulated.
-**Zeroing this metric is still not the success criterion.** 127 aliases remain across
-`--shadow-soft` (71), `--shadow-elevated` (17), `--shadow-hover` (17), `--shadow-card` (12),
-`--shadow-lux` (8) and `--shadow-lift` (2) — `#262`'s remaining tranches. Count the token by
-reading the `var()` call, not the declaration it sits in: two of the 71 `--shadow-soft` hits
-are the **value** of the `--shadow-focus` declarations in each theme, which is how an earlier
-pass mis-read them as a `--shadow-focus` tally. `LEGACY_SHADOW_ALIAS` has matched exactly
+**Zeroing this metric is still not the success criterion.** 125 aliases remain across
+`--shadow-soft` (69), `--shadow-elevated` (17), `--shadow-hover` (17), `--shadow-card` (12),
+`--shadow-lux` (8) and `--shadow-lift` (2) — `#262`'s remaining tranches; measured 11 Aug 2026
+by running the analysers over the gate's own walk, and equal to the gate's printed total.
+Count the token by reading the `var()` call, not the declaration it sits in. That rule is what
+moved this number without any call site changing: `--shadow-soft` was 71 until `#261` deleted
+the two `--shadow-focus` declarations, whose **value** ended in `var(--shadow-soft)` and so
+scored as two `soft` aliases — which is also how an earlier pass mis-read them as a
+`--shadow-focus` tally. `LEGACY_SHADOW_ALIAS` has matched exactly
`tight|card|soft|hover|elevated|lux|lift` since PR #1616 and has never included `focus`, so
-`#261` shares no counter with this row.
+`#261` shared no counter with this row; it moved the number only through that indirection.
diff --git a/docs/design-system/HANDOVER-2026-08-07.md b/docs/design-system/HANDOVER-2026-08-07.md
index 910afb132e..55d8ebf73a 100644
--- a/docs/design-system/HANDOVER-2026-08-07.md
+++ b/docs/design-system/HANDOVER-2026-08-07.md
@@ -22,6 +22,17 @@
> conclusion: the ubuntu CI job already produces the only ones that count. They are still
> uncommitted — `#118` remains open, and `tests/__screenshots__/` holds only `README.md`
> until reviewed Linux PNGs are adopted.
+> - **A2's "visible focus-state change on the search composer"** overstates the reach.
+> Correction #2 below is right that `--shadow-focus` has a live consumer and that a
+> `--include=*.tsx` grep misses it — but the consumer class reaches **no production
+> route**. `.chat-composer-shell-delta` comes from `chatComposerShell`, imported only by
+> `calculators/search-detail.tsx` and its mockup twin; the production `/calculators` page
+> renders `chatComposerShellBase` + `answer-footer-search-pill` instead, and
+> `CalculatorSearchHome` is reached only from two unrouted mockup exports. Measured
+> 11 Aug 2026 by probing all **37** static production routes in Chromium: **zero**
+> render the class; the single live render is `/mockups/calculators-search`. So A2 was a
+> correctness fix to production CSS with no production pixel moving today — do the
+> Chromium look on the mockup route, which is where it is visible. Retired in `#261`.
> - **`#270`'s "22 call sites pair a tap token with a dead numeric height"** does not
> survive re-measurement at all: **zero** same-variant pairs exist, and the 84 survivors
> are live cross-variant responsive step-downs rather than dead classes. The stated
diff --git a/docs/design-system/SPEC.md b/docs/design-system/SPEC.md
index 2eab8ec04c..9f27f5f766 100644
--- a/docs/design-system/SPEC.md
+++ b/docs/design-system/SPEC.md
@@ -263,8 +263,8 @@ The ladder itself is gated against baked-in hairlines (**[verified:** contract t
`--shadow-inset` **stays the DS bevel**; `--shadow-well` is the recessed-well role. The
former v2 `--shadow-inset` overrides became `--shadow-well` in `59e4c3dfc`, with the
-contract test's pin updated in the same commit (C1, done). Alias cleanup: `--shadow-focus` is deleted (it encodes a
-companion focus ring the conventions forbid), `--shadow-lift` retires into the ladder, the
+contract test's pin updated in the same commit (C1, done). Alias cleanup: `--shadow-focus` is deleted (done
+11 Aug 2026 — it encoded a companion focus ring the conventions forbid), `--shadow-lift` retires into the ladder, the
three dead springs go — **retire aliases inside the recipes first**, or "never use an alias"
is unfollowable.
@@ -796,7 +796,7 @@ close this runtime concern; revisit before adoption puts them on a hot path.
| PR | Contents | Status |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
-| PR 9 · Motion, stacking, edges | Wire `--duration-*`/`--ease-*`/`--z-*` to utilities + lint; `transform` for `Progress`, `LinkAction`, `ToggleSwitch`; edge-rule gate; `Quantity` off the retiring type step; delete `--shadow-focus`, `--shadow-lift`, dead springs | open |
+| PR 9 · Motion, stacking, edges | Wire `--duration-*`/`--ease-*`/`--z-*` to utilities + lint; `transform` for `Progress`, `LinkAction`, `ToggleSwitch`; edge-rule gate; `Quantity` off the retiring type step; delete `--shadow-focus`, `--shadow-lift`, dead springs | open — `--shadow-focus` deleted 11 Aug 2026 (`#261`); the rest of the row is untouched |
| PR 10 · Overlays | One `OverlayRoot`; mandatory `Sheet` name; portal by default; `Tooltip` composes child handlers; `Toast` splits tone/priority/persistence, pauses on hover and focus | **done** — component/publication contract and app-root mount; v2 style activation is unchanged |
| PR 11 · Print and documents | Print as a tokenised theme; `[data-print-hide]`; print primitives; `DocumentFrame` | open — COMPONENTS §6 |
| PR 12 · Design-sync integrity | Declarations generated from real types; manifest parity; direct tests for every registered component; preview state matrices; `tailwind-merge` or slot props; split `ui-primitives.tsx` | **publication slice done** — deterministic props, parity, previews and direct contract proof; override policy and module split remain deferred |
diff --git a/docs/design-system/TOKENS.md b/docs/design-system/TOKENS.md
index b81b325c86..e85e29c4bc 100644
--- a/docs/design-system/TOKENS.md
+++ b/docs/design-system/TOKENS.md
@@ -109,16 +109,16 @@ The v2 layer _references_ or _depends on_ these; their values stay in `live` / `
## 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-tight` | **Done (10 Aug 2026).** Its 90 production call sites reach for `--e1` directly and all three declarations are deleted; the alias resolved to exactly `var(--e1)` in every scope, so nothing rendered differently. GATES §6. | `design-token-contract.test.ts` asserts the token is absent from the whole stylesheet; `legacyShadowAliases` 220 → 127. |
-| `--shadow-lift` | Retire into the `--eN` ladder (PR 9). | 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. |
-| `--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. |
+| 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` | **Done (11 Aug 2026).** Both theme declarations are deleted and its one consumer, `.chat-composer-shell-delta:focus-within`, now uses the sanctioned `outline: 2px solid var(--focus)` at `outline-offset: 2px`. Resting `box-shadow` is untouched on focus, so the pill no longer re-seats. GATES §3. | `design-token-contract.test.ts` rejects both a `--shadow-focus:` declaration and a `var(--shadow-focus)` consumer, in `globals.css` and the v2 layer; `legacyShadowAliases` 127 → 125. |
+| `--shadow-tight` | **Done (10 Aug 2026).** Its 90 production call sites reach for `--e1` directly and all three declarations are deleted; the alias resolved to exactly `var(--e1)` in every scope, so nothing rendered differently. GATES §6. | `design-token-contract.test.ts` asserts the token is absent from the whole stylesheet; `legacyShadowAliases` 220 → 127. |
+| `--shadow-lift` | Retire into the `--eN` ladder (PR 9). | 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. |
+| `--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. |
## 7 · Usage rules — allowed and forbidden, per group
diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md
index 0db81cdce6..2f69e4aa5f 100644
--- a/docs/outstanding-issues.md
+++ b/docs/outstanding-issues.md
@@ -295,7 +295,6 @@ removed after current-main verification; it is not missing recommended work.
| #257 | P3 | issue | Single unreproduced ui-formulation flake: keeps specifier and formulation route families clinically separate | Observed once on 2026-08-06 at PR #1647 head f5833acc, running tests/ui-formulation.spec.ts + tests/ui-specifiers.spec.ts together against local Chromium (1 failed, 11 passed). Did NOT reproduce: passed in isolation with --grep, and passed again on a full-file re-run (7/7). Recorded only so a second sighting is recognisable as a second rather than looking like a first. Per docs/testing.md this is one reproduction of three — do NOT quarantine, and do not weaken the assertion. Next: no action unless it recurs; if a second reproduction lands on the same SHA, note it here, and only on a third open a tests/flake-ledger.json entry with @quarantine and a <=30-day expiry. | session 2026-08-06; PR #1647 | 2026-08-06 |
| #258 | P2 | rec | The PR-handoff stop rule is enforced for Claude Code only; Codex and Cursor get prose with no gate | **Outcome:** a session that opens a PR stops following it in every agent this repo supports, not just Claude Code. **Detail:** PR #1649 added `.claude/hooks/pr-handoff-stop.sh` plus the AGENTS.md "Stop when the pull request is open" section. The hook is registered in `.claude/settings.json`, which only Claude Code reads, so the PostToolUse marker and the PreToolUse denials (shell `gh pr checks/status/view/run watch`, GitHub MCP tools named pull_request/workflow_run/workflow_job/check_run/check_suite/job_log/update_branch, and Monitor/ScheduleWakeup/CronCreate) simply do not exist for Codex or Cursor sessions. Those agents get the AGENTS.md prose and nothing else — and prose alone is exactly what was already in force, and already insufficient, before #1649. Cost is the same long tail of post-handoff CI polling the hook was built to cut, just relocated to whichever agent lacks the gate; a cloud Codex session is the worst case because nothing naturally ends it. **Next:** cheapest first — check whether Codex and Cursor expose any pre-tool interception this repo can register (Codex plugin hooks under `plugins/clinical-kb/`, Cursor rules under `.cursor/`); if neither offers a deny path, the fallback is a shared marker file plus a wrapper the agent is told to route `gh` through, which is weaker but still detectable. If no mechanism exists at all, record that explicitly here so the gap is a known limit rather than an open task. **Stop:** do not weaken the Claude Code hook to make the tools symmetric, and do not add a second copy of the deny list — one script, multiple registrations. | PR #1649; .claude/hooks/pr-handoff-stop.sh; .claude/settings.json; AGENTS.md "Stop when the pull request is open"; session 2026-08-07 | 2026-08-07 |
| #260 | P2 | task | Two unpushed Sentry commits are stranded on a Windows-only branch and will be lost with that machine | **Outcome:** the Sentry setup/logging-hardening work is either shipped or consciously discarded, not left sitting in one machine's reflog. **Detail:** `claude/cloud-pr-loop-prevention-bc052b` carries two commits — `c3c9d6a31` and `abbcdc8e9`, ~389 lines across `src/sentry.*.config.ts`, `src/lib/env.ts`, `src/lib/supabase/client.tsx`, `src/components/ui-primitives.tsx` — that were never pushed and are not the authoring session's own work. The branch does not exist on the remote, so the commits are unreachable from any cloud or remote container; a 2026-08-07 remote session could not inspect, verify, or ship them and could only record their existence. The same worktree (`.claude/worktrees/pensive-borg-6be2f0`) still holds the same four files uncommitted. Two Sentry branches DO exist on origin — `claude/sentry-nextjs-sdk-setup-2v24q5` and `cursor/sentry-nextjs-sdk-7cee` — but whether either already carries this change is unconfirmed: a three-dot diff against `origin/main` from the remote container returned empty for both, which is not trustworthy as proof either way and was not pursued further. Note this touches `src/lib/env.ts` and `src/lib/supabase/client.tsx`, so it is not a docs-class change and needs a real gate whenever it does ship. **Next:** from the Windows machine, diff those two commits against the two remote Sentry branches to decide whether the work is already represented. If it is, delete the branch; if it is not, push it and open a PR rather than leaving it local. **Stop:** do not discard the commits blind, and do not assume the remote Sentry branches supersede them without a content diff — nothing has yet compared them. | session 2026-08-07 remote container; handoff notes from the PR #1649 session; origin branches claude/sentry-nextjs-sdk-setup-2v24q5 and cursor/sentry-nextjs-sdk-7cee | 2026-08-07 |
-| #261 | P2 | task | DS Track A2: retire --shadow-focus from the search composer | Replace the composer's companion focus ring with the sanctioned outline / --focus treatment used everywhere else, then delete the token (both theme declarations). Live consumer is .chat-composer-shell-delta:focus-within in globals.css — a --include=*.tsx grep reports zero consumers and is wrong. This is a visible focus-state change on the search composer: read docs/search-chrome-behaviour.md first and get a Chromium look. Gate: npm run check:design-system-contract + npm run verify:phone-chrome. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 |
| #262 | P2 | task | DS Track A3: finish the design-token debt | Three parts. (1) DONE 2026-08-10 - --shadow-tight is retired outright: 90 gated production sites across 48 files (plus 60 mockup occurrences, migrated in the same pass so no file names a dead token) now reach for var(--e1), and all three declarations - both themes and the forced-colors flattening - are deleted. The alias resolved to exactly var(--e1) in every scope and the forced-colors block already flattened --e1 alongside the roles, so the substitution was value-preserving in light, dark and forced-colors and needed no visual review. Do NOT take that from the declarations alone for the remaining tranches: ckb-v2-tokens.css redeclares --e1 (light 13 40 71 / 5% vs globals 11 42 56 / 7%) and never redeclares the roles, and a custom property containing var() substitutes on the element it is DECLARED on - an alias declared in an outer scope and overridden in a narrower one freezes at the outer value. This migration is safe only because .ckb-v2 is on (layout.tsx) and .ckb-v2.ckb-v2 outspecifies :root, so the alias substitutes against the winning v2 tier; measured in Chromium, both spellings compute to rgba(13, 40, 71, 0.05) 0px 1px 2px 0px. Re-run that check per alias, it is about where a declaration sits. legacyShadowAliases 220 -> 127 with per-path counts pinned to measured, which also closed 3 aliases of re-accumulated stale slack across the other six roles (measured 217 against a 220 ceiling - the same drift #264 found on 9 Aug). design-token-contract.test.ts now asserts the token is absent from the whole stylesheet, mutation-verified. Remaining 127: soft 71, elevated 17, hover 17, card 12, lux 8, lift 2 - and count a token by reading the var() call, not the declaration it sits in, because two of the soft hits are the VALUE of the --shadow-focus declarations. Parts (2) and (3) below are untouched; (3) landed separately in PR #1780 per #301. ORIGINAL SCOPE NOTE, kept for the remaining tranches: SCOPE RE-MEASURED 2026-08-08 against origin/main 2675e6e1d, running analyzeClassContractsInSource + analyzeCssContractsInSource over the same walk check-design-system-contract.mjs uses (src/**, .ts/.tsx/.css, mockups excluded). The inherited figures were wrong in three ways. First, the legacyShadowAliases metric counts SEVEN tokens, not one: measured total 228 = tight 100, soft 72, elevated 17, hover 17, card 12, lux 8, lift 2. So the '229 --shadow-tight aliases' in HANDOVER-2026-08-07 is the all-token total mislabelled, and this row's earlier '155 consumers' was closer to a raw repo-wide grep (160 occurrences including mockups) than to the gated number. Second, the real scope is 100 production --shadow-tight sites across 55 files, so the inherited figure overstates the work by roughly 1.55x, and clearing all 100 will NOT zero the ratchet: 128 aliases across the six other tokens remain, so do not treat legacyShadowAliases=0 as the success criterion. Third, --shadow-focus is NOT in this metric at all: LEGACY_SHADOW_ALIAS has matched exactly tight\|card\|soft\|hover\|elevated\|lux\|lift since PR #1616 and has never included focus, so an earlier note claiming 'eight tokens, focus 2' and an overlap with #261 was wrong. #261 is a separate token with one consumer (src/app/globals.css:1476) and two theme declarations (lines 423, 664); the two tasks do not share this metric. Baseline pins legacyShadowAliases at 231 and the baseline is a ceiling, so today's 228 already passes. Re-measure before starting rather than trusting any of these numbers. (2) Add a step-SELECTION lint for the eight non-standard type steps (1318 sites) — check:type-scale already blocks arbitrary text-[12px], so do NOT write a lint duplicating the half that ships. (3) Extend the contract ratchet to raw padding / radius / line-height literals; it covers colour, shadow, tap and tracking today. Gate: npm run check:design-system-contract. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 |
| #265 | P2 | task | DS Track A6: move design-system gates 2, 4, 7 and 8 from partial to blocking | RE-MEASURED AND PART-CLOSED 2026-08-09 against origin/main 8db1e53937. GATE 4 CLOSED: colourOnlyStatusIndicators in check:design-system-contract is the repository-wide enumeration this row asked for - a status hue on a box with no children, no aria-label/aria-labelledby/title on it or any ancestor, no sibling text, and not a StatusMark. It also flags shared swatch recipes, because the analyzer is per-file and cannot follow an imported statusDotReady to its call sites. Ratcheted at 4 with per-path pins (the two bare statusDot recipes GATES.md named, a calculator risk band, a therapy meter fill); a new colour-only indicator anywhere in src now fails. Mutation-verified. GATE 2 NOT CLOSED, and this row's description of it was wrong in a way that cost a session. It is NOT true that test:e2e:style-contract needs wiring into verify:cheap: the npm script is only an alias for running that one spec, the spec matches productionSpecPattern in playwright.config.ts and is listed in scripts/playwright-pr-shards.mjs, so it ALREADY runs in the required Production UI job. It must NOT be added to verify:cheap:internal, because check:gate-manifest then demands a matching step in static-pr, which has no browser and no server. The real gap is the h-10 blind spot inside the audit itself, and an enumeration for it was written, shown to find genuine defects, and then reverted rather than landed because it is not deterministic on a live-search route - see #293 for the six-run evidence and the follow-up. REMAINING: gate 2's enumeration (needs a deterministic surface first, #293), gate 7 (elevation child/parent, needs a render-tree check, untouched), and gate 8's recorded debt only - its two checks already ship and ratchet per path, so that work is retiring 27 edge conflicts across 15 files and 2 globals.css spreads, then pinning both at zero. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 |
| #266 | P2 | task | DS Track B1: adopt the 23 unadopted components demand-driven, never as a race to 53/53 | Pick a surface and let it pull, the way PR #1658 did for AnswerCard. COUNT RE-MEASURED 2026-08-08 from docs/design-system/adoption-manifest.json on origin/main: 53 registered, 30 with at least one productImportFiles entry, 23 UNADOPTED — not 24. Button moved into the adopted set when AccessibleTable's expand control stopped being a hand-rolled recipe (#263, PR #1712); its sole production importer is src/components/AccessibleTable.tsx, which is the demand-driven route this row describes, so it is the pattern to copy rather than an exception. The 23 measured today: AnswerFooter, Checkbox, Citation, CitationList, ConfirmDialog, Disclosure, DisclosureGroup, DoseLine, DownloadLink, ErrorSummary, ExternalTextLink, FieldError, FieldHint, LinkAction, Pagination, Progress, RadioGroup, SearchField, StageList, Tabs, TextLink, ToastRegion, Tooltip. Forms are still the largest single tranche: FieldError, FieldHint, ErrorSummary, SearchField, Checkbox and RadioGroup all land together on one form conversion. Do not stub a component to move the adoption count. Regenerate with npm run design-system:adoption:update after any import change; ALSO run npm run design-system:design-sync:update, because changing any *Props type or adopting a component fails check:design-sync-contract with 'dtsPropsFor must be generated from source public Props types' if only the first is run. Both manifests are generated, never hand-edited. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 |
@@ -337,6 +336,7 @@ Move resolved rows here with the resolution date and a one-line outcome. Keep th
| ID | Type | Summary | Outcome | Resolved |
| ---- | ----- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
+| #261 | task | DS Track A2: retire --shadow-focus from the search composer | RESOLVED 2026-08-11. Both theme declarations deleted; the one consumer, `.chat-composer-shell-delta:focus-within`, now uses the sanctioned `outline: 2px solid var(--focus)` at `outline-offset: 2px` and no longer overrides `box-shadow`, so the pill keeps its resting elevation while focused instead of re-seating (the retired token carried `--shadow-soft` as its second layer). Measured in Chromium both themes: light `solid 2px rgb(29, 111, 184)`, dark `solid 2px rgb(116, 189, 240)`, box-shadow identical resting vs focused in both. CORRECTION to this row's own premise, which was inherited from HANDOVER-2026-08-07 A2: it is NOT a visible production focus change. The consumer class reaches no production route - `chatComposerShell` is imported only by calculators/search-detail.tsx and its mockup twin, production /calculators renders `chatComposerShellBase` + `answer-footer-search-pill` instead, and `CalculatorSearchHome` is reached only from two unrouted mockup exports. Probed all 37 static production routes in Chromium: zero render the class; the single live render is /mockups/calculators-search, which is where the Chromium look was taken. The row was right that a `--include=*.tsx` grep misses the consumer - it is in CSS - but wrong about its reach. Guard: design-token-contract.test.ts rejects both a `--shadow-focus:` declaration and a `var(--shadow-focus)` consumer, in globals.css and the v2 layer, mutation-verified both ways; it is deliberately not the whole-file substring check used for --shadow-tight, because the composer rule names the retired token in a comment on purpose. legacyShadowAliases 127 -> 125 (soft 71 -> 69) with the globals.css per-path pin tightened 3 -> 1: the deleted declarations' VALUE ended in `var(--shadow-soft)`, so they scored as two `soft` aliases - the indirection GATES documents. NOT ratcheted, and still open as pre-existing slack unrelated to this diff: rawPaddingLiterals 67 -> 65 and rawGapLiterals 34 -> 32 (therapy-compass/therapy-card.tsx), layoutTransitionExceptions 12 -> 11 (secondary-navigation.tsx). Gate: npm run check:design-system-contract passed. verify:phone-chrome NOT run - this container's Chromium is rev 1194 against the repo's pinned 1234 (#255 drift), and no production phone chrome renders the class; browser proof delegated to CI. | 2026-08-11 |
| #173 | issue | Facet counts and format counts are computed against different sets, so half the filter panel goes stale | RESOLVED 2026-07-31 in PR #1526. `projectSmartTagFacetGroups` recounts an already-built facet index against the live selection so each row answers how many documents remain if that facet is also ticked; zero-count facets stay visible (not removed) and the facet rail disables unselected zeros so they cannot advertise a dead end. Originally captured as open `#172` on this branch before `main` claimed `#169` for the local-branches finding; renumbered to `#173` on merge. | 2026-07-31 |
| #160 | task | Reland closed PR #1515 (#093 + #138 fixes never reached main) | RESOLVED 2026-07-31: capture recorded while #1515 was closed unmerged; #1515 then landed on `main` as squash `ca2c4de51faae9a0502b0b0570b6866acbb943fe`, which also archived `#093`/`#138`. **Content-verified on `origin/main` (not SHA/PR state alone):** `tests/playwright-settlement.ts` exports `visibleByTestId` (`.filter({ visible: true })`) and it is used from `tests/ui-tools.spec.ts`, `tests/ui-smoke.spec.ts`, and `tests/ui-accessibility.spec.ts`; `.github/workflows/ci-triage.yml` enables by default with `vars.CI_TRIAGE_ENABLED != 'false'`. Reland no longer needed; chat archive unblocked. | 2026-07-31 |
| #093 | issue | Next streaming `S:` clone causes Playwright strict-mode violations under CI load | RESOLVED 2026-07-31: shared `visibleByTestId` scopes page-root/shell testids to the visible DOM owner (not bare `.first()`), applied to the known hotspots in `ui-tools` / `ui-smoke` / `ui-accessibility`. `expectSingleSettledOwner` remains for full-convergence races. Product mount bisect remains optional if a new surface appears. | 2026-07-31 |
diff --git a/scripts/design-system-contract-baseline.json b/scripts/design-system-contract-baseline.json
index 174204b7b9..b6e6158a63 100644
--- a/scripts/design-system-contract-baseline.json
+++ b/scripts/design-system-contract-baseline.json
@@ -11,7 +11,7 @@
"rawCssZIndices": 9,
"legacyPaletteUtilities": 0,
"darkColorOverrides": 0,
- "legacyShadowAliases": 127,
+ "legacyShadowAliases": 125,
"arbitraryTracking": 0,
"rawPaddingLiterals": 67,
"rawRadiusLiterals": 24,
@@ -64,7 +64,7 @@
"legacyPaletteUtilities": {},
"darkColorOverrides": {},
"legacyShadowAliases": {
- "src/app/globals.css": 3,
+ "src/app/globals.css": 1,
"src/app/layout.tsx": 1,
"src/app/not-found.tsx": 1,
"src/components/applications-launcher-page.tsx": 3,
diff --git a/src/app/globals.css b/src/app/globals.css
index c5f31a6369..dd4fc78a34 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -419,7 +419,6 @@
--shadow-elevated: var(--e4);
--shadow-lux: inset 0 1px 0 rgb(255 255 255 / 90%), var(--e4);
--shadow-inset: inset 0 1px 0 rgb(255 255 255 / 58%);
- --shadow-focus: 0 0 0 3px color-mix(in srgb, var(--focus) 25%, transparent), var(--shadow-soft);
/* Active left-rail marker (nav/list "current" indicator). One token so the
accent rail is a consistent 2px everywhere instead of hand-inlined at 2px/3px. */
--shadow-rail-active: inset 2px 0 0 var(--clinical-accent);
@@ -659,7 +658,6 @@
--shadow-elevated: var(--e4);
--shadow-lux: var(--e4);
--shadow-inset: inset 0 1px 0 rgb(255 255 255 / 4%);
- --shadow-focus: 0 0 0 3px color-mix(in srgb, var(--focus) 25%, transparent), var(--shadow-soft);
/* A drop shadow does not read on a near-black ground, so the lifted control
is described by an inner top highlight instead. */
--shadow-lift: inset 0 1px 0 rgb(255 255 255 / 7%), 0 2px 6px rgb(0 0 0 / 45%);
@@ -1469,9 +1467,17 @@ summary::-webkit-details-marker {
box-shadow: var(--shadow-soft);
}
+/* One focus owner, same as every other control. The retired `--shadow-focus`
+ token painted a 3px accent halo ON TOP of the accent border swap below — the
+ companion ring the shared `:focus-visible` treatment near the top of this file
+ exists to prevent (`#261`). The resting `--shadow-soft` elevation is
+ deliberately not overridden here: `--shadow-focus` carried it as its second
+ layer, so leaving box-shadow alone keeps the pill seated at exactly the same
+ height focused as at rest, and the outline is the only thing that changes. */
.chat-composer-shell-delta:focus-within {
border-color: var(--clinical-accent);
- box-shadow: var(--shadow-focus);
+ outline: 2px solid var(--focus);
+ outline-offset: 2px;
}
.chat-composer-input {
diff --git a/tests/design-token-contract.test.ts b/tests/design-token-contract.test.ts
index 1b32ae6977..d89b298b09 100644
--- a/tests/design-token-contract.test.ts
+++ b/tests/design-token-contract.test.ts
@@ -169,6 +169,33 @@ describe("elevation ladder", () => {
expect(v2Stylesheet, "--shadow-tight is retired; call sites use --e1").not.toContain("--shadow-tight");
});
+ // `--shadow-focus` is retired (`#261`). It was not an elevation alias at all:
+ // it packed a 3px accent halo in FRONT of `--shadow-soft`, so its one consumer
+ // — `.chat-composer-shell-delta:focus-within` — painted a companion ring on
+ // top of the accent border swap, which is the second focus affordance the
+ // shared `:focus-visible` treatment is written to prevent. The composer now
+ // uses the sanctioned `outline: 2px solid var(--focus)`.
+ //
+ // Unlike the `--shadow-tight` assertion above this is not a raw substring
+ // check: the stylesheet comment at the composer rule names the retired token
+ // on purpose, so that the next author reaching for a focus halo finds the
+ // reason it is gone rather than re-deriving it. The two spellings below are
+ // the only ways the token can actually come back to life — a declaration and
+ // a `var()` consumer — so they are what the gate rejects.
+ it("keeps the retired --shadow-focus token deleted in every scope", () => {
+ for (const [name, stylesheet] of [
+ ["globals.css", globals],
+ ["ckb-v2-tokens.css", v2Stylesheet],
+ ] as const) {
+ expect(stylesheet, `${name} redeclares --shadow-focus; focus is an outline, not a ring`).not.toContain(
+ "--shadow-focus:",
+ );
+ expect(stylesheet, `${name} consumes --shadow-focus; focus is an outline, not a ring`).not.toContain(
+ "var(--shadow-focus)",
+ );
+ }
+ });
+
it("flattens the ladder itself under forced colors, not only the role aliases", () => {
const forced = globals.slice(globals.indexOf("@media (forced-colors: active)"));
for (const tier of ["--e1", "--e2", "--e3", "--e4"]) {