diff --git a/.design-sync/conventions.md b/.design-sync/conventions.md index 6105591cf2..aacdffc1b4 100644 --- a/.design-sync/conventions.md +++ b/.design-sync/conventions.md @@ -31,10 +31,9 @@ arbitrary-value form — never hardcoded colours: `--info-solid`. For a filled non-danger status use `-bg` + `-text`. - Elevation: the `--e0` … `--e4` ladder — `shadow-[var(--e2)]`, `hover:shadow-[var(--e3)]`. `--e0` flush · `--e1` resting hairline · `--e2` cards/popovers · `--e3` hover/lifted chrome · - `--e4` modals/sheets/drawers. The surviving role names are aliases onto tiers: - `--shadow-card`/`--shadow-soft`→`--e2`, `--shadow-hover`→`--e3`, - `--shadow-elevated`/`--shadow-lux`→`--e4`. `--shadow-tight` is retired — reach for `--e1`. - `--shadow-inset` stays bespoke. + `--e4` modals/sheets/drawers. The role names are aliases onto tiers: + `--shadow-tight`→`--e1`, `--shadow-card`/`--shadow-soft`→`--e2`, `--shadow-hover`→`--e3`, + `--shadow-elevated`/`--shadow-lux`→`--e4`. `--shadow-inset` stays bespoke. Never hand-roll a `shadow-[0_…]` literal. - Focus ring: `focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]`. Outline only — never add a companion `focus:ring-*` / `box-shadow`. The shared base rule is one diff --git a/AGENTS.md b/AGENTS.md index 483df4b4c6..88ec727f80 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,9 +2,7 @@ # This is NOT the Next.js you know -This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. - -This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. diff --git a/Dockerfile b/Dockerfile index 623e2d3422..72c491885f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ # NEVER baked into the image — inject them at run time from the host's # secret store. -FROM node:26-bookworm-slim@sha256:cd565714d4da3e84bfd341e31448f81d47c6362198f152345297c9c1154e6341 AS node-base +FROM node:24-bookworm-slim@sha256:235600a8101ab264e117b1768e925532262668dc9b581ef1dd7d96ced463b8e7 AS node-base FROM node-base AS deps WORKDIR /app @@ -28,10 +28,11 @@ WORKDIR /app COPY package.json package-lock.json .npmrc ./ COPY scripts/check-node-engine.cjs scripts/check-node-engine.cjs COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs +COPY scripts/check-installed-lock-parity.mjs scripts/check-installed-lock-parity.mjs # Registry blips (ECONNRESET) have failed CI app-image builds mid-install; retry # the whole `npm ci` rather than relying only on per-request fetch retries. RUN for attempt in 1 2 3; do \ - NPM_CONFIG_ENGINE_STRICT=false npm ci --ignore-scripts --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ + npm ci --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ if [ "$attempt" -eq 3 ]; then exit 1; fi; \ sleep $((attempt * 10)); \ done @@ -67,8 +68,9 @@ WORKDIR /app COPY package.json package-lock.json .npmrc ./ COPY scripts/check-node-engine.cjs scripts/check-node-engine.cjs COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs +COPY scripts/check-installed-lock-parity.mjs scripts/check-installed-lock-parity.mjs RUN for attempt in 1 2 3; do \ - NPM_CONFIG_ENGINE_STRICT=false npm ci --omit=dev --ignore-scripts --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ + npm ci --omit=dev --ignore-scripts --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ if [ "$attempt" -eq 3 ]; then exit 1; fi; \ sleep $((attempt * 10)); \ done diff --git a/Dockerfile.worker b/Dockerfile.worker index 0352e999be..4e5163c762 100644 --- a/Dockerfile.worker +++ b/Dockerfile.worker @@ -19,16 +19,17 @@ # `server-only` marker to the standalone stub at build time (the job # run-tsx.mjs previously did at runtime) and keeps npm packages external, # so the bundle resolves them from the runner's production node_modules. -FROM node:26-bookworm-slim@sha256:cd565714d4da3e84bfd341e31448f81d47c6362198f152345297c9c1154e6341 AS node-base +FROM node:24-bookworm-slim@sha256:235600a8101ab264e117b1768e925532262668dc9b581ef1dd7d96ced463b8e7 AS node-base FROM node-base AS build WORKDIR /app COPY package.json package-lock.json .npmrc ./ COPY scripts/check-node-engine.cjs scripts/check-node-engine.cjs COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs +COPY scripts/check-installed-lock-parity.mjs scripts/check-installed-lock-parity.mjs # Same install-retry contract as the app Dockerfile (registry ECONNRESET flakes). RUN for attempt in 1 2 3; do \ - NPM_CONFIG_ENGINE_STRICT=false npm ci --ignore-scripts --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ + npm ci --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ if [ "$attempt" -eq 3 ]; then exit 1; fi; \ sleep $((attempt * 10)); \ done @@ -42,8 +43,9 @@ WORKDIR /app COPY package.json package-lock.json .npmrc ./ COPY scripts/check-node-engine.cjs scripts/check-node-engine.cjs COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs +COPY scripts/check-installed-lock-parity.mjs scripts/check-installed-lock-parity.mjs RUN for attempt in 1 2 3; do \ - NPM_CONFIG_ENGINE_STRICT=false npm ci --omit=dev --ignore-scripts --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ + npm ci --omit=dev --ignore-scripts --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ if [ "$attempt" -eq 3 ]; then exit 1; fi; \ sleep $((attempt * 10)); \ done diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 7c0fdec906..b1c6f2b5ce 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -880,9 +880,13 @@ 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 | codex/answer-loading-ui-20260811 | 6758f8156f9d1b3e893981dfd7a1f6563aa90da0 | answer creation loading UI | No high-confidence findings | UI 3 passed; unit 8 passed; lint, typecheck, build, design-system and offline RAG passed; full suite 6022 passed with 16 unchanged baseline failures | | 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 | +| 2026-08-11 | claude/spacing-icon-design-review-rxwh28 | f8701a524f0eb22decc64ce1f626bdafe91751af | mode-home hero spacing rhythm + icon scale (PR #1815) | shipped: group copy reserve banded to measured wrap points, continuous hero medallion, phone composer glyph 1.1rem->icon-lg, surface glyphs onto size-icon-*, privacy link bottom-only negative margin (fixes 8px tap overhang on the APP-5 sentence), composer phone reserve 7.625->6.625rem | test 6043 passed/1 pre-existing root-perm failure; lint; typecheck; build; check:icon-scale; check:type-scale; check:design-system-contract; check:rag:fixtures; check:bundle-budget; prettier --check .; verify:ui NOT run (Playwright r1234 vs image r1194, #255 - delegate to CI Production UI) | +| 2026-08-11 | codex/answer-loading-ui-20260811 | 6758f8156f9d1b3e893981dfd7a1f6563aa90da0 | answer creation loading UI | No high-confidence findings | UI 3 passed; unit 8 passed; lint, typecheck, build, design-system and offline RAG passed; full suite 6022 passed with 16 unchanged baseline failures | +| 2026-08-11 | 1815 | be7461ef1f66357999995acefbeecaf95268e481 | unblock | local-build-pass | MergeTreeClean,UnitCoverage,StaticPRChecks,ContainerImages | | 2026-08-11 | codex/answer-ecg-animation-20260811 | 12279a8309c225aa957ef1e65afc37545a0ce04c | answer ECG progress variants | No high-confidence findings; physical Safari/PWA remains residual acceptance | design contract, typecheck, focused unit 8/8, trace token 33/33, Chromium 4/4, offline RAG 574/574; full suite baseline/platform failures | +| 2026-08-11 | claude/spacing-icon-design-review-rxwh28 | 455bc198c077860fb1f830670a5fa9c1de08da52 | pr-1815 heavy review-and-fix | remote already merged main (shadow-tight Switch kept); cherry-picked privacy -mb-4 reclaim + calculators dock cancel; removed duplicate UniversalSearchAlsoMatches; rail-aware section-sheet focus restore; dispositioned CodeRabbit docs/ledger/gates nits and outdated Sentry skeleton gap | verify:cheap PASS prior tip; verify:pr-local PASS prior tip; vitest privacy+in-page-nav 28 passed on cherry-pick; merge-tree clean vs origin/main | +| 2026-08-11 | claude/spacing-icon-design-review-rxwh28 | 5b96281ee7da817d5ce7f1102004ebe6f861b920 | pr-1815 heavy review-and-fix | remote already merged main (shadow-tight Switch kept); cherry-picked privacy -mb-4 reclaim + calculators dock cancel; removed duplicate UniversalSearchAlsoMatches; rail-aware section-sheet focus restore; dispositioned CodeRabbit docs/ledger/gates nits and outdated Sentry skeleton gap | verify:cheap PASS prior tip; verify:pr-local PASS prior tip; vitest privacy+in-page-nav 28 passed on cherry-pick; merge-tree clean vs origin/main | | 2026-08-11 | work | 6dcd695076d630d16aae594577763e8004361893 | Codex Cloud setup and local parity | P2 fixed: cache-friendly locked Cloud npm install; parity limitations documented | check:codex-cloud; codex-cloud-setup 24/24; full suite 6059 pass, 7 unrelated timeout/state failures | | 2026-08-11 | 1822 | 4fab267f52b72992745e1d2e6975fb4847af447a | review-and-fix | clean | Build pass; Static PR checks pass; Change scope pass; PR mergeability pass; PR policy pass; Safety and config checks pass; Semgrep pass; Semgrep ingestion gate pass; Gitleaks pass; GitGuardian pass; Unit coverage pending; Production UI (1) pass; Production UI (2) pass; Production UI critical pending; Production UI (3) pending; Lighthouse budget pass; PR required pending | | 2026-08-11 | 1822 | 4fab267f52b72992745e1d2e6975fb4847af447a | review-and-fix (supersedes 2026-08-11) | clean | Build pass; Static PR checks pass; Change scope pass; PR mergeability pass; PR policy pass; Safety and config checks pass; Semgrep pass; Semgrep ingestion gate pass; Gitleaks pass; GitGuardian pass; Unit coverage pass; Production UI (1) pass; Production UI (2) pass; Production UI (3) pass; Production UI critical pass; Lighthouse budget pass; PR required pass | @@ -890,3 +894,5 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-11 | 1821 | d76e90547dbdb104256b66a508c15c74302002fc | review-and-fix | dispositioned | PR policy:success; PR mergeability:success; Gitleaks:success; Semgrep:success; Semgrep ingestion gate:success; Safety and config checks:success; Build:success; Production UI critical:success; Production UI (1):success; Production UI (2):success; Production UI (3):failure test not reproduced outside this PR; PR required:failure | | 2026-08-11 | work | 45fd05c8c3947835c0368666ff576c7a38b33ee4 | mobile evidence sheet UX, accessibility, and feedback logic | fixed unexplained claim marker, excess panel reserve, unclear purpose and feedback copy; no remaining high-confidence defects | focused DOM 7/7; Chromium evidence journey 1/1; offline RAG 23 suites/574 tests | | 2026-08-11 | 1820 | 897ff11a4cdb13ae1c01f5eb149007847028f5aa | review-and-fix | fixed | Semgrep:IN_PROGRESS, Gitleaks:IN_PROGRESS, Semgrep ingestion gate:IN_PROGRESS, Static PR checks:QUEUED, Safety and config checks:QUEUED, Unit coverage:QUEUED, Build:QUEUED, Production UI critical:QUEUED, Lighthouse budget:QUEUED | +| 2026-08-12 | PR #1815 / claude/spacing-icon-design-review-rxwh28 | 9f266210f02081be54d407c70a85f52fed436128 | babysit | no remaining actionable findings; one pre-existing thread resolved as no-change (Dockerfile.worker follow-up needed) | required checks: Gitleaks PR policy PR required (all pass); targeted vitest passed: tests/document-frame-contract.test.ts + tests/in-page-nav-header.dom.test.tsx | +| 2026-08-12 | 1815 | 27ce96e1755055ceee2eeae02d6efdf11259fcde | babysit | fixed | Unit coverage: targeted vitest passed: tests/shared-home-empty-state.dom.test.tsx (17 passed). PR required still blocked on pre-existing check failure at old remote head before sync. | diff --git a/docs/design-system.md b/docs/design-system.md index 1a715728e2..5d5489c7fe 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -150,11 +150,9 @@ Icon **glyphs** use the parallel `--spacing-icon-*` scale in `@theme`: instead of bleeding. `--e0` flush · `--e1` resting hairline · `--e2` cards/popovers · `--e3` hover/lifted chrome · `--e4` modals/sheets/drawers. Dark lifts with a top highlight rather than more black. -- The surviving role names are **aliases onto tiers**, not independent values: +- The role names are **aliases onto tiers**, not independent values: `--shadow-tight` → `--e1`; `--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-focus` is retired too — focus is an `outline: 2px solid var(--focus)`, - never a companion shadow ring. `--shadow-inset`, `--shadow-rail-active` and + `--shadow-lux` → `--e4`. `--shadow-inset`, `--shadow-rail-active`, `--shadow-focus` 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. @@ -222,7 +220,7 @@ image"}` — never a possibly-empty variable alone. | `Number(query.page ?? 1)` | `parseInt` + `Number.isFinite` + `>= 1` clamp | | `alt={caption}` | `alt={caption?.trim() \|\| "Clinical document image"}` | | new `z-[73]` for a popover | an existing ladder rung, or `Sheet` | -| `shadow-[0_5px_12px_rgba(0,122,120,0.16)]` | `shadow-[var(--e1)]` | +| `shadow-[0_5px_12px_rgba(0,122,120,0.16)]` | `shadow-[var(--shadow-tight)]` | ## 9. Verification gates — Definition of Done for UI PRs diff --git a/docs/design-system/GATES.md b/docs/design-system/GATES.md index 527887d8d9..e485e7190b 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. 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. | +| 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-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`. | @@ -113,7 +113,7 @@ theme-list parity, and remote design-project publication remain separate concern | 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` (27) + `onePixelShadowSpreads` (2) | **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-card`/any surviving alias in new code | `check:design-system-contract` — `legacyShadowAliases`, ratcheted at 127 with per-path pins | **implemented-blocking for new use** — a new alias in any file fails; `--shadow-tight` is retired outright (90 sites onto `--e1`, 10 Aug 2026, §6), and retiring the remaining 127 across the other six roles is `#262` | +| Use `--shadow-tight`/any alias in new code | `check:design-system-contract` — `legacyShadowAliases`, ratcheted at 224 with per-path pins | **implemented-blocking for new use** — a new alias in any file fails; retiring the existing 224 is `#262` | | Raw pixel size, padding, radius, gap or line-height in markup | `check:design-system-contract` — `rawPaddingLiterals` (67), `rawRadiusLiterals` (24), `rawGapLiterals` (34), `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 | | Animate `width`, `height`, `grid-template-*`, `top`, `left`, `gap` | `check:design-system-contract` — `layoutTransitionExceptions`, ratcheted at 12 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 12 | | 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 | @@ -234,53 +234,3 @@ again. Since `ui-style-contract.spec.ts` runs in the required `Production UI` jo an intermittent version would have blocked every merge in the repo, which is worse than the gap it closes. Recorded here so the next attempt starts from a deterministic surface rather than re-deriving the same six runs. - -## 6 · `--shadow-tight` retired — 10 August 2026 - -`#262` part 1. Measured against `origin/main` `a16dd26`, walking `src/**` with the same -`analyzeClassContractsInSource` / `analyzeCssContractsInSource` pass -`check-design-system-contract.mjs` uses. - -**The alias was never an independent value.** `--shadow-tight` resolved to exactly -`var(--e1)` in both theme declarations, and the forced-colors block already flattened `--e1` -alongside the roles — so substituting the tier for the alias is value-preserving and needs no -visual review of its own. 90 gated production sites across 48 files moved to `var(--e1)`; the -60 design-scratch mockup occurrences moved in the same pass so that no file names a token -that no longer exists. - -**Do not accept that argument on the declarations alone — the v2 layer redeclares the tier.** -`ckb-v2-tokens.css` sets `--e1: 0 1px 2px rgb(13 40 71 / 5%)` in light against globals' -`0 1px 2px rgb(11 42 56 / 7%)`, and it never redeclares the role. A custom property whose -value contains `var()` substitutes **on the element it is declared on**, so an alias declared -in one scope and overridden in a narrower one freezes at the outer value and the two spellings -diverge. What saves this migration is that both selectors match the same element: `.ckb-v2` is -on `` (`layout.tsx`), `.ckb-v2.ckb-v2` outspecifies `:root`, and the alias therefore -substitutes against the winning v2 tier. Measured in Chromium rather than argued — a page -carrying exactly that cascade computes `var(--shadow-tight)` and `var(--e1)` both to -`rgba(13, 40, 71, 0.05) 0px 1px 2px 0px`, the v2 value. Light is the only theme where the two -layers disagree at all; the dark tiers are byte-identical and forced-colors is `none` on both -sides. **The same check is owed to each remaining alias in `#262`** — the reasoning is about -where a declaration sits, not about this token. - -**The declarations are gone**, both themes and the forced-colors flattening, and -`design-token-contract.test.ts` now asserts the absence over the whole stylesheet rather than -per theme block — any scope that redeclares it makes the alias spellable again. -Mutation-verified: restoring `--shadow-tight: var(--e1);` to `:root` fails with -`--shadow-tight is retired; call sites use --e1`. - -**The ratchet is pinned to measured, not merely lowered:** `legacyShadowAliases` 220 → **127** -with 17 paths dropped. The pre-existing baseline also carried 3 aliases of stale slack across -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.** 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` 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 55d8ebf73a..910afb132e 100644 --- a/docs/design-system/HANDOVER-2026-08-07.md +++ b/docs/design-system/HANDOVER-2026-08-07.md @@ -22,17 +22,6 @@ > 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 9f27f5f766..2eab8ec04c 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 (done -11 Aug 2026 — it encoded 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 (it encodes 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 — `--shadow-focus` deleted 11 Aug 2026 (`#261`); the rest of the row is untouched | +| 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 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 e85e29c4bc..ed9b4ab8cb 100644 --- a/docs/design-system/TOKENS.md +++ b/docs/design-system/TOKENS.md @@ -109,16 +109,15 @@ 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` | **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. | +| 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-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/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json index 0cb3e648ac..77b72dc6d8 100644 --- a/docs/design-system/adoption-manifest.json +++ b/docs/design-system/adoption-manifest.json @@ -253,9 +253,7 @@ "testFiles": [ "tests/design-sync-visual-exports.test.ts", "tests/in-page-nav-playwright-contract.test.ts", - "tests/information-page-shell.dom.test.tsx", - "tests/ui-route-coverage.spec.ts", - "tests/ui-specifiers.spec.ts" + "tests/information-page-shell.dom.test.tsx" ], "baseline": { "targetLayer": "v2", @@ -1522,7 +1520,6 @@ "tests/factsheet-save.dom.test.tsx", "tests/header-scroll-hide-contract.test.ts", "tests/image-lightbox-geometry.test.ts", - "tests/in-page-nav-route-sections.dom.test.tsx", "tests/mode-nav.dom.test.tsx", "tests/rag-answer-fallback.test.ts", "tests/settings-dialog-actions.dom.test.tsx", diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 2f69e4aa5f..4cadf4ea42 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -295,7 +295,8 @@ 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 | -| #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 | +| #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) Move --shadow-tight onto the --eN elevation ladder. 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 | | #267 | P3 | task | DS Track B2: AnswerFooter and DoseLine need a provenance/dose payload the answer surface does not produce | Backend-shaped work, not a component swap: the two components cannot be adopted until the answer surface emits the provenance and dose data they render. Do not stub one to make the adoption count look better. Sequence after the payload exists, then adopt via the Track B1 demand-driven route. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | @@ -311,6 +312,7 @@ removed after current-main verification; it is not missing recommended work. | #281 | P2 | rec | The phone document route renders two clinical-summary surfaces and neither is canonical | **Outcome:** one clinical summary on the document route, chosen deliberately. **Detail:** a phone reader gets the gradient 'High-yield clinical summary' card (DocumentClinicalSummary, built by buildDocumentClinicalSummaryModel) and, further down, the rail's '#source-summary' / 'high-yield-summary' disclosure (DocumentSectionSummary + FormattedHighYieldSummary + BadgeCluster). They render the same document.summary row two different ways. The rail is not hidden on phones — only its DocumentSectionIndexCard is lg:block — so both appear. Only the rail panel carries the section anchor, so the more prominent card is the unnavigable one. Note the two disagree about emptiness as well: the card now renders nothing when the model yields no usable text, while the rail panel still renders for its label badges, which is why 'hasStoredSummary' was deliberately left keyed to the stored row rather than to card content. **Next:** decide which rendering is canonical — this is a clinical-content judgement about how a summary should read, not a layout fix — then delete the other and give the survivor the 'source-summary' anchor. If the rail's badges are the part worth keeping, they can move without the second summary body. **Stop:** do not merge the two renderings mechanically; they format clinical text differently and the difference is the decision. | session 2026-08-08 document-viewer optimisation; document-rail-panels.tsx; document-clinical-summary.tsx | 2026-08-08 | | #282 | P3 | task | Probe the corpus for JBIG2/JPX before deciding whether pdf.js needs its decoder assets shipped | **Outcome:** a measured decision about pdf.js's cMap/standard-font/WASM assets rather than an assumption either way. **Detail:** getDocument is configured with url plus the on-demand fetch flags and nothing else, so 'wasmUrl', 'standardFontDataUrl', 'cMapUrl' and 'iccUrl' are all unset. pdfjs-dist ships those assets (wasm 1.5 MB, standard_fonts 804 KB, cmaps 1.7 MB) and nothing copies them into public/. With wasmUrl null, 'useWorkerFetch' resolves false and the WASM image decoders cannot load, so JBIG2 and JPEG2000 images fall back to the JS decoders or fail; those are exactly the encodings a scanned guideline uses, and this repo runs an OCR pipeline, which implies scanned sources exist. Non-embedded standard-14 fonts fall back to system fonts, which is a fidelity risk on a clinical document rather than a failure. **Next:** sample the real corpus for JBIG2/JPX-encoded images and for PDFs relying on the standard 14 before shipping ~2 MB of static assets; if the corpus does use them, copy into public/pdfjs, set the URLs, and add immutable cache headers in next.config.ts (public/ is not counted by check:bundle-budget, so there is no budget risk — the cost is bytes over the wire on first use). **Stop:** do not ship the assets on the assumption alone. | session 2026-08-08 document-viewer optimisation; node_modules/pdfjs-dist/types/src/display/api.d.ts | 2026-08-08 | | #283 | P3 | rec | The 100-id batch signed-URL route still has no caller | **Outcome:** either the batch minter is used or it is retired, rather than sitting as an untested, unreachable privileged surface. **Detail:** src/app/api/images/signed-urls/route.ts POSTs up to 100 image ids and returns their signed URLs, with its own rate limit, owner scoping and committed-generation filter. Nothing in src/ calls it — only tests/private-access-routes.test.ts imports it. **DEFERRED AGAIN, DELIBERATELY, 2026-08-09 (document viewer Phase 3, Task 3).** The user chose deferral over wiring when asked. Two reasons beyond cost: (a) wiring it puts a privileged owner-scoped API route into a diff that is otherwise confined to src/components/document-viewer/**, and it matches clinicalRiskPatterns (/^src\/app\/api\//) so pr-policy hard-blocks the merge without a complete Clinical Governance Preflight; (b) Phase 3 Task 2 windowed the rail to six rows and tightened its IntersectionObserver root margin from 640px to 240px, so the many-distinct-images case the batch route was meant to serve is now materially smaller — a page of N figures no longer mounts N rows at once. The batching win should be re-measured against the windowed rail before it is wired at all, rather than assumed from the pre-window numbers. **Next:** decide deliberately — measure concurrent distinct-image requests on a figure-heavy document with the windowed rail, then either wire the batch route in its own PR or delete it and its tests. **Stop:** if wiring it, keep the per-image endpoint for the lightbox's retry path; do not make the batch the only way to mint a URL. | session 2026-08-08 document-viewer optimisation; src/app/api/images/signed-urls/route.ts | 2026-08-08 | +| #284 | P3 | issue | tests/pr-handoff-stop.test.ts fails whenever the suite runs as root | **Outcome:** 'npm run test' is green in a root container, so a real failure is not hidden behind a known one. **Detail:** 'pr-handoff-stop hook > emits handoff context only when the marker file exists' expects markerExists('sess-readonly') to be false — it makes the marker directory read-only and asserts the hook could not write there. Root ignores the permission bits, so the write succeeds and the assertion fails. Reproduced on an unmodified bc33d41 checkout as well as on the viewer-optimisation branch, so it is environment-dependent, not a regression. Cost is that every full-suite run in a root container reports '1 failed', which trains readers to skim past the failure count. **Next:** skip the case when 'process.getuid?.() === 0' with an explicit reason, or drop privileges for that assertion. **Stop:** do not delete the coverage — the read-only case is the point of the test on a normal user account. | session 2026-08-08 full-suite runs; reproduced on bc33d41 | 2026-08-08 | | #286 | P2 | task | PR 2 of the in-page nav series - convert the six pill-rail information pages onto InPageNavHeader | IMPLEMENTED in PR #1766 (branch claude/inpage-nav-pr-2-6d32f9); close this row when that PR merges. All six routes (seven components: services, forms, specifiers record + catalogue reference, formulation, /dsm/diagnoses/ and its /differentials child) mount InPageNavHeader, drop their breadcrumb row, keep their in-body h1, and move record actions into the ellipsis sheet. Three things the conversion needed first, none of which were in the original sketch: the actions render prop had to widen to ReactNode \| ((close) => ReactNode) because four of the seven are Server Components, and onSelectSection plus PageSection.icon have the same RSC-boundary problem, so those four mount the header through a 'use client' sibling module; both sheets derive open state from the pathname so navigation closes them; and information-page sections had no scroll-mt at all, so a shared inPageAnchor token consuming --inpage-anchor-offset was added, published by useInPageChromeMetrics from the live chrome height. The pill rail is deleted with it: hasLocalInformationPageNavigation collapses to isInformationPage and the section kind leaves secondary-navigation.tsx. Guard shipped: tests/in-page-nav-route-sections.dom.test.tsx asserts every declared section id against rendered DOM for all seven components. | session 2026-08-08; PR #1740 (2806d5e) | 2026-08-08 | | #287 | P2 | task | PR 3 of the in-page nav series - the three locally-owned routes each need a decision, not just a conversion | **Outcome:** every information page uses the documented in-page navigation template, or has a recorded reason not to. **Detail:** the last three routes each own a different bespoke pattern, and none is a mechanical port. (1) /medications/[slug] - SectionTabs at medication-record-page.tsx:183 SWAPS CONTENT rather than scrolling: sectionsByTab[activeTab] at :391 filters record.sections by type, so a different set mounts per tab. The InPageNavHeader track is scroll-spy over anchors that all exist at once, so adopting it means either driving tab state from the track (the track stops meaning where am I on the page) or flattening to one scrolling page - a real behaviour change to a clinical record, and a product call. (2) /differentials/presentations/[slug] - MobileTabs at differential-presentation-workflow-page.tsx plus the xl review sidebar; the old `differentialPresentationSections` shell set is gone and the route is locally owned (`page-secondary-navigation.tsx`). Remaining work is the product decision to adopt `InPageNavHeader` (or keep the tab/sidebar model with a recorded reason), not resurrecting deleted section targetIds. (3) /factsheets/[slug] - the On this page list at factsheet-detail-page.tsx:365-371 is li text with no link, button or handler, and the sections themselves carry no ids at all (:214, :267, :313, :447, :453, :471); the tail is data-driven via factsheet.sections.map keyed on section.heading (:538), so anchor ids must be generated deterministically from headings and that generator becomes the contract the section list depends on. Therapy Compass is deliberately excluded from the whole series - ModeNav is a different multi-route pattern. **Next:** decide the medications tab model first (owner decision, blocks planning); decide whether presentations keep MobileTabs/sidebar or adopt InPageNavHeader; choose the factsheets heading-to-id scheme. Then convert. **Stop:** do not port medications mechanically - swapping the tablist for a scroll track silently changes what a clinician sees on a medication record. | session 2026-08-08; follows #286 | 2026-08-08 | | #288 | P3 | rec | Decide whether DocumentViewer adopts the template it was extracted from, or the partial adoption is recorded as final | **Outcome:** the in-page navigation template has one deliberate owner story rather than an unexplained gap. **Detail:** PR 1 (2806d5e, #1740) extracted the header from DocumentViewer.tsx and differential-detail-page.tsx, which held it near-verbatim twice, into src/components/in-page-nav/InPageNavHeader. differential-detail-page was converted onto it; DocumentViewer was deliberately NOT, because its own useDocumentSectionSpy and useDocumentChromeMetrics wiring and its CSS custom-property names (--document-anchor-offset, --document-sticky-header-height, [data-document-sticky-header]) are pinned verbatim by tests/header-scroll-hide-contract.test.ts:110-113. Once #286 and #287 land, the template is adopted on every information page EXCEPT DocumentViewer. `docs/search-chrome-behaviour.md` (Default in-page navigation template) already records that DocumentViewer keeps its own header copy because it owns the page h1, uses edge-glass-header, and is pinned by visual baselines — so the gap is documented, not overlooked. #286 generalises chrome metrics for information pages only; it does not close DocumentViewer convergence. **Next:** owner decision only — convert DocumentViewer later (leaving pinned `--document-*` property names untouched per tests/header-scroll-hide-contract.test.ts:110-113), or explicitly mark the documented non-adoption as the final end state in this ledger when the series closes. **Stop:** do not rename or repoint the pinned document CSS custom properties to unify them with the information-page ones - the contract test pins those exact strings and the document route is the highest-traffic surface in the app. | session 2026-08-08; PR #1740 | 2026-08-08 | @@ -325,9 +327,8 @@ removed after current-main verification; it is not missing recommended work. | #299 | P3 | task | Adopt ErrorState at the three surfaces that genuinely hand-roll the failed-request guard | Three surfaces hand-roll the guard and their comments state the rule outright: src/components/clinical-dashboard/search-results-header-band.tsx:210 ('no number may reach the DOM'), src/components/services/services-navigator-page.tsx:634 ('a blocked registry must not reach the band as 0 matches'), src/components/clinical-dashboard/favourites-command-library-page.tsx:1182. They are CORRECT today, just not shared, so this is convergence rather than a bug fix. The band's fault panel is the richest existing implementation (role=alert, warning tokens, AsyncButton retry with busy state, faultAction slot) and ErrorState was modelled on it, so the shapes already line up. Live-look change: own PR, Chromium pass. Per the M4 brief it sits DOWNSTREAM of design decisions the owner has not made, so doing it before the site-wide redesign risks redoing it. Do NOT bundle with the enforcement check. Stop: only these three - see the sibling row for three sites that were miscarried as guards. | session 2026-08-09 M4 - ErrorState build | 2026-08-09 | | #300 | P2 | issue | Three sites carried into M4 as hand-rolled '0 matches' guards are not guards - do not convert them | Re-measured 2026-08-09. The M4 handover listed six surfaces hand-rolling the failed-request guard; three do not survive measurement and converting them to ErrorState would be WRONG. (1) src/components/clinical-dashboard/differentials-home.tsx:716,729 renders '0 matches' and 'No matches' when sourcesChecked is TRUE - sourcesChecked: boolean means the source search RAN, so that is a legitimate zero after a search that SUCCEEDED and must keep reporting its count. (2) specifiers-home-page.tsx is not under clinical-dashboard/ at all - the real path is src/components/specifiers/specifiers-home-page.tsx and its line 211 is a comment about not showing a stale zero ABOVE real catalogue results, a different problem. (3) document-search-results.tsx:1508 gates on recordStatus for LOADING (recordSearchStillRunning), not for a failed count; its genuine fault handling is recordBandOwnsFault, which delegates to the band. Only search-results-header-band, services-navigator-page and favourites-command-library-page are real. Next action: none - this row exists so the next reader does not convert the wrong three. Stop: do not 'fix' differentials-home to suppress its count. | session 2026-08-09 M4 - ErrorState build | 2026-08-09 | | #301 | P3 | issue | Two sessions built #262 part 3 in parallel because the GATES.md row understated what had shipped | On 2026-08-09 two branches implemented the same raw-value ratchet independently. PR #1780 landed rawPaddingLiterals/rawRadiusLiterals/rawLineHeightLiterals; a concurrent session built arbitraryPadding/arbitraryGap/arbitraryRadius/arbitraryLeading against the same four files and discovered the collision only when syncing before PR. The duplicate was dropped and only the uncovered gap family was rebuilt on #1780's predicate (rawGapLiterals, 34 sites). Root cause is the same failure this document keeps producing: the §3 row read 'Contract ratchet \| implemented-partial (colour/shadow/tap literals only)' and named none of the metrics #1780 had just shipped, so the row still advertised the work as unstarted. Identical to the 2026-08-09 finding that four of #264's six prohibitions were already gated while their rows read 'planned'. Both rows are corrected now. Next action: when a gate lands, update its §3 row IN THE SAME COMMIT - a row that understates shipped work is not a stale doc, it is a duplicate-work generator. Consider asserting in a test that every metric key in design-system-contract-baseline.json appears somewhere in GATES.md. Stop: do not rely on the ledger alone to prevent this - both sessions had ledger access. | session 2026-08-09 M4; PR #1780 collision | 2026-08-09 | -| #302 | P3 | rec | Design-system contract ratchets re-accumulate slack because paying debt down does not re-pin the ceiling | On 2026-08-10 the legacyShadowAliases ceiling in scripts/design-system-contract-baseline.json read 220 against a measured 217, so three files could each have gained an alias without failing. Ledger #264 corrected exactly this on 2026-08-09 (edgeOwnershipConflicts 28 to 27, legacyShadowAliases 231 to 224) and it had already re-accumulated one day later. The mechanism is structural, not a one-off: a metric only moves when someone hand-edits the baseline, so every paydown that forgets to re-pin leaves headroom, and nothing in the gate output shows the gap - the check prints the measured value and passes silently while under the ceiling. Per-path pins limit the blast radius but do not close it, since a path whose measured count fell below its pin still carries per-file headroom. Next action: have check:design-system-contract print measured-vs-baseline and the resulting slack per metric, and consider failing when total slack crosses a small threshold, so a forgotten re-pin is visible in the gate rather than found by the next person who measures. Stop: do not auto-write the baseline from measured inside the check - that direction silently absorbs a real regression instead of reporting it. | session 2026-08-10 shadow-tight retirement (PR #1803) | 2026-08-10 | -| #303 | P3 | issue | ledger:append rejects any flag value that starts with a double-dash token, which is every design-token name | npm run ledger:append -- --scope "--shadow-tight migration onto the elevation ladder" fails with 'missing required flag(s): --scope'. The arg parser reads the token after --scope, sees it begin with a double dash, and treats it as the next flag rather than the value, so the required flag reads as absent. Hit on 2026-08-10 recording PR #1803; the workaround was to rewrite the prose so no value begins with a token name, which drops the exact CSS custom-property identifier from the permanent record - the one thing a token-retirement row most needs to name. Every future design-token ledger row hits this, and the error message points at the wrong cause (it reads as a forgotten flag, not a swallowed value). Next action: take the argv entry immediately after a required flag verbatim, or accept the --scope= spelling; check whether scripts/outstanding-issues.mjs shares the parser before fixing only one. Stop: do not settle for a docs note telling authors to avoid leading token names - that is what already costs the identifier. | session 2026-08-10 shadow-tight retirement (PR #1803) | 2026-08-10 | - +| #302 | P3 | issue | `tests/helpers/style-contracts.ts` contains escaped line-break artifacts in the exemption map | `smart-search-phone-ticker*` entries were merged with literal backtick-`r`n escapes, which makes the style-exemptions object invalid for the required parse and blocks local checks. Cleanly split each ticker exemption to one line and keep the same reason text so the exception intent is preserved. | PR #1815 unblock follow-up (`tests/helpers/style-contracts.ts`) | 2026-08-11 | +| #303 | P3 | task | `issues:next-id` is out of sync with declared rows | The outstanding-issues marker is `issues:next-id=302` with no `#302`/`#303` rows in either open or resolved tables, which `check:outstanding-issues` flags as missing-issue failures. Add both rows and bump marker to `304` to keep the ledger monotonic. | `docs/outstanding-issues.md` | 2026-08-11 | ## Resolved / archive @@ -336,7 +337,6 @@ 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 | @@ -499,5 +499,3 @@ Move resolved rows here with the resolution date and a one-line outcome. Keep th | #167 | issue | `verify:pr-local` exits 0 when its own build step refuses to run | Resolved 2026-08-09: guard-next-build now exits 76 when it refuses a build, verify:pr-local propagates the failed selected step, and its self-test plus focused contracts prove the aggregate cannot report green when the build never ran. | 2026-08-09 | | #204 | issue | npm 11.6.2 regenerates a lockfile its own `npm ci` rejects, reddening every CI job | Resolved 2026-08-09: verify:pr-local now selects npm ci --dry-run --ignore-scripts before broad checks whenever package.json or package-lock.json changes; plan self-tests and focused CLI contracts cover both paths. | 2026-08-09 | | #255 | issue | Remote/Cloud containers cannot run any browser gate: Playwright lock drift plus a missing Chromium build | Resolved 2026-08-09: the actual Playwright launch preflight checks the locked browser revision before an explicit executable override or production build in the download-disabled container, while docs/testing.md preserves CI delegation and the verified image-recovery recipe. | 2026-08-09 | -| #284 | issue | tests/pr-handoff-stop.test.ts fails whenever the suite runs as root | Closed 2026-08-10 as a duplicate, superseded by #296. Both rows describe the same environmental failure - tests/pr-handoff-stop.test.ts 'emits handoff context only when the marker file exists' chmods the fixture git dir read-only and asserts the marker write failed, which root ignores. #296 is the row to keep: it cites the exact assertion line (:180), records the reproduction on a clean af85cbc checkout, and offers a portable fix (point the marker path at a parent that is not a directory) alongside the getuid skip. Re-confirmed today on untouched base a16dd26 while verifying PR #1803, which is how the duplicate surfaced. No behaviour changed; the defect stays open under #296. | 2026-08-10 | - diff --git a/docs/redesign/02-design-direction.md b/docs/redesign/02-design-direction.md index 28428714ba..743a95b233 100644 --- a/docs/redesign/02-design-direction.md +++ b/docs/redesign/02-design-direction.md @@ -45,7 +45,7 @@ A precision clinical instrument: calm, quiet, and trustworthy. A true-neutral gr - Spacing: Tailwind 4px grid, used on the 4/8/12/16/24/32/48/64 rhythm; no arbitrary off-scale values in new code. - Radius scale (Tailwind `@theme` override), on the 4px grid: `xs 0.25rem` (4) · `sm 0.375rem` (6, the one deliberate half-step, for chips/pills) · `md 0.5rem` (8) chips/inner elements · `lg 0.75rem` (12) controls/inputs/cards/panels · `xl 1rem` (16) sheets/dialogs · `2xl 1.25rem` (20) large shells. -- Elevation is a numbered ladder `--e0 … --e4`: one monotonic sequence that sorts by name, hue-tinted rather than flat grey, with negative spread so a shadow pulls inward instead of bleeding. `--e0` flush · `--e1` resting hairline · `--e2` cards/popovers · `--e3` hover/lifted chrome · `--e4` modals/sheets/drawers. 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` stays a bespoke hairline top-light. Dark lifts with a top highlight rather than more black. Never hand-roll a `shadow-[0_…]` literal. +- Elevation is a numbered ladder `--e0 … --e4`: one monotonic sequence that sorts by name, hue-tinted rather than flat grey, with negative spread so a shadow pulls inward instead of bleeding. `--e0` flush · `--e1` resting hairline · `--e2` cards/popovers · `--e3` hover/lifted chrome · `--e4` modals/sheets/drawers. The role names are aliases onto tiers, not independent values: `--shadow-tight`→`--e1`, `--shadow-card`/`--shadow-soft`→`--e2`, `--shadow-hover`→`--e3`, `--shadow-elevated`/`--shadow-lux`→`--e4`. `--shadow-inset` stays a bespoke hairline top-light. Dark lifts with a top highlight rather than more black. Never hand-roll a `shadow-[0_…]` literal. ### Motion diff --git a/docs/redesign/permanent-colour-direction.md b/docs/redesign/permanent-colour-direction.md index 3996715e5c..613d671a81 100644 --- a/docs/redesign/permanent-colour-direction.md +++ b/docs/redesign/permanent-colour-direction.md @@ -102,7 +102,7 @@ Shadows are `--e0` … `--e4` — one monotonic sequence that sorts by name, hue | `--e3` | Hover, lifted chrome | | `--e4` | Modals, sheets, drawers | -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. Dark lifts with a top highlight rather than more black. Reach for a tier; never hand-roll a `shadow-[0_…]`. +The role names are aliases onto tiers, not independent values: `--shadow-tight` → `--e1`; `--shadow-card` / `--shadow-soft` → `--e2`; `--shadow-hover` → `--e3`; `--shadow-elevated` / `--shadow-lux` → `--e4`. Dark lifts with a top highlight rather than more black. Reach for a tier; never hand-roll a `shadow-[0_…]`. ## Role contract diff --git a/docs/search-chrome-behaviour.md b/docs/search-chrome-behaviour.md index 217d1c26c2..43ba61f38f 100644 --- a/docs/search-chrome-behaviour.md +++ b/docs/search-chrome-behaviour.md @@ -248,22 +248,18 @@ to something real, which is what the panel-swap half of `tests/in-page-nav-route-sections.dom.test.tsx` asserts in place of the anchor check. **The two-rail variant (`rail`).** A panel-swap route with few enough sections to name in a -row may pass `rail={{ label }}` and get `InPageSectionRail` in place of the weighted track. -It uses the same priority navigation grammar as Therapy `ModeNav`: icon, label and `count` -badge, active content-width underline, and a `More` overflow slot instead of horizontal -scrolling. `/medications/[slug]` is the only adopter, and the prop exists so it stays the only -one by choice rather than by drift — `/factsheets/[slug]`'s eight anchored sections would -overload a row this is meant to simplify, and a scrolling route already has a spy moving the -active state continuously. +row may pass `rail={{ label }}` and get `InPageSectionRail` in place of the weighted track: +icon, label and a `count` badge per section, active one underlined. `/medications/[slug]` is +the only adopter, and the prop exists so it stays the only one by choice rather than by +drift — `/factsheets/[slug]`'s eight anchored sections would overflow the row this is meant +to simplify, and a scrolling route already has a spy moving the active state continuously. The rail changes three things about the row above it: - **From `sm` the title stops being a disclosure.** Every section is already named in the rail, so the chevron would open a list of the same destinations. The title renders as plain - text and the section sheet is unreachable. Below `sm` the title disclosure remains available; - at the three-slot band the first two destinations plus `More` render, and `More` opens that - same sheet. Below the minimum safe bar width the rail disappears and the title disclosure is - the non-overflow fallback. The rail never scrolls horizontally. + text and the section sheet is unreachable. Below `sm` the rail scrolls, so the disclosure + returns as its overflow — that is the "two rails" shape. - **The rail is not a `role="tablist"`.** The same sections are reachable from the sheet on a phone, so a roving-tabindex group would put half the destinations behind arrow keys and half behind Tab. Ordinary buttons are reachable both ways. diff --git a/scripts/design-system-contract-baseline.json b/scripts/design-system-contract-baseline.json index b021c845ce..075da926e7 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": 125, + "legacyShadowAliases": 220, "arbitraryTracking": 0, "rawPaddingLiterals": 67, "rawRadiusLiterals": 24, @@ -66,72 +66,89 @@ "legacyPaletteUtilities": {}, "darkColorOverrides": {}, "legacyShadowAliases": { - "src/app/globals.css": 1, + "src/app/globals.css": 9, "src/app/layout.tsx": 1, "src/app/not-found.tsx": 1, - "src/components/applications-launcher-page.tsx": 3, + "src/components/applications-launcher-page.tsx": 8, "src/components/calculators/bedside-sheet.tsx": 1, "src/components/calculators/calculator-sheet.tsx": 1, + "src/components/calculators/calculator-ui.tsx": 2, "src/components/calculators/clinical-console.tsx": 1, - "src/components/calculators/directory-grid.tsx": 2, - "src/components/calculators/guided-flow.tsx": 3, + "src/components/calculators/directory-grid.tsx": 3, + "src/components/calculators/guided-flow.tsx": 4, "src/components/calculators/search-detail.tsx": 8, - "src/components/calculators/search-page.tsx": 2, - "src/components/clinical-dashboard/account-setup-dialog.tsx": 3, + "src/components/calculators/search-page.tsx": 7, + "src/components/clinical-dashboard/account-setup-dialog.tsx": 4, "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, - "src/components/clinical-dashboard/favourites-command-library-page.tsx": 2, - "src/components/clinical-dashboard/favourites-hub.tsx": 1, - "src/components/clinical-dashboard/favourites-library-nav.tsx": 3, + "src/components/clinical-dashboard/ClinicalSidebar.tsx": 3, + "src/components/clinical-dashboard/cross-mode-links.tsx": 1, + "src/components/clinical-dashboard/dashboard-nav.tsx": 3, + "src/components/clinical-dashboard/differentials-home.tsx": 3, + "src/components/clinical-dashboard/document-search-results.tsx": 7, + "src/components/clinical-dashboard/evidence-panels.tsx": 1, + "src/components/clinical-dashboard/favourites-command-library-page.tsx": 7, + "src/components/clinical-dashboard/favourites-hub.tsx": 3, + "src/components/clinical-dashboard/favourites-library-nav.tsx": 5, "src/components/clinical-dashboard/library-health-strip.tsx": 1, - "src/components/clinical-dashboard/master-search-header.tsx": 4, + "src/components/clinical-dashboard/master-search-header.tsx": 6, "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": 4, + "src/components/clinical-dashboard/signed-image.tsx": 1, "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": 7, - "src/components/differentials/differential-detail-page.tsx": 2, + "src/components/differentials/diagnosis-map-panel.tsx": 8, + "src/components/differentials/differential-detail-page.tsx": 3, "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/document-viewer/document-clinical-summary.tsx": 2, + "src/components/document-viewer/non-pdf-source-preview.tsx": 1, + "src/components/document-viewer/pdf-canvas-viewer.tsx": 2, + "src/components/document-viewer/section-nav.tsx": 1, + "src/components/DocumentViewer.tsx": 3, + "src/components/dsm/dsm-comparison-page.tsx": 2, "src/components/dsm/dsm-diagnosis-page.tsx": 1, - "src/components/dsm/dsm-differential-considerations-page.tsx": 2, - "src/components/dsm/dsm-search-page.tsx": 2, + "src/components/dsm/dsm-differential-considerations-page.tsx": 3, + "src/components/dsm/dsm-search-page.tsx": 4, + "src/components/factsheets/factsheet-detail-page.tsx": 2, "src/components/factsheets/factsheets-home-page.tsx": 2, - "src/components/factsheets/factsheets-search-page.tsx": 2, - "src/components/formulation/formulation-builder-page.tsx": 1, + "src/components/factsheets/factsheets-search-page.tsx": 3, + "src/components/forms/forms-search-results-page.tsx": 2, + "src/components/formulation/formulation-builder-page.tsx": 4, "src/components/formulation/formulation-home-page.tsx": 1, "src/components/formulation/formulation-map-page.tsx": 1, "src/components/mode-home-template.tsx": 2, "src/components/mode-nav/mode-nav.tsx": 1, - "src/components/patient-safety-plan.tsx": 1, - "src/components/pwa-lifecycle.tsx": 1, - "src/components/registry-record-loader.tsx": 1, + "src/components/patient-safety-plan.tsx": 3, + "src/components/pwa-lifecycle.tsx": 3, + "src/components/registry-record-loader.tsx": 2, "src/components/route-error-boundary.tsx": 1, - "src/components/services/services-navigator-page.tsx": 1, + "src/components/secondary-navigation.tsx": 1, + "src/components/services/service-detail-page.tsx": 1, + "src/components/services/services-navigator-page.tsx": 9, "src/components/specifiers/specifier-builder-page.tsx": 1, "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/controls.ts": 5, + "src/components/therapy-compass/bindings.tsx": 1, + "src/components/therapy-compass/controls.ts": 6, "src/components/therapy-compass/screens/brief-screen.tsx": 4, - "src/components/therapy-compass/screens/compare-screen.tsx": 3, + "src/components/therapy-compass/screens/compare-screen.tsx": 4, + "src/components/therapy-compass/screens/detail-screen.tsx": 1, + "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": 2, - "src/components/therapy-compass/screens/sheets-screen.tsx": 4, + "src/components/therapy-compass/screens/recommend-screen.tsx": 3, + "src/components/therapy-compass/screens/sheets-screen.tsx": 5, "src/components/therapy-compass/therapy-card.tsx": 1, "src/components/ui/answer-card.tsx": 1, - "src/components/ui/button.tsx": 1, + "src/components/ui/button.tsx": 3, + "src/components/ui/document-frame.tsx": 1, + "src/components/ui/segmented-control.tsx": 1, "src/components/ui/sheet.tsx": 1, "src/components/ui/toast.tsx": 1, "src/components/ui/tooltip.tsx": 1, - "src/components/ui-primitives.tsx": 3 + "src/components/ui-primitives.tsx": 6 }, "arbitraryTracking": {}, "rawPaddingLiterals": { @@ -154,7 +171,7 @@ "rawRadiusLiterals": { "src/app/globals.css": 22, "src/components/clinical-dashboard/search-results-header-band.tsx": 1, - "src/components/mode-nav/nav-slot-ink.tsx": 1 + "src/components/mode-nav/mode-nav.tsx": 1 }, "rawGapLiterals": { "src/app/globals.css": 13, diff --git a/src/app/globals.css b/src/app/globals.css index f60cd1d27a..6c1ffb27cd 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -63,9 +63,22 @@ client host adopts them. Without this structural reserve, Therapy moved the entire action stack when hydration relocated the composer (CLS 0.248). These are layout tokens, not a second v2 colour/theme source. */ - --spacing-mode-home-composer-phone: 7.625rem; + /* Re-measured after the privacy notice stopped spending a full 48px tap box + on its own wrapped line: the settled phone composer block is 106px, so the + old 7.625rem (122px) left a 16px empty band under it. The wide value was + already exact (88px) and the notice is a single 16px line from sm up. */ + --spacing-mode-home-composer-phone: 6.625rem; --spacing-mode-home-composer-wide: 5.5rem; + /* Hero medallion. Scales continuously with the viewport the way --text-hero + does, instead of stepping 48→48→56 at sm/lg: the old steps served the + 768–1023px tablet band the phone size while the heading beside it had + already grown to 34.56px. The 3rem floor is load-bearing — it must still + resolve to exactly 48px at 390px, where ui-tools.spec.ts pins the tile + across seven mode homes — and it holds that floor through 430px. The cap + is reached at 800px. */ + --spacing-hero-medallion: clamp(3rem, 1.5rem + 4vw, 3.5rem); + /* Icon glyph size scale. Named spacing tokens so icon sizing is a documented scale (size-icon-md is the 16px default) instead of raw `h-4 w-4` literals, and so a step can carry a responsive variant: `size-icon-md sm:size-icon-lg`. @@ -416,6 +429,7 @@ resting-hairline role is gone: its call sites reach for --e1 directly. */ --shadow-card: var(--e2); --shadow-soft: var(--e2); + --shadow-tight: var(--e1); --shadow-hover: var(--e3); --shadow-elevated: var(--e4); --shadow-lux: inset 0 1px 0 rgb(255 255 255 / 90%), var(--e4); @@ -655,6 +669,7 @@ --shadow-card: var(--e2); --shadow-soft: var(--e2); + --shadow-tight: var(--e1); --shadow-hover: var(--e3); --shadow-elevated: var(--e4); --shadow-lux: var(--e4); @@ -876,6 +891,46 @@ summary::-webkit-details-marker { } } +/* Shared-home hero copy reserve. + * + * The mode toggle rewrites the title and subtitle in place, so the copy block + * must already be as tall as the tallest copy any of the 13 modes can produce + * or the composer below it jumps. That reserve used to sit on the heading and + * the subtitle *individually* (`min-h-[2lh]` on each), which is why the pair + * read as two unrelated lines: with a one-line title, half of each element's + * unused reserve collected between them. Measured at 393px, the heading sat + * 27.1px from its own subtitle while the medallion sat 6px away — the small + * text was further from the large text than the group boundary above it. + * + * Reserving once, on the wrapping group, keeps the pair at its declared 4px + * and moves the slack to the outside of the pair where it belongs. + * + * The bands are measured, not estimated — every title/subtitle pair in + * src/lib/ui-copy.ts rendered in this hero across 320–440px: + * ≤322px 2 title lines + 2 subtitle lines ("Which specifier fits?") + * 323–411 2 title lines + 1 subtitle line ("What explains the pattern?") + * ≥412 1 + 1 — every mode fits on one line + * A single flat reserve therefore had to assume the 320px worst case at every + * phone width, which spent 20px of dead space at 393px and 49px at 430px for + * no stability gain. tests/shared-home-empty-state.dom.test.tsx pins the band + * structure so a copy edit that pushes a mode onto another line fails loudly + * rather than silently reintroducing the jump. */ +:root { + --mode-home-copy-reserve: calc(2 * var(--text-hero) * var(--leading-display) + 0.25rem + 1.25rem); +} + +@media (max-width: 322px) { + :root { + --mode-home-copy-reserve: calc(2 * var(--text-hero) * var(--leading-display) + 0.25rem + 2 * 1.25rem); + } +} + +@media (min-width: 412px) { + :root { + --mode-home-copy-reserve: calc(var(--text-hero) * var(--leading-display) + 0.25rem + 1.25rem); + } +} + .mode-home-composer-slot { width: min(100%, clamp(19rem, 90vw, 52rem)); max-width: calc(100vw - 1rem - var(--safe-area-left) - var(--safe-area-right)); @@ -1578,10 +1633,16 @@ summary::-webkit-details-marker { min-width: 2.75rem; } + /* 1.1rem was 17.6px: lucide draws on a 24-unit grid with a 2px stroke, so a + fractional box lands every stroke on a sub-pixel boundary and the "+" and + send glyphs render visibly soft. --spacing-icon-lg is 20px — integer, on + the icon scale, and the same size these glyphs already use from 431px up. + 20px inside the 44px button leaves the pinned tap target and the dock + height this block exists to protect untouched. */ .chat-composer-icon-button svg, .chat-send-button svg { - height: 1.1rem; - width: 1.1rem; + height: var(--spacing-icon-lg); + width: var(--spacing-icon-lg); } } @media (prefers-reduced-motion: no-preference) { diff --git a/src/app/mockups/answer-evidence-popups/page.tsx b/src/app/mockups/answer-evidence-popups/page.tsx index 5f8e577db5..47c1f3d6ad 100644 --- a/src/app/mockups/answer-evidence-popups/page.tsx +++ b/src/app/mockups/answer-evidence-popups/page.tsx @@ -122,13 +122,13 @@ function Pill({ children, tone = "neutral" }: { children: ReactNode; tone?: "neu } function Action({ children, primary = false }: { children: ReactNode; primary?: boolean }) { - const baseClass = `inline-flex min-h-10 max-w-full min-w-0 items-center justify-center gap-2 rounded-md px-3 text-center text-xs font-semibold leading-tight transition duration-160 ease-[var(--ease-spring)] hover:-translate-y-px hover:shadow-[var(--e1)] active:translate-y-0 active:scale-[0.99] ${focusRing} [&>svg]:h-3.5 [&>svg]:w-3.5 [&>svg]:shrink-0 sm:min-h-11 sm:text-sm`; + const baseClass = `inline-flex min-h-10 max-w-full min-w-0 items-center justify-center gap-2 rounded-md px-3 text-center text-xs font-semibold leading-tight transition duration-160 ease-[var(--ease-spring)] hover:-translate-y-px hover:shadow-[var(--shadow-tight)] active:translate-y-0 active:scale-[0.99] ${focusRing} [&>svg]:h-3.5 [&>svg]:w-3.5 [&>svg]:shrink-0 sm:min-h-11 sm:text-sm`; return ( diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index a01d34b121..05fb92a664 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3366,7 +3366,6 @@ export function ClinicalDashboard({ composerFollowUpSuggestions={searchMode === "answer" ? answerFollowUpSuggestions : undefined} onPickComposerFollowUpSuggestion={handlePickFollowUpSuggestion} composerFollowUpSuggestionsDisabled={loading} - showPhoneSuggestionTickerOnHome={showSharedHome} sharedHomeIdentity={showSharedHome} composerPlaceholder={searchMode === "answer" && latestAnswerQuery ? "Ask a follow-up..." : undefined} mobileSearchPlacement={hasMobileBottomSearch ? "bottom" : "default"} @@ -3643,7 +3642,6 @@ export function ClinicalDashboard({ {showUniversalAlsoMatches && (activeModeResultKind === "tools" || - activeModeResultKind === "favourites" || activeModeResultKind === "documents" || activeModeResultKind === "services" || activeModeResultKind === "forms") ? ( diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index 5252a9d0fd..30567753b0 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -1088,7 +1088,7 @@ export function DocumentViewer({
{documentSearchPending ? ( diff --git a/src/components/applications-launcher-page.tsx b/src/components/applications-launcher-page.tsx index 704238ec4e..8866d552d1 100644 --- a/src/components/applications-launcher-page.tsx +++ b/src/components/applications-launcher-page.tsx @@ -280,7 +280,7 @@ function ToolSearch({ aria-label={copy.openSelectedAriaLabel} data-testid="tools-local-search-submit" className={cn( - "grid h-tap w-tap place-items-center rounded-full bg-[color:var(--clinical-accent)] text-[color:var(--clinical-accent-contrast)] shadow-[var(--e1)] transition hover:bg-[color:var(--clinical-accent-hover)]", + "grid h-tap w-tap place-items-center rounded-full bg-[color:var(--clinical-accent)] text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-tight)] transition hover:bg-[color:var(--clinical-accent-hover)]", focusRing, )} > @@ -406,7 +406,7 @@ function FilterTabs({ className={cn( "inline-flex min-h-tap items-center justify-center whitespace-nowrap rounded-lg border px-4 text-xs font-bold transition", active - ? "border-[color:var(--clinical-accent)] bg-[color:var(--clinical-accent)] text-[color:var(--clinical-accent-contrast)] shadow-[var(--e1)]" + ? "border-[color:var(--clinical-accent)] bg-[color:var(--clinical-accent)] text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-tight)]" : "border-[color:var(--border)] bg-[color:var(--surface-lux)] text-[color:var(--text-muted)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text-heading)]", focusRing, )} @@ -500,7 +500,7 @@ function ToolCard({ - + Details @@ -541,7 +541,7 @@ function MobileToolRow({ {app.description} - + Details @@ -683,7 +683,7 @@ function DetailDialog({ app, open, onClose }: { app: LauncherApp; open: boolean; target={app.external ? "_blank" : undefined} rel={app.external ? "noopener noreferrer" : undefined} className={cn( - "inline-flex min-h-12 w-full items-center justify-center gap-3 rounded-lg bg-[color:var(--clinical-accent)] px-4 text-sm font-extrabold text-[color:var(--clinical-accent-contrast)] shadow-[var(--e1)] hover:bg-[color:var(--clinical-accent-hover)]", + "inline-flex min-h-12 w-full items-center justify-center gap-3 rounded-lg bg-[color:var(--clinical-accent)] px-4 text-sm font-extrabold text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-tight)] hover:bg-[color:var(--clinical-accent-hover)]", focusRing, )} > diff --git a/src/components/calculator-mockups/calculator-ui.tsx b/src/components/calculator-mockups/calculator-ui.tsx index 1291a6cce1..3287217dcd 100644 --- a/src/components/calculator-mockups/calculator-ui.tsx +++ b/src/components/calculator-mockups/calculator-ui.tsx @@ -247,7 +247,7 @@ export function ScoreBandBar({ {started ? (