diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md
index 7c94ea76d..4165e5929 100644
--- a/docs/branch-review-ledger.md
+++ b/docs/branch-review-ledger.md
@@ -614,3 +614,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-18 | origin/main framework and dependency modernization snapshot | 4057677c8b92a5e1d997ec44958764fa91f5d424 | parallel build/infra, backend, and frontend modernization audit | Changes requested. No P0. Confirmed two P1 defects: Supabase SSR 0.12 auth-cookie responses discard mandatory anti-cache headers, and reindex bypasses the server-only-aware TSX runner after mutation-capable setup. Five P2 blockers cover the Webpack-to-Turbopack production cutover, incomplete Railway image-build watch ownership, missing clean `next typegen`, Node 26 types over a Node 24 runtime, and App Router retry actions that reset without re-fetching. P3 removal-readiness debt remains in Zod and Next Image APIs. Manual rewrite zones are auth response ownership, bundler/CSP/artifact consumers, JSZip resource limits, OpenAI request typing, and React Compiler adoption in the stateful dashboard/viewer roots. | Parallel read-only source/config/test audit against the exact snapshot; Node 24.18.0/npm 11.17.0; exact-version Next 16.2.10 bundled upgrade, Turbopack, error-boundary, and codemod guidance; TypeScript 6 backend no-emit analysis and Node import probes via a separately installed exact-version local dependency tree; `git diff --check`. No install, registry outdated/audit, full repo lint/typecheck/test/build/browser/Docker, Supabase/OpenAI, deployment, or hosted CI run; this worktree had no `node_modules`. |
| 2026-07-18 | claude/clinical-kb-pwa-review-asi3wb (PR #890, plan Phase 4; single-batch implementation+ledger commit — Phase 3 landed as squash 44a4c511bc1381168474794d6f89273564659ead) | ceabc04d75fd0cea2a120d504418d44edd704bfb | App-wide performance pass: audit-then-fix + budget ratchet (plan Phase 4) | Audit on post-#872 main: enforced gzip budget +3.13% over the 1,363,382-byte baseline; analyzer treemap showed pdfjs-dist (123 KB) and cross-mode-differentials data (121 KB) already correctly lazy, and disproved the suspected forms-catalog dashboard leak (type-only import). Confirmed one real defect: the 143-line `/services` client home page value-imported `defaultServiceSlug` from `@/lib/services`, compiling the ~915 KB services snapshot (~100 KB gzip) into its route chunk. Fixed by computing the slug in the server page (`src/app/services/page.tsx`) and passing it as a prop; the client component builds its task cards from the prop. Measured result: budget swung from +3.13% to −3.97%; baseline ratcheted down 1,363,382 → 1,309,286 bytes gzip so CI locks the win. pdfjs/differentials/#718 paths deliberately untouched per audit rules. Two transient `.next/dev` generated-type corruptions from the long-lived dev server were resolved by stopping the server and setting the generated dir aside (reversible); nothing hand-deleted. | Build + `check:bundle-budget --json` before/after (artifacts in session scratchpad); `check:bundle-budget -- --update` for the ratchet; `npm run test` 2789 passed/1 failed (known container-only `pdf-extraction-budget` artifact, hosted-CI-green through #826/#835/#872); `verify:cheap` green to the same artifact; `verify:ui` 219 passed/2 failed (the two long-established container artifacts; ui-tools spec covering the services surface passed); `verify:pr-local` unit stage identical; typecheck/lint/format clean. Lighthouse not run (dev-server churn; bundle evidence sufficed for this fix set). No provider-backed checks run. |
| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | bb85b546e + ledger closeout | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production for both prototype items and set suggestions, separated local no-auth upload capability from Favourites demo treatment, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; the final demo/upload boundary selection passed 4/4; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. The full local Chromium sweep completed 236/237 with one hydration-timing failure: the app-mode test clicked before React attached its handler. The existing handler-readiness pattern was applied; focused rerun attempts were then lock-blocked by another registered worktree, so exact-head hosted Chromium remains the required proof. Initial hosted checks, including Production UI, passed before these final follow-ups. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. |
+| 2026-07-18 | claude/clinical-kb-pwa-review-asi3wb (PR #896, plan Phase 5; content commit + this ledger follow-up) | a6c2b4e92374e9002fb00c547eb5677d01ce538c | Design-polish sweep: audit-then-fix (plan Phase 5, final phase) | Audit on post-#890 main: three strict design guards clean; full re-run of the 07-token-adoption-audit grep method shows all July 3 debt resolved (M1–M3 done, L4 reduced to the deliberate theme-aware `ring-white/N dark:ring-white/10` glass idiom, L5/L7 gone; production hex all legitimate print/brand/console/comment classes); 43-capture live sweep across 15 routes × desktop/phone + 320px spots + dark/reduced-motion/forced-colors spots found 0 overflow and 0 console errors. Three defects found and fixed: (1) forced-colors solid-button labels rendered as blank Canvas-on-Canvas backplate boxes (axe-invisible) — command controls flattened to the native HCM ButtonFace/ButtonText pairing and accent glyph tokens flipped to ButtonText inside the existing forced-colors block, regression-locked by a new ui-accessibility test; (2) tools desktop 6-up quick-action rail truncated card titles at 1440×1000 — card metrics tightened, all six titles verified unclipped; (3) privacy page rendered "systemand" from a JSX newline-adjacent-to-tag drop — explicit space, locked by a privacy-ui assertion. Dated July 18 run appended to docs/redesign/07-token-adoption-audit.md (archived design-qa.md not resurrected). | Guards + focused vitest 14/14; `verify:cheap` chain green to the known container-only pdf-extraction-budget artifact (2806/2809); `verify:ui` 220 passed/2 failed (the two long-baselined container artifacts, hosted-CI-green through #826/#835/#872/#890); `test:e2e:accessibility` 8/8 incl. the new forced-colors token test; production build + client-bundle secret scan passed; `check:bundle-budget` within tolerance vs the Phase 4 ratchet (1290.6 vs 1278.6 KiB baseline); `verify:pr-local` runtime/format/lint/typecheck/build/rag-fixtures green with the same sole unit-suite artifact. `verify:release` not run (provider-backed; awaits explicit confirmation). No provider-backed checks run. |
diff --git a/docs/redesign/07-token-adoption-audit.md b/docs/redesign/07-token-adoption-audit.md
index bb7e6562d..683f331b3 100644
--- a/docs/redesign/07-token-adoption-audit.md
+++ b/docs/redesign/07-token-adoption-audit.md
@@ -78,3 +78,69 @@ findings as "clean before the pattern spreads," not shipped-primitive regression
- Raw palette: `rg '\b(bg|text|border|ring|from|to|via|fill|stroke)-(gray|slate|…|rose)-(50|…|950)\b' src/components -g '*.tsx'`
- Dead bridge: `rg '("|'"'"'| )(bg-surface|text-muted|text-heading|…)( |"|'"'"')' src` → 0 hits.
- Arbitrary values ranked: `rg -oN '…-\[[^]]+\]' src/components -g '*.tsx' | sort | uniq -c | sort -rn`
+
+## July 18 run — Phase 5 design-polish sweep (re-score + live route sweep)
+
+Scope: full re-run of the July 3 grep method on current `main`, plus a live
+route sweep — 15 production routes × (1440×1000 desktop + 390×844 phone),
+320×844 spot checks on the 4 densest routes, and dark / reduced-motion /
+forced-colors desktop spots on `/`, `/differentials`, `/documents/search`.
+Server identity confirmed via `/api/local-project-id` before attaching.
+
+**Score: 92/100** — 0 critical · 0 medium · 2 low (naming consistency and the
+mockup-file palette leaks, both unchanged P3 charter exclusions).
+
+### Re-score of July 3 findings
+
+| Item | July 3 | July 18 |
+| --------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| M1 dead `@theme inline` bridge | Fix | ✅ Resolved (bridge deleted). |
+| M2 raw-palette tone helpers | Done on branch | ✅ Landed; production `src/components` has 0 raw-palette classes (3 mockup files only). |
+| M3 ~295 hardcoded px font sizes | Partly unblocked | ✅ Adopted; `check:type-scale --strict` passes with 0 arbitrary sizes in `src`. |
+| L4 hardcoded white/black neutrals | Polish | ✅ Down to 4 sites, all the deliberate `ring-1 ring-white/N dark:ring-white/10` glass-highlight idiom with explicit dark variants. 0 accent-button `text-white`. |
+| L5 `border-blue-400` | Polish | ✅ Gone. |
+| L6 tap-target magic numbers | Done | ✅ Holds (0 arbitrary tap tokens). |
+| L7 `rounded-[var(--radius-lg)]` | Polish | ✅ Gone. |
+| L8 recipe contract docs | Done | ✅ Holds (`09-ui-primitives-recipes.md`). |
+
+Hex audit note: every current hex hit in production components is a legitimate
+class — print-handout surfaces (`FactsheetPrintSheet`, therapy-compass sheet
+`@media print`), official provider brand marks (Google/Microsoft), web-vitals
+console-log colors, PR-number code comments (regex false positives), and the
+white switch-knob-on-accent-track pattern.
+
+### Route sweep result
+
+43 captures: 0 horizontal overflow at any viewport (including 320px), 0
+console errors, 0 failed network requests. Mode-home template routes render
+consistently across desktop/phone/320; dark theme and reduced-motion spots
+clean.
+
+### New findings (all fixed in this pass)
+
+1. **Forced-colors blank button labels** (`globals.css` forced-colors block).
+ Chromium paints a Canvas backplate behind every glyph run in forced-colors
+ mode, so glyph tokens that resolved to the Canvas/ButtonFace family
+ (`--command-contrast`, `--primary-contrast`, `--clinical-accent-contrast`,
+ `--danger-solid-contrast`) rendered solid-button labels as blank boxes —
+ invisible to axe, which reads CSS rather than painted pixels. SVG strokes
+ get no backplate, so the dark `--command` fill also swallowed ButtonText
+ icons. Fix: command controls flatten to the native HCM pairing
+ (ButtonFace fill / ButtonText glyphs); accent fills keep their system
+ colors and flip only their glyph tokens to ButtonText. Regression-locked by
+ a new `ui-accessibility.spec.ts` test asserting the glyph tokens never
+ resolve to the Canvas color.
+2. **Tools quick-action rail title truncation at 1440×1000**
+ (`applications-launcher-page.tsx`). The desktop 6-up rail left ~85px for
+ text against ~92px titles ("Ask eviden…", "Safety che…"). Fix: tightened
+ card metrics (icon column 2.25rem→2rem, gap-3→gap-2, px-3→px-2.5, icon
+ h-9→h-8); all six titles now render whole, descriptions remain
+ designed-supplementary ellipsis (full copy lives in the All-tools cards and
+ aria-labels). Phone icon-grid variant untouched.
+3. **Privacy-page microcopy** (`src/app/privacy/page.tsx`). JSX drops a
+ newline adjacent to a tag, rendering "…patient-record systemand does not
+ ask…". Fixed with an explicit `{" "}`; locked by a `privacy-ui.test.ts`
+ assertion.
+
+Observation, no action: a sub-perceptual glass-header backdrop-gradient ghost
+at the content top-left (visible only at 4× zoom) — by-design translucency.
diff --git a/src/app/globals.css b/src/app/globals.css
index be324e945..48dc21e36 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -2335,16 +2335,24 @@ html[data-motion="reduced"] .source-capsule-hit[aria-expanded="true"]:hover .sou
--primary: LinkText;
--primary-strong: LinkText;
--primary-soft: Canvas;
- --primary-contrast: Canvas;
+ --primary-contrast: ButtonText;
--surface-chrome: Canvas;
- --command: ButtonText;
- --command-hover: ButtonText;
- --command-contrast: ButtonFace;
+ /* Chromium paints a Canvas backplate behind every glyph run in
+ forced-colors mode, so label/glyph tokens must never resolve to the
+ Canvas/ButtonFace family — Canvas-on-Canvas glyphs vanish and solid
+ buttons render blank label boxes. SVG icon strokes get no backplate at
+ all, so the dark command fill also cannot survive: it would swallow
+ ButtonText icons. Command controls therefore flatten to the native HCM
+ button pairing (ButtonFace fill, ButtonText glyphs); accent fills keep
+ their system colors and flip only their glyph tokens. */
+ --command: ButtonFace;
+ --command-hover: ButtonFace;
+ --command-contrast: ButtonText;
--clinical-accent: LinkText;
--clinical-accent-hover: LinkText;
--clinical-accent-soft: Canvas;
--clinical-accent-border: ButtonBorder;
- --clinical-accent-contrast: Canvas;
+ --clinical-accent-contrast: ButtonText;
--info: LinkText;
--info-soft: Canvas;
--success: CanvasText;
@@ -2354,7 +2362,9 @@ html[data-motion="reduced"] .source-capsule-hit[aria-expanded="true"]:hover .sou
--danger: Mark;
--danger-soft: Canvas;
--danger-solid: Mark;
- --danger-solid-contrast: Canvas;
+ /* Mark's palette-paired foreground — ButtonText is not guaranteed to
+ contrast a Mark fill in arbitrary user schemes. */
+ --danger-solid-contrast: MarkText;
--tone-purple: CanvasText;
--tone-indigo: CanvasText;
--tone-rose: CanvasText;
diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx
index a5de27ca2..0375be696 100644
--- a/src/app/privacy/page.tsx
+++ b/src/app/privacy/page.tsx
@@ -25,9 +25,8 @@ const SECTIONS: Section[] = [
body: (
<>
Clinical KB is a knowledge base over clinical reference material. It is{" "}
- not a patient-record system
- and does not ask for patient data. The main privacy risk is incidental patient information entered into a
- free-text question or uploaded document.
+ not a patient-record system and does not ask for patient data. The main privacy risk is
+ incidental patient information entered into a free-text question or uploaded document.
>
),
},
diff --git a/src/components/applications-launcher-page.tsx b/src/components/applications-launcher-page.tsx
index bceb8cb4b..256bd1a0b 100644
--- a/src/components/applications-launcher-page.tsx
+++ b/src/components/applications-launcher-page.tsx
@@ -287,14 +287,14 @@ function QuickActions({ onSelect, mobile }: { onSelect: (id: string) => void; mo
focusRing,
mobile
? "grid h-14 min-w-0 place-items-center gap-0.5 rounded-lg px-1 py-1.5 text-center"
- : "grid min-h-14 grid-cols-[2.25rem_minmax(0,1fr)] items-center gap-3 rounded-lg px-3 py-2.5",
+ : "grid min-h-14 grid-cols-[2rem_minmax(0,1fr)] items-center gap-2 rounded-lg px-2.5 py-2.5",
)}
>
diff --git a/tests/privacy-ui.test.ts b/tests/privacy-ui.test.ts
index 1192f575d..6c3f70392 100644
--- a/tests/privacy-ui.test.ts
+++ b/tests/privacy-ui.test.ts
@@ -32,6 +32,9 @@ describe("privacy UI", () => {
]) {
expect(markup).toContain(heading);
}
+ // JSX drops a newline adjacent to a tag, so the space after the bolded
+ // phrase must be explicit or this renders as "systemand".
+ expect(markup).toContain("not a patient-record system and does not ask for patient data");
expect(markup).toContain("Generated answer text is also omitted from durable query logs by default");
expect(markup).toContain("application service in Singapore");
expect(markup).toContain("Railway in Singapore");
diff --git a/tests/ui-accessibility.spec.ts b/tests/ui-accessibility.spec.ts
index 3253ec31d..4574c417d 100644
--- a/tests/ui-accessibility.spec.ts
+++ b/tests/ui-accessibility.spec.ts
@@ -253,6 +253,48 @@ test.describe("Clinical KB accessibility coverage", () => {
await expectNoPageHorizontalOverflow(page);
});
+ test("solid-button label tokens stay legible with forced colors", async ({ page }) => {
+ // Chromium paints a Canvas backplate behind every glyph run in forced-colors
+ // mode: a label whose color resolves into the Canvas/ButtonFace family
+ // disappears into its own backplate (axe cannot see this — it reads CSS,
+ // not painted pixels). Lock the glyph tokens to a non-Canvas resolution.
+ await page.emulateMedia({ forcedColors: "active" });
+ await page.setViewportSize({ width: 1440, height: 1000 });
+ await mockMinimalDashboardApi(page);
+ await gotoApp(page);
+ await expectDashboardUsable(page);
+
+ const newChat = page.getByRole("button", { name: "New chat" }).first();
+ await expect(newChat).toBeVisible();
+ const { canvas, buttonLabelColor, tokenColors } = await newChat.evaluate((button) => {
+ const probe = document.createElement("span");
+ document.body.append(probe);
+ const resolveColor = (value: string) => {
+ probe.style.color = "";
+ probe.style.color = value;
+ return getComputedStyle(probe).color;
+ };
+ const resolvedCanvas = resolveColor("Canvas");
+ const glyphTokens = [
+ "--command-contrast",
+ "--primary-contrast",
+ "--clinical-accent-contrast",
+ "--danger-solid-contrast",
+ ].map((token) => [token, resolveColor(`var(${token})`)] as const);
+ probe.remove();
+ return {
+ canvas: resolvedCanvas,
+ buttonLabelColor: getComputedStyle(button).color,
+ tokenColors: glyphTokens,
+ };
+ });
+
+ expect(buttonLabelColor).not.toBe(canvas);
+ for (const [token, color] of tokenColors) {
+ expect(`${token}: ${color}`).not.toBe(`${token}: ${canvas}`);
+ }
+ });
+
test("dashboard remains usable at 200 percent zoom", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await mockMinimalDashboardApi(page);