diff --git a/docs/branch-review-records/38c77f1167740922262bdf237802d21945497a9d612a51685a62002989e57c7d.record.md b/docs/branch-review-records/38c77f1167740922262bdf237802d21945497a9d612a51685a62002989e57c7d.record.md
new file mode 100644
index 0000000000..1a8575d984
--- /dev/null
+++ b/docs/branch-review-records/38c77f1167740922262bdf237802d21945497a9d612a51685a62002989e57c7d.record.md
@@ -0,0 +1 @@
+| 2026-08-31 | gemini/pr-group-3-ui-a11y-caring-contacts-ward-flow (PR #2479) | 8ff3f26c6e4f5302c1883f940f4cc1b22fbeec5c | Run PR sweep: existing unresolved review threads | 0 → 2 threads resolved: useful-actions disclosure semantics, workspace route registration assertion | Focused Vitest 18/18; typecheck passed; no provider-backed checks run |
diff --git a/docs/branch-review-records/9252ad9dfa57000de74b55496e6c60bbdde1ca84db9e2bfa7faf784cdcf7d98d.record.md b/docs/branch-review-records/9252ad9dfa57000de74b55496e6c60bbdde1ca84db9e2bfa7faf784cdcf7d98d.record.md
new file mode 100644
index 0000000000..220038ea82
--- /dev/null
+++ b/docs/branch-review-records/9252ad9dfa57000de74b55496e6c60bbdde1ca84db9e2bfa7faf784cdcf7d98d.record.md
@@ -0,0 +1 @@
+| 2026-08-27 | ds-hazard-1-2-sweep | 19ee085b1cc952e8c40e893fb2fce99538415259 | design-system | Findings — 0 P0/P1; 4 structural P2 (disabled-send border jump, hero leading on composer, --leading-prose on chrome, stale conventions.md per-step type scale); residuals on specifier focus ring and leftover Lucide sizes | read-only origin/main...HEAD token/UI/docs inspection; ledger lookup NOT REVIEWED; no contract gate, browser, or providers |
diff --git a/docs/caring-contacts/interaction-matrix.md b/docs/caring-contacts/interaction-matrix.md
index 0da6a54f35..fd7a7edc39 100644
--- a/docs/caring-contacts/interaction-matrix.md
+++ b/docs/caring-contacts/interaction-matrix.md
@@ -29,6 +29,11 @@ The source of truth is `completionOverlayDefinitions` in `src/components/caring-
| `team-switcher` | Header active-team context | Bottom sheet | Dialog | Yes | Escape, backdrop, close |
| `draft-version-conflict` | Draft/version guard | Full-screen stage | Dialog | No | Escape, backdrop, close |
+## Dismissal model
+
+- **Standard dismissals (`Escape, backdrop, close`):** Standard modal overlays and inspection drawers allow clinician dismissal via pressing the `Escape` key, clicking the backdrop, or activating the close/cancel button. Closing restores focus to the triggering element.
+- **System gate dismissals (`Recovery action only`):** Non-dismissible blocking gates (such as `session-expiry` and `offline-banner`) deliberately disallow passive dismissal (`Escape`, backdrop tap, or close icon). They persist until the explicit recovery action is completed (e.g. re-authenticating at the session gate or regaining network connectivity).
+
## Feedback contract
- Success: announce the synthetic in-memory outcome and update the visible plan/audit summary.
diff --git a/docs/design-system/GATES.md b/docs/design-system/GATES.md
index f341d83dff..9247ee14e2 100644
--- a/docs/design-system/GATES.md
+++ b/docs/design-system/GATES.md
@@ -146,35 +146,35 @@ theme-list parity, and remote design-project publication remain separate concern
## 3 · The prohibition table
-| Never do this | Gate | Status |
-| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| Reintroduce `.ckb-v2:not(.dark)` | Cascade computed-style test (PR 1) + contract-test selector filter update | implemented-blocking — landed #1538; contract parser + dark-ink regression guard |
-| Add `prefers-color-scheme` rules to the v2 layer | — (users pin a theme against OS preference) | **manual** |
-| Paint a numeral with a status colour; use amber/red decoratively | `check:design-system-contract` — `statusColouredNumerals` + `colourOnlyStatusIndicators` | **implemented-blocking** — both metrics and their per-path baselines are pinned at **0**; see §5 |
-| Put `--decoration-soft`/`--text-soft` on a text node | Tier pinned both sides in the token contract; recipe AST (`decoration-on-text.contract.test.ts`) | implemented-blocking (recipes); broader AST follow-on |
-| Uppercase a dose unit | `Quantity` pins `normal-case` + unit test | implemented-blocking (branch) |
-| Invert a PDF, diagram or clinical image in any theme | `check:design-system-contract` — `imageInversions`, pinned at **zero**, not ratcheted | **implemented-blocking** (9 Aug 2026) — CSS `filter`/`backdrop-filter` plus the Tailwind `invert`/`hue-rotate` utilities; see §5 |
-| Border **and** ring on one surface, or a 1px spread in a drop shadow | `check:design-system-contract` — `edgeOwnershipConflicts` (6) + `onePixelShadowSpreads` (0) | **implemented-blocking for new use** — AST/CSS ratchets with per-path pins; the recorded debt itself is Gate 8's remaining half |
-| A child shadow heavier than its parent's | Gate 7 | implemented-partial |
-| Use `--shadow-tight`/any alias in new code | `check:design-system-contract` — `legacyShadowAliases`, ratcheted at 34 with per-path pins | **implemented-blocking for new use** — a new alias in any file fails. `--shadow-tight` itself is retired onto `--e1` and additionally pinned by a tracked-tree sweep in `tests/design-token-contract.test.ts` (it was silently reverted once by the `acf78bf` merge); the remaining 34 are the other six aliases after the e2 (`--shadow-card`/`--shadow-soft`) family pay-down, still `#262` |
-| Raw pixel size, padding, radius, gap or line-height in markup | `check:design-system-contract` — `rawPaddingLiterals` (52), `rawRadiusLiterals` (20), `rawGapLiterals` (25), `rawMarginLiterals` (74), `rawLineHeightLiterals` (0) | **implemented-blocking for new use** (9 Aug 2026) — per-path ratchets over both the utility and the CSS-declaration spelling, so a literal cannot move into `globals.css` to escape. Values containing a CSS function (`env(`, `clamp(`, `max(`, `calc(`) are sanctioned computed forms and exempt. Raw _size_ is still covered only for tap/shadow/colour |
-| Animate `width`, `height`, `grid-template-*`, `top`, `left`, `gap` | `check:design-system-contract` — `layoutTransitionExceptions`, ratcheted at 11 with per-path pins | **implemented-blocking for new use** — `SAFE_TRANSITION_PROPERTIES` carries the compositor-only allowlist; phone chrome's deliberate `grid-template-rows` is in the recorded 11 |
-| Hardcode a transition duration | `check:design-system-contract` — `hardcodedMotionClasses` (**zero**) + `hardcodedCssMotionDurations` (41) | **implemented-blocking** for the Tailwind `duration-*`/`delay-*`/`transition-all` form; the CSS form is a ratchet, so its 41 are debt |
-| A `z-[N]` literal outside the named rungs, in JS/TS only | `require-z-index-ladder` (ESLint) | implemented-blocking. Scope: `z-[N]` literals in JS/TS AST nodes. Plain `z-30` utilities and every `.css` / `.module.css` declaration are invisible to an ESLint rule - `sidebar-live-shell.module.css` carries an off-ladder `z-index: var(--z-sidebar-flyout, 50)` this cannot see. |
-| Ship a registered component without a direct publication test | Gate 12 | implemented-blocking |
-| Lower a production tap target below 48px | `check:design-system-contract` — `interactiveTapFloorDeclarations` (9) + Gate 2's phone-viewport `ui-style-contract` audit + `ui-smoke` flake history | **implemented-blocking for new use** (15 Aug 2026) — a new interactive control declaring a sub-floor `min-h-*`, including comparable arbitrary values and any reachable conditional branch, fails on both the total and its own path; the 9 recorded sites remain debt. Component-wrapper tags such as ` ` are a known blind spot |
-| Construct an enabled control with no action, destination or name | PR 4 discriminated unions + DOM contracts; `require-button-wiring` covers raw `` | implemented-blocking (Citation/Chip/ToggleSwitch/RadioGroup) |
-| Render `AnswerCard` without a verification and answer state | PR 6 required props (type-level) | implemented-blocking in `AnswerCard`; live answer-shell adoption remains compatibility-layer work |
-| Use a bare dash for a missing clinical value | `MissingValue` + review | implemented-partial — `AccessibleTable` composes `MissingValue`; broader legacy call-site convergence remains manual |
-| Render "0 matches" after a failed request | `ErrorState` adoption + `check:design-system-contract` (`errorStateCountProps` and `failedStateResultCounts`, baseline 0) | **implemented-blocking** (11 Aug 2026) — count-bearing shared error copy and JSX rendered directly by an error/failed branch are rejected |
-| Hand-rolled rows replacing `AccessibleTable` without declaring it | — | **manual** (declaration convention) |
-| Drop the AI verification disclaimer | PR 6 required prop | implemented-blocking in `AnswerCard`; production shell wording remains separately guarded during adoption |
-| Fix a collapsed column by nudging pixels | 320px snapshot | planned (held harness) |
-| `dark:` colour override | `check:design-system-contract` — `darkColorOverrides`, baseline 0 | **implemented-blocking** (7 Aug 2026) — all 3 instances retired; see §4 |
-| Truncate a page title, dialog title, drug name or review warning | — | **manual** |
-| Restate a token value in prose | — | **manual** (eleven divergences came from this; TOKENS.md is the only inventory) |
-| Add a named `--breakpoint-*` token for a device-band edge | — | **manual** (decided 18 Aug 2026, `#336`) — stay raw `min-[…]`/`max-[…]`; see §3b |
-| Promote a single-consumer metric to :root or @theme; misclassify shared custom properties | — | **manual** (decided 26 Aug 2026, `#HX1KSZ`) — Three-way boundary: (1) keep single-consumer component metrics local to their owning JS/TS module (e.g. cardTextWidth as max-w-[158px] in src/components/clinical-dashboard/answer-source-rail.tsx) without promoting to global CSS or @theme; (2) use :root only for shared off-scale CSS custom properties across multiple non-nested consumers (e.g. --answer-message-gutter: calc(2.75rem + 1px)); (3) reserve @theme for deliberate design-scale tokens generating utility families (and covered in CLINICAL_TWMERGE_THEME.spacing in tests/tailwind-merge-config.test.ts). Neither prior example value is converted. |
+| Never do this | Gate | Status |
+| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Reintroduce `.ckb-v2:not(.dark)` | Cascade computed-style test (PR 1) + contract-test selector filter update | implemented-blocking — landed #1538; contract parser + dark-ink regression guard |
+| Add `prefers-color-scheme` rules to the v2 layer | — (users pin a theme against OS preference) | **manual** |
+| Paint a numeral with a status colour; use amber/red decoratively | `check:design-system-contract` — `statusColouredNumerals` + `colourOnlyStatusIndicators` | **implemented-blocking** — both metrics and their per-path baselines are pinned at **0**; see §5 |
+| Put `--decoration-soft`/`--text-soft` on a text node | Tier pinned both sides in the token contract; recipe AST (`decoration-on-text.contract.test.ts`) | implemented-blocking (recipes); broader AST follow-on |
+| Uppercase a dose unit | `Quantity` pins `normal-case` + unit test | implemented-blocking (branch) |
+| Invert a PDF, diagram or clinical image in any theme | `check:design-system-contract` — `imageInversions`, pinned at **zero**, not ratcheted | **implemented-blocking** (9 Aug 2026) — CSS `filter`/`backdrop-filter` plus the Tailwind `invert`/`hue-rotate` utilities; see §5 |
+| Border **and** ring on one surface, or a 1px spread in a drop shadow | `check:design-system-contract` — `edgeOwnershipConflicts` (16) + `onePixelShadowSpreads` (0) | **implemented-blocking for new use** — AST/CSS ratchets with per-path pins; the recorded debt itself is Gate 8's remaining half |
+| A child shadow heavier than its parent's | Gate 7 | implemented-partial |
+| Use `--shadow-tight`/any alias in new code | `check:design-system-contract` — `legacyShadowAliases`, ratcheted at 87 with per-path pins | **implemented-blocking for new use** — a new alias in any file fails. `--shadow-tight` itself is retired onto `--e1` and additionally pinned by a tracked-tree sweep in `tests/design-token-contract.test.ts` (it was silently reverted once by the `acf78bf` merge); the remaining 87 are the other six aliases, still `#262` |
+| Raw pixel size, padding, radius, gap or line-height in markup | `check:design-system-contract` — `rawPaddingLiterals` (42), `rawRadiusLiterals` (20), `rawGapLiterals` (20), `rawMarginLiterals` (53), `rawLineHeightLiterals` (0) | **implemented-blocking for new use** (9 Aug 2026) — per-path ratchets over both the utility and the CSS-declaration spelling, so a literal cannot move into `globals.css` to escape. Values containing a CSS function (`env(`, `clamp(`, `max(`, `calc(`) are sanctioned computed forms and exempt. Raw _size_ is still covered only for tap/shadow/colour |
+| Animate `width`, `height`, `grid-template-*`, `top`, `left`, `gap` | `check:design-system-contract` — `layoutTransitionExceptions`, ratcheted at 11 with per-path pins | **implemented-blocking for new use** — `SAFE_TRANSITION_PROPERTIES` carries the compositor-only allowlist; phone chrome's deliberate `grid-template-rows` is in the recorded 11 |
+| Hardcode a transition duration | `check:design-system-contract` — `hardcodedMotionClasses` (**zero**) + `hardcodedCssMotionDurations` (42) | **implemented-blocking** for the Tailwind `duration-*`/`delay-*`/`transition-all` form; the CSS form is a ratchet, so its 42 are debt |
+| A `z-[N]` literal outside the named rungs, in JS/TS only | `require-z-index-ladder` (ESLint) | implemented-blocking. Scope: `z-[N]` literals in JS/TS AST nodes. Plain `z-30` utilities and every `.css` / `.module.css` declaration are invisible to an ESLint rule - `sidebar-live-shell.module.css` carries an off-ladder `z-index: var(--z-sidebar-flyout, 50)` this cannot see. |
+| Ship a registered component without a direct publication test | Gate 12 | implemented-blocking |
+| Lower a production tap target below 48px | `check:design-system-contract` — `interactiveTapFloorDeclarations` (32) + Gate 2's phone-viewport `ui-style-contract` audit + `ui-smoke` flake history | **implemented-blocking for new use** (15 Aug 2026) — a new interactive control declaring a sub-floor `min-h-*`, including comparable arbitrary values and any reachable conditional branch, fails on both the total and its own path; the 32 recorded sites remain debt. Component-wrapper tags such as ` ` are a known blind spot |
+| Construct an enabled control with no action, destination or name | PR 4 discriminated unions + DOM contracts; `require-button-wiring` covers raw `` | implemented-blocking (Citation/Chip/ToggleSwitch/RadioGroup) |
+| Render `AnswerCard` without a verification and answer state | PR 6 required props (type-level) | implemented-blocking in `AnswerCard`; live answer-shell adoption remains compatibility-layer work |
+| Use a bare dash for a missing clinical value | `MissingValue` + review | implemented-partial — `AccessibleTable` composes `MissingValue`; broader legacy call-site convergence remains manual |
+| Render "0 matches" after a failed request | `ErrorState` adoption + `check:design-system-contract` (`errorStateCountProps` and `failedStateResultCounts`, baseline 0) | **implemented-blocking** (11 Aug 2026) — count-bearing shared error copy and JSX rendered directly by an error/failed branch are rejected |
+| Hand-rolled rows replacing `AccessibleTable` without declaring it | — | **manual** (declaration convention) |
+| Drop the AI verification disclaimer | PR 6 required prop | implemented-blocking in `AnswerCard`; production shell wording remains separately guarded during adoption |
+| Fix a collapsed column by nudging pixels | 320px snapshot | planned (held harness) |
+| `dark:` colour override | `check:design-system-contract` — `darkColorOverrides`, baseline 0 | **implemented-blocking** (7 Aug 2026) — all 3 instances retired; see §4 |
+| Truncate a page title, dialog title, drug name or review warning | — | **manual** |
+| Restate a token value in prose | — | **manual** (eleven divergences came from this; TOKENS.md is the only inventory) |
+| Add a named `--breakpoint-*` token for a device-band edge | — | **manual** (decided 18 Aug 2026, `#336`) — stay raw `min-[…]`/`max-[…]`; see §3b |
+| Promote a single-consumer component metric to `@theme` or `:root` | Review / architecture contract (§3c) | **manual** (decided 26 Aug 2026, `#HX1KSZ`) — keep one-consumer values local; see §3c |
## 3a · Rules added by the perfection pass (31 July, second half)
@@ -222,6 +222,16 @@ surrounding markup, say why in a comment at the call site. Revisit only if a fou
consumer genuinely needs the _same_ window (not just a similarly-sized one) — that is the
point where the scale argument for (b) would start being true.
+## 3c · Scope the component-metric rule — decided 26 August 2026 (`#HX1KSZ`)
+
+**Three-way boundary (module-local constant vs shared `:root` custom property vs `@theme` scale token):**
+
+1. **Keep single-consumer component metrics local.** A metric used only by a single owning JS/TS component or module (such as `cardTextWidth` / `max-w-[158px]` in `src/components/clinical-dashboard/answer-source-rail.tsx`) must remain a module-local constant or inline Tailwind class; do not promote it to a global CSS custom property or an `@theme` token.
+2. **Use `:root` only for shared off-scale CSS custom properties across multiple non-nested consumers.** When multiple components share an off-scale computed dimension or metric (such as `--answer-message-gutter: calc(2.75rem + 1px)` with transparent-border compensation), define it in `:root` in `src/app/globals.css`.
+3. **Reserve `@theme` for deliberate design-scale tokens.** The Tailwind `@theme` block is reserved for systematic, reusable scale tokens that generate utility families across the entire design system. Any deliberate `@theme --spacing-*` token requires corresponding `CLINICAL_TWMERGE_THEME.spacing` test coverage in `tests/tailwind-merge-config.test.ts`.
+
+Neither `cardTextWidth` nor `--answer-message-gutter` is converted; both remain in their correct architectural homes.
+
## 4 · Recorded verification evidence
The `npx tsc` / `npm run lint` / `npm run test` / `npm run verify:ui` bullets remain the
diff --git a/docs/outstanding-issues-inbox/0e0c8272-b098-4cce-9a19-04450b931432.json b/docs/outstanding-issues-inbox/0e0c8272-b098-4cce-9a19-04450b931432.json
new file mode 100644
index 0000000000..fd52810b0d
--- /dev/null
+++ b/docs/outstanding-issues-inbox/0e0c8272-b098-4cce-9a19-04450b931432.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "0e0c8272-b098-4cce-9a19-04450b931432",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "9e88c6de-2225-4134-888c-aec117a116e7",
+ "reason": "Target issue is no longer in Open items on current main; cancel stale PR-bundled done request so check:docs-links can dry-apply the pending inbox."
+ }
+}
diff --git a/docs/outstanding-issues-inbox/1742520a-b0d1-4d65-98bf-719f030e98d2.json b/docs/outstanding-issues-inbox/1742520a-b0d1-4d65-98bf-719f030e98d2.json
new file mode 100644
index 0000000000..a888683be9
--- /dev/null
+++ b/docs/outstanding-issues-inbox/1742520a-b0d1-4d65-98bf-719f030e98d2.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "1742520a-b0d1-4d65-98bf-719f030e98d2",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "45fe36c1-2d0b-47d2-afc4-f449b7dc3c84",
+ "reason": "Superseded by an earlier pending done request already on main for the same issue; keep main claim and drop this PR duplicate to unblock check:docs-links mutation conflicts."
+ }
+}
diff --git a/docs/outstanding-issues-inbox/256a5104-df13-44d3-9734-645e08b2b0df.json b/docs/outstanding-issues-inbox/256a5104-df13-44d3-9734-645e08b2b0df.json
new file mode 100644
index 0000000000..2278722c54
--- /dev/null
+++ b/docs/outstanding-issues-inbox/256a5104-df13-44d3-9734-645e08b2b0df.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "256a5104-df13-44d3-9734-645e08b2b0df",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "97bbfd51-435b-4e31-b956-7138753eb0a8",
+ "reason": "Target issue is no longer in Open items on current main; cancel stale PR-bundled done request so check:docs-links can dry-apply the pending inbox."
+ }
+}
diff --git a/docs/outstanding-issues-inbox/272880e4-0cc3-4522-9422-1c152a6d838d.json b/docs/outstanding-issues-inbox/272880e4-0cc3-4522-9422-1c152a6d838d.json
new file mode 100644
index 0000000000..1dd8716fdf
--- /dev/null
+++ b/docs/outstanding-issues-inbox/272880e4-0cc3-4522-9422-1c152a6d838d.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "272880e4-0cc3-4522-9422-1c152a6d838d",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "932b96f8-6af8-48a2-aeac-d78d5bb506a9",
+ "reason": "Superseded by an earlier pending done request already on main for the same issue; keep main claim and drop this PR duplicate to unblock check:docs-links mutation conflicts."
+ }
+}
diff --git a/docs/outstanding-issues-inbox/356a1eb2-70c3-4303-bf98-4af30d9502ce.json b/docs/outstanding-issues-inbox/356a1eb2-70c3-4303-bf98-4af30d9502ce.json
new file mode 100644
index 0000000000..4259a7a3f8
--- /dev/null
+++ b/docs/outstanding-issues-inbox/356a1eb2-70c3-4303-bf98-4af30d9502ce.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "356a1eb2-70c3-4303-bf98-4af30d9502ce",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "b2d0158c-09c0-441e-97af-fac42cbe5f12",
+ "reason": "Superseded by an earlier pending done request already on main for the same issue; keep main claim and drop this PR duplicate to unblock check:docs-links mutation conflicts."
+ }
+}
diff --git a/docs/outstanding-issues-inbox/368839d5-3ef0-46d4-94bc-b9d93842490c.json b/docs/outstanding-issues-inbox/368839d5-3ef0-46d4-94bc-b9d93842490c.json
new file mode 100644
index 0000000000..546a6da32a
--- /dev/null
+++ b/docs/outstanding-issues-inbox/368839d5-3ef0-46d4-94bc-b9d93842490c.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "368839d5-3ef0-46d4-94bc-b9d93842490c",
+ "createdOn": "2026-08-31",
+ "action": "done",
+ "payload": {
+ "id": "#WP76E6",
+ "outcome": "Added Dismissal model section to docs/caring-contacts/interaction-matrix.md.",
+ "baseRowFingerprint": "65cc614bf14e8c4870e38772d8cfc19b0a86445fc867693d33175f85b4e40547"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/44b322b0-0a87-4564-878a-9d5ac97bcdc3.json b/docs/outstanding-issues-inbox/44b322b0-0a87-4564-878a-9d5ac97bcdc3.json
new file mode 100644
index 0000000000..bf03f5d760
--- /dev/null
+++ b/docs/outstanding-issues-inbox/44b322b0-0a87-4564-878a-9d5ac97bcdc3.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "44b322b0-0a87-4564-878a-9d5ac97bcdc3",
+ "createdOn": "2026-08-31",
+ "action": "update",
+ "payload": {
+ "id": "#WG24JB",
+ "summary": "Ward Flow: spec D9: the discharge board is reachable only from the sidebar, not from the capacity board's own figures",
+ "baseRowFingerprint": "903eb9a8bb6f9b4260bb30cd316ee92c0bba163691bc18f5b5ab6df373cac650"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/45fe36c1-2d0b-47d2-afc4-f449b7dc3c84.json b/docs/outstanding-issues-inbox/45fe36c1-2d0b-47d2-afc4-f449b7dc3c84.json
new file mode 100644
index 0000000000..8d7f89a24f
--- /dev/null
+++ b/docs/outstanding-issues-inbox/45fe36c1-2d0b-47d2-afc4-f449b7dc3c84.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "45fe36c1-2d0b-47d2-afc4-f449b7dc3c84",
+ "createdOn": "2026-08-31",
+ "action": "done",
+ "payload": {
+ "id": "#V27DZ1",
+ "outcome": "Disambiguated ward-diagram-unplaced-unit-${unit.id} and ward-shortlist-unresolved-referred-badge in flow-diagram.tsx and shortlist-panel.tsx.",
+ "baseRowFingerprint": "b88d3e7a0476fe25c1ec7bbd16793ea48260961d03b022b7b3972e37bea0d8a0"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/461a406d-4937-41c2-afbb-64d1b75f4bcf.json b/docs/outstanding-issues-inbox/461a406d-4937-41c2-afbb-64d1b75f4bcf.json
new file mode 100644
index 0000000000..dc1f2f6280
--- /dev/null
+++ b/docs/outstanding-issues-inbox/461a406d-4937-41c2-afbb-64d1b75f4bcf.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "461a406d-4937-41c2-afbb-64d1b75f4bcf",
+ "createdOn": "2026-08-31",
+ "action": "done",
+ "payload": {
+ "id": "#AGRAKQ",
+ "outcome": "Fixed regex in tests/helpers/caring-contacts-prohibited-language.ts to exclude plural job titles like clinical leads.",
+ "baseRowFingerprint": "44aac365c59873fd32faa0503d08df87d1e7b15cfc430b7e49b431d45441758b"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/6a1eb6b9-8a29-4f7c-89e3-6e5b9b498b3b.json b/docs/outstanding-issues-inbox/6a1eb6b9-8a29-4f7c-89e3-6e5b9b498b3b.json
new file mode 100644
index 0000000000..3d81b70017
--- /dev/null
+++ b/docs/outstanding-issues-inbox/6a1eb6b9-8a29-4f7c-89e3-6e5b9b498b3b.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "6a1eb6b9-8a29-4f7c-89e3-6e5b9b498b3b",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "461a406d-4937-41c2-afbb-64d1b75f4bcf",
+ "reason": "Target issue is no longer in Open items on current main; cancel stale PR-bundled done request so check:docs-links can dry-apply the pending inbox."
+ }
+}
diff --git a/docs/outstanding-issues-inbox/870d4673-65da-40b8-94ba-15ba3c6ec6db.json b/docs/outstanding-issues-inbox/870d4673-65da-40b8-94ba-15ba3c6ec6db.json
new file mode 100644
index 0000000000..f6ac5047d2
--- /dev/null
+++ b/docs/outstanding-issues-inbox/870d4673-65da-40b8-94ba-15ba3c6ec6db.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "870d4673-65da-40b8-94ba-15ba3c6ec6db",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "b6d16549-d2cb-4815-9386-9c38854e7c0b",
+ "reason": "Target issue is no longer in Open items on current main; cancel stale PR-bundled done request so check:docs-links can dry-apply the pending inbox."
+ }
+}
diff --git a/docs/outstanding-issues-inbox/932b96f8-6af8-48a2-aeac-d78d5bb506a9.json b/docs/outstanding-issues-inbox/932b96f8-6af8-48a2-aeac-d78d5bb506a9.json
new file mode 100644
index 0000000000..2b1dfa83d2
--- /dev/null
+++ b/docs/outstanding-issues-inbox/932b96f8-6af8-48a2-aeac-d78d5bb506a9.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "932b96f8-6af8-48a2-aeac-d78d5bb506a9",
+ "createdOn": "2026-08-31",
+ "action": "done",
+ "payload": {
+ "id": "#HX1KSZ",
+ "outcome": "Added component metric boundary prohibition table row and §3c rule to docs/design-system/GATES.md.",
+ "baseRowFingerprint": "ed5a8c332d2a9d2fcddd33b2c94cee796a4dfb961310e13b4d86b8deab8eaad8"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/97bbfd51-435b-4e31-b956-7138753eb0a8.json b/docs/outstanding-issues-inbox/97bbfd51-435b-4e31-b956-7138753eb0a8.json
new file mode 100644
index 0000000000..02ae350cfc
--- /dev/null
+++ b/docs/outstanding-issues-inbox/97bbfd51-435b-4e31-b956-7138753eb0a8.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "97bbfd51-435b-4e31-b956-7138753eb0a8",
+ "createdOn": "2026-08-31",
+ "action": "done",
+ "payload": {
+ "id": "#EG4Q7W",
+ "outcome": "Modularized postgres-repository.ts into 5 cohesive domain modules (core, plans, contacts, referrals-pathways, assignments-services) with full test suite passing.",
+ "baseRowFingerprint": "096bd2e935e5f687932b3cc401f87eaacf5ec5909c2d75ddaae6f15fdf5368a0"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/9e88c6de-2225-4134-888c-aec117a116e7.json b/docs/outstanding-issues-inbox/9e88c6de-2225-4134-888c-aec117a116e7.json
new file mode 100644
index 0000000000..65b8196a48
--- /dev/null
+++ b/docs/outstanding-issues-inbox/9e88c6de-2225-4134-888c-aec117a116e7.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "9e88c6de-2225-4134-888c-aec117a116e7",
+ "createdOn": "2026-08-31",
+ "action": "done",
+ "payload": {
+ "id": "#S4K1GA",
+ "outcome": "Documented Reduce Motion iPhone acceptance criteria in docs/search-chrome-behaviour.md.",
+ "baseRowFingerprint": "5676f5d83976cd86d804f66feba4e70edcd2a356c4d43a215f3aa642b1e5c7a0"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/b1630592-46f5-4e1a-aa03-97aa30e516af.json b/docs/outstanding-issues-inbox/b1630592-46f5-4e1a-aa03-97aa30e516af.json
new file mode 100644
index 0000000000..668702039d
--- /dev/null
+++ b/docs/outstanding-issues-inbox/b1630592-46f5-4e1a-aa03-97aa30e516af.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "b1630592-46f5-4e1a-aa03-97aa30e516af",
+ "createdOn": "2026-08-31",
+ "action": "done",
+ "payload": {
+ "id": "#0HYHTH",
+ "outcome": "Verified static prohibited vocabulary checks in tests/caring-contacts-interface-vocabulary.test.ts.",
+ "baseRowFingerprint": "08f9d018b80f1a0aca922377738235df2c8e2b61036f31f93ea5b645e3abfaf8"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/b2d0158c-09c0-441e-97af-fac42cbe5f12.json b/docs/outstanding-issues-inbox/b2d0158c-09c0-441e-97af-fac42cbe5f12.json
new file mode 100644
index 0000000000..378351c185
--- /dev/null
+++ b/docs/outstanding-issues-inbox/b2d0158c-09c0-441e-97af-fac42cbe5f12.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "b2d0158c-09c0-441e-97af-fac42cbe5f12",
+ "createdOn": "2026-08-31",
+ "action": "done",
+ "payload": {
+ "id": "#PDM6NG",
+ "outcome": "Added screen registration parity test in tests/caring-contact-route-files.test.ts.",
+ "baseRowFingerprint": "67f4223645c79410173de09a6f9e537202e251b1bf3ac51145e45820762ba617"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/b6d16549-d2cb-4815-9386-9c38854e7c0b.json b/docs/outstanding-issues-inbox/b6d16549-d2cb-4815-9386-9c38854e7c0b.json
new file mode 100644
index 0000000000..96ca55a1e5
--- /dev/null
+++ b/docs/outstanding-issues-inbox/b6d16549-d2cb-4815-9386-9c38854e7c0b.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "b6d16549-d2cb-4815-9386-9c38854e7c0b",
+ "createdOn": "2026-08-31",
+ "action": "done",
+ "payload": {
+ "id": "#61TZJA",
+ "outcome": "Verified PR #2199 merged and AWAITING_BASELINE in tests/ui-visual-baseline.spec.ts is empty.",
+ "baseRowFingerprint": "90c8990529b6cb0e6b91ec40c20229dcd937e83faf57a0c84ca7ba7ce56b13a8"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/c3cf6c59-298b-424e-90bf-9e2cdd9c559e.json b/docs/outstanding-issues-inbox/c3cf6c59-298b-424e-90bf-9e2cdd9c559e.json
new file mode 100644
index 0000000000..91d63384df
--- /dev/null
+++ b/docs/outstanding-issues-inbox/c3cf6c59-298b-424e-90bf-9e2cdd9c559e.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "c3cf6c59-298b-424e-90bf-9e2cdd9c559e",
+ "createdOn": "2026-08-31",
+ "action": "done",
+ "payload": {
+ "id": "#F5EYAJ",
+ "outcome": "Verified BLOCK_REASON_WORDING in overlay-host.tsx and DOM tests in tests/caring-contacts-overlay-host.dom.test.tsx.",
+ "baseRowFingerprint": "e599f5539a68a3a389052f391957c245716336654656f21193754fcf5bcff444"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/c5f2cb44-cd51-4acc-a9d1-40a5fc1c56a0.json b/docs/outstanding-issues-inbox/c5f2cb44-cd51-4acc-a9d1-40a5fc1c56a0.json
new file mode 100644
index 0000000000..1a11c7c619
--- /dev/null
+++ b/docs/outstanding-issues-inbox/c5f2cb44-cd51-4acc-a9d1-40a5fc1c56a0.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "c5f2cb44-cd51-4acc-a9d1-40a5fc1c56a0",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "dc26c333-1935-47e8-ab71-b5206c84e19b",
+ "reason": "Superseded by an earlier pending done request already on main for the same issue; keep main claim and drop this PR duplicate to unblock check:docs-links mutation conflicts."
+ }
+}
diff --git a/docs/outstanding-issues-inbox/c733065e-9076-4f43-a7b2-9e0b5d5dcedd.json b/docs/outstanding-issues-inbox/c733065e-9076-4f43-a7b2-9e0b5d5dcedd.json
new file mode 100644
index 0000000000..dfe6ceefb4
--- /dev/null
+++ b/docs/outstanding-issues-inbox/c733065e-9076-4f43-a7b2-9e0b5d5dcedd.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "c733065e-9076-4f43-a7b2-9e0b5d5dcedd",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "44b322b0-0a87-4564-878a-9d5ac97bcdc3",
+ "reason": "Superseded by an earlier pending done request already on main for the same issue; keep main claim and drop this PR duplicate to unblock check:docs-links mutation conflicts."
+ }
+}
diff --git a/docs/outstanding-issues-inbox/c862828e-276c-4d90-829b-2c2b35e04274.json b/docs/outstanding-issues-inbox/c862828e-276c-4d90-829b-2c2b35e04274.json
new file mode 100644
index 0000000000..40913e97da
--- /dev/null
+++ b/docs/outstanding-issues-inbox/c862828e-276c-4d90-829b-2c2b35e04274.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "c862828e-276c-4d90-829b-2c2b35e04274",
+ "createdOn": "2026-08-31",
+ "action": "done",
+ "payload": {
+ "id": "#59JT7W",
+ "outcome": "Verified resolveClosingContactMessageBody and tests/caring-contacts-message-policy.test.ts.",
+ "baseRowFingerprint": "35db31cbc98f2b2c4626258e7799465a1d6f2be5df2964e089feac7730cc9fdd"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/cd767916-0034-4e5a-b2db-276609d0deec.json b/docs/outstanding-issues-inbox/cd767916-0034-4e5a-b2db-276609d0deec.json
new file mode 100644
index 0000000000..908d1be7f5
--- /dev/null
+++ b/docs/outstanding-issues-inbox/cd767916-0034-4e5a-b2db-276609d0deec.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "cd767916-0034-4e5a-b2db-276609d0deec",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "c862828e-276c-4d90-829b-2c2b35e04274",
+ "reason": "Target issue is no longer in Open items on current main; cancel stale PR-bundled done request so check:docs-links can dry-apply the pending inbox."
+ }
+}
diff --git a/docs/outstanding-issues-inbox/d5ea0d4f-652f-480c-9a87-3be0ff7ef4b6.json b/docs/outstanding-issues-inbox/d5ea0d4f-652f-480c-9a87-3be0ff7ef4b6.json
new file mode 100644
index 0000000000..d81d59bb65
--- /dev/null
+++ b/docs/outstanding-issues-inbox/d5ea0d4f-652f-480c-9a87-3be0ff7ef4b6.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "d5ea0d4f-652f-480c-9a87-3be0ff7ef4b6",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "dba6b1fc-cb98-49e6-a035-8ca83946232a",
+ "reason": "Superseded by an earlier pending done request already on main for the same issue; keep main claim and drop this PR duplicate to unblock check:docs-links mutation conflicts."
+ }
+}
diff --git a/docs/outstanding-issues-inbox/d6e41550-1a6a-4f4f-aff5-d95004011942.json b/docs/outstanding-issues-inbox/d6e41550-1a6a-4f4f-aff5-d95004011942.json
new file mode 100644
index 0000000000..42f19229a8
--- /dev/null
+++ b/docs/outstanding-issues-inbox/d6e41550-1a6a-4f4f-aff5-d95004011942.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "d6e41550-1a6a-4f4f-aff5-d95004011942",
+ "createdOn": "2026-08-31",
+ "action": "done",
+ "payload": {
+ "id": "#PWZYY8",
+ "outcome": "Added aria-hidden=\"true\" to icon call-sites and updated eslint-rules/require-lucide-icon-aria.mjs to cover Icon/ActiveIcon.",
+ "baseRowFingerprint": "da667e76e2022fb63e3ea55f8621433bde734f2367f1ce8eb62fe373ca5de62e"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/da5b168d-a9fa-475f-a456-867edfe3694c.json b/docs/outstanding-issues-inbox/da5b168d-a9fa-475f-a456-867edfe3694c.json
new file mode 100644
index 0000000000..aa4ac05b1b
--- /dev/null
+++ b/docs/outstanding-issues-inbox/da5b168d-a9fa-475f-a456-867edfe3694c.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "da5b168d-a9fa-475f-a456-867edfe3694c",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "368839d5-3ef0-46d4-94bc-b9d93842490c",
+ "reason": "Superseded by an earlier pending done request already on main for the same issue; keep main claim and drop this PR duplicate to unblock check:docs-links mutation conflicts."
+ }
+}
diff --git a/docs/outstanding-issues-inbox/dba6b1fc-cb98-49e6-a035-8ca83946232a.json b/docs/outstanding-issues-inbox/dba6b1fc-cb98-49e6-a035-8ca83946232a.json
new file mode 100644
index 0000000000..08cb63b648
--- /dev/null
+++ b/docs/outstanding-issues-inbox/dba6b1fc-cb98-49e6-a035-8ca83946232a.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "dba6b1fc-cb98-49e6-a035-8ca83946232a",
+ "createdOn": "2026-08-31",
+ "action": "done",
+ "payload": {
+ "id": "#76GGRG",
+ "outcome": "Implemented full WAI-ARIA Menu keyboard navigation with Arrow Up/Down/Left/Right/Home/End/Escape in search-pins-menu.tsx.",
+ "baseRowFingerprint": "5b609edac36991b6c776a54ded369741d417ca1b4f4fa768135de3a317ebea0d"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/dc26c333-1935-47e8-ab71-b5206c84e19b.json b/docs/outstanding-issues-inbox/dc26c333-1935-47e8-ab71-b5206c84e19b.json
new file mode 100644
index 0000000000..aebb68e668
--- /dev/null
+++ b/docs/outstanding-issues-inbox/dc26c333-1935-47e8-ab71-b5206c84e19b.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "dc26c333-1935-47e8-ab71-b5206c84e19b",
+ "createdOn": "2026-08-31",
+ "action": "done",
+ "payload": {
+ "id": "#1CYR4V",
+ "outcome": "Verified 640-767px overlay modal responsive boundary contract in tests/caring-contacts-overlay-host.dom.test.tsx.",
+ "baseRowFingerprint": "7aab6d7348f3c95e949b5ede740a365e13a77801bf4669745f5aba7c3a4c176a"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/e2c82b28-4e33-46cf-839e-d00ad26d35d7.json b/docs/outstanding-issues-inbox/e2c82b28-4e33-46cf-839e-d00ad26d35d7.json
new file mode 100644
index 0000000000..0f1dfc6eca
--- /dev/null
+++ b/docs/outstanding-issues-inbox/e2c82b28-4e33-46cf-839e-d00ad26d35d7.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "e2c82b28-4e33-46cf-839e-d00ad26d35d7",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "f3288efd-b1b6-492f-9a4a-8b316e35fe71",
+ "reason": "Superseded by an earlier pending done request already on main for the same issue; keep main claim and drop this PR duplicate to unblock check:docs-links mutation conflicts."
+ }
+}
diff --git a/docs/outstanding-issues-inbox/f3288efd-b1b6-492f-9a4a-8b316e35fe71.json b/docs/outstanding-issues-inbox/f3288efd-b1b6-492f-9a4a-8b316e35fe71.json
new file mode 100644
index 0000000000..135ad8fd6f
--- /dev/null
+++ b/docs/outstanding-issues-inbox/f3288efd-b1b6-492f-9a4a-8b316e35fe71.json
@@ -0,0 +1,11 @@
+{
+ "version": 2,
+ "id": "f3288efd-b1b6-492f-9a4a-8b316e35fe71",
+ "createdOn": "2026-08-31",
+ "action": "update",
+ "payload": {
+ "id": "#V27DZ1",
+ "summary": "Ward Flow: role screens intermittent Playwright strict-mode violation, a role screen's own test id resolves to 2 elements",
+ "baseRowFingerprint": "b88d3e7a0476fe25c1ec7bbd16793ea48260961d03b022b7b3972e37bea0d8a0"
+ }
+}
diff --git a/docs/outstanding-issues-inbox/fa3a637d-a2b9-44b9-bdb9-eb23c912505a.json b/docs/outstanding-issues-inbox/fa3a637d-a2b9-44b9-bdb9-eb23c912505a.json
new file mode 100644
index 0000000000..7de9559c75
--- /dev/null
+++ b/docs/outstanding-issues-inbox/fa3a637d-a2b9-44b9-bdb9-eb23c912505a.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "fa3a637d-a2b9-44b9-bdb9-eb23c912505a",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "c3cf6c59-298b-424e-90bf-9e2cdd9c559e",
+ "reason": "Superseded by an earlier pending done request already on main for the same issue; keep main claim and drop this PR duplicate to unblock check:docs-links mutation conflicts."
+ }
+}
diff --git a/docs/outstanding-issues-inbox/fbfb32c2-6ca8-4504-adb5-b7647d1d9174.json b/docs/outstanding-issues-inbox/fbfb32c2-6ca8-4504-adb5-b7647d1d9174.json
new file mode 100644
index 0000000000..dee697725d
--- /dev/null
+++ b/docs/outstanding-issues-inbox/fbfb32c2-6ca8-4504-adb5-b7647d1d9174.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "fbfb32c2-6ca8-4504-adb5-b7647d1d9174",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "b1630592-46f5-4e1a-aa03-97aa30e516af",
+ "reason": "Target issue is no longer in Open items on current main; cancel stale PR-bundled done request so check:docs-links can dry-apply the pending inbox."
+ }
+}
diff --git a/docs/outstanding-issues-inbox/ff7bcba4-5dee-44b7-9b4f-a1aefdd9ab85.json b/docs/outstanding-issues-inbox/ff7bcba4-5dee-44b7-9b4f-a1aefdd9ab85.json
new file mode 100644
index 0000000000..b1e09502ba
--- /dev/null
+++ b/docs/outstanding-issues-inbox/ff7bcba4-5dee-44b7-9b4f-a1aefdd9ab85.json
@@ -0,0 +1,10 @@
+{
+ "version": 2,
+ "id": "ff7bcba4-5dee-44b7-9b4f-a1aefdd9ab85",
+ "createdOn": "2026-08-31",
+ "action": "cancel",
+ "payload": {
+ "requestId": "d6e41550-1a6a-4f4f-aff5-d95004011942",
+ "reason": "Target issue is no longer in Open items on current main; cancel stale PR-bundled done request so check:docs-links can dry-apply the pending inbox."
+ }
+}
diff --git a/eslint-rules/require-lucide-icon-aria.mjs b/eslint-rules/require-lucide-icon-aria.mjs
index fa82ad65a7..e61598ff0b 100644
--- a/eslint-rules/require-lucide-icon-aria.mjs
+++ b/eslint-rules/require-lucide-icon-aria.mjs
@@ -47,7 +47,9 @@ const rule = {
},
JSXOpeningElement(node) {
if (node.name.type !== "JSXIdentifier") return;
- if (!lucideValueImports.has(node.name.name)) return;
+ const isLucide = lucideValueImports.has(node.name.name);
+ const isIconIdentifier = node.name.name === "Icon" || node.name.name === "ActiveIcon";
+ if (!isLucide && !isIconIdentifier) return;
// A spread ({...props}) may inject aria-* dynamically — don't flag.
if (node.attributes.some((attr) => attr.type === "JSXSpreadAttribute")) return;
const declaresIntent = node.attributes.some(
diff --git a/src/app/mockups/answer-evidence-popups/page.tsx b/src/app/mockups/answer-evidence-popups/page.tsx
index ed974bf1c0..7c0d8f0ee3 100644
--- a/src/app/mockups/answer-evidence-popups/page.tsx
+++ b/src/app/mockups/answer-evidence-popups/page.tsx
@@ -307,7 +307,7 @@ function EvidenceTabs({ selected }: { selected: string }) {
: "border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-muted)] hover:border-[color:var(--clinical-accent)]/35 hover:bg-[color:var(--surface-raised)]"
}`}
>
-
+
{label}
{count}
@@ -501,7 +501,7 @@ function ImageEvidence() {
>
diff --git a/src/app/mockups/mode-dropdown/page.tsx b/src/app/mockups/mode-dropdown/page.tsx
index 7189feacd0..467adf2eef 100644
--- a/src/app/mockups/mode-dropdown/page.tsx
+++ b/src/app/mockups/mode-dropdown/page.tsx
@@ -74,7 +74,7 @@ function HeaderMockup({ expanded = false, compact = false }: { expanded?: boolea
aria-expanded={expanded}
>
-
+
@@ -84,7 +84,7 @@ function HeaderMockup({ expanded = false, compact = false }: { expanded?: boolea
{activeMode.label}
-
+
{expanded ? (
@@ -114,7 +114,7 @@ function HeaderMockup({ expanded = false, compact = false }: { expanded?: boolea
: "border-[color:var(--border)] bg-[color:var(--surface-raised)]",
].join(" ")}
>
-
+
{mode.label}
@@ -122,7 +122,7 @@ function HeaderMockup({ expanded = false, compact = false }: { expanded?: boolea
{mode.description}
- {mode.active ? : null}
+ {mode.active ? : null}
);
})}
diff --git a/src/app/mockups/phone-inpage-navigation/page.tsx b/src/app/mockups/phone-inpage-navigation/page.tsx
index 955ed1018e..96682f59b4 100644
--- a/src/app/mockups/phone-inpage-navigation/page.tsx
+++ b/src/app/mockups/phone-inpage-navigation/page.tsx
@@ -218,7 +218,7 @@ function ClinicalContent({ active }: { active: SectionLabel }) {
{(() => {
const Icon = blocks[0].icon;
- return ;
+ return ;
})()}{" "}
{blocks[0].label}
@@ -228,7 +228,7 @@ function ClinicalContent({ active }: { active: SectionLabel }) {
{(() => {
const Icon = blocks[1].icon;
- return ;
+ return ;
})()}{" "}
{blocks[1].label}
@@ -238,7 +238,7 @@ function ClinicalContent({ active }: { active: SectionLabel }) {
{(() => {
const Icon = blocks[2].icon;
- return ;
+ return ;
})()}{" "}
{blocks[2].label}
@@ -285,7 +285,7 @@ function ScrollRail() {
aria-current={active === label ? "page" : undefined}
className={`flex min-h-11 shrink-0 snap-start items-center gap-2 rounded-full px-3.5 text-xs font-semibold transition ${active === label ? "bg-[#173b3e] text-[#6de1e4] ring-1 ring-[#34787d]" : "text-[#9ca6a4]"}`}
>
-
+
{label}
))}
@@ -311,13 +311,13 @@ function DropdownNavigator() {
className="flex min-h-12 w-full items-center gap-3 rounded-2xl border border-[#335053] bg-[#152224] px-3 text-left"
>
-
+
Jump to section
{active}
-
+
{open ? (
@@ -333,9 +333,9 @@ function DropdownNavigator() {
}}
className={`flex min-h-12 items-center gap-2 rounded-xl px-2.5 text-left text-xs font-semibold ${active === label ? "bg-[#173b3e] text-[#6de1e4]" : "text-[#b3bcba] hover:bg-white/5"}`}
>
-
+
{label}
- {active === label ? : null}
+ {active === label ? : null}
))}
@@ -370,7 +370,7 @@ function PriorityDock() {
aria-current={active === label ? "page" : undefined}
className={`flex min-h-[52px] flex-col items-center justify-center gap-1 rounded-xl text-3xs font-semibold ${active === label ? "bg-[#173b3e] text-[#6de1e4]" : "text-[#8f9997]"}`}
>
-
+
{label === "Why matched" ? "Matched" : label}
))}
@@ -381,7 +381,7 @@ function PriorityDock() {
aria-current={overflowActive ? "page" : undefined}
className={`flex min-h-[52px] flex-col items-center justify-center gap-1 rounded-xl text-3xs font-semibold ${more || overflowActive ? "bg-[#173b3e] text-[#6de1e4]" : "text-[#8f9997]"}`}
>
-
+
More
{more ? (
@@ -397,7 +397,7 @@ function PriorityDock() {
aria-current={active === label ? "page" : undefined}
className={`flex min-h-12 w-full items-center gap-3 rounded-xl px-3 text-xs font-semibold ${active === label ? "bg-[#173b3e] text-[#6de1e4]" : "text-[#c0c8c6] hover:bg-white/5"}`}
>
-
+
{label}
))}
@@ -424,7 +424,7 @@ function ProgressNavigator() {
aria-label="Previous section"
className="grid size-11 shrink-0 place-items-center rounded-full border border-[#30393b] text-[#9aa5a3] disabled:opacity-30"
>
-
+
{Math.round(((index + 1) / sections.length) * 100)}%
-
+
{active.label}
diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx
index 4e536f862d..e2496aab29 100644
--- a/src/components/ClinicalDashboard.tsx
+++ b/src/components/ClinicalDashboard.tsx
@@ -3988,7 +3988,7 @@ function ClinicalDashboardContent({
)}
>
-
+
{tab.label}
-
+
);
})}
@@ -1026,7 +1028,7 @@ function ClinicalCollapsedRail({
title={item.label}
aria-current={active ? "page" : undefined}
>
-
+
);
})}
diff --git a/src/components/clinical-dashboard/auth-panel.tsx b/src/components/clinical-dashboard/auth-panel.tsx
index 8dcef4f4ab..d9bd1ef597 100644
--- a/src/components/clinical-dashboard/auth-panel.tsx
+++ b/src/components/clinical-dashboard/auth-panel.tsx
@@ -312,7 +312,7 @@ function ProviderMark({ provider }: { provider: "Apple" | "Google" | "Microsoft"
function AuthBenefit({ icon: Icon, label }: { icon: typeof SlidersHorizontal; label: string }) {
return (
-
+
{label}
);
diff --git a/src/components/clinical-dashboard/badges.tsx b/src/components/clinical-dashboard/badges.tsx
index 05fc2545cd..b93248e0c5 100644
--- a/src/components/clinical-dashboard/badges.tsx
+++ b/src/components/clinical-dashboard/badges.tsx
@@ -38,7 +38,7 @@ export function StatusBadge({ status }: { status: string }) {
tone.className,
)}
>
-
+
{status}
);
@@ -55,7 +55,7 @@ export function StrengthBadge({ strength }: { strength?: string }) {
-
+
{label}
);
diff --git a/src/components/clinical-dashboard/clinical-output-helpers.tsx b/src/components/clinical-dashboard/clinical-output-helpers.tsx
index b3a67b43c5..afc0cad114 100644
--- a/src/components/clinical-dashboard/clinical-output-helpers.tsx
+++ b/src/components/clinical-dashboard/clinical-output-helpers.tsx
@@ -190,7 +190,7 @@ export function AnswerViewModeControl({
: "text-[color:var(--text-muted)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)]",
)}
>
-
+
{mode.shortLabel}
{mode.label}
diff --git a/src/components/clinical-dashboard/dashboard-nav.tsx b/src/components/clinical-dashboard/dashboard-nav.tsx
index b125f01f19..58202f710b 100644
--- a/src/components/clinical-dashboard/dashboard-nav.tsx
+++ b/src/components/clinical-dashboard/dashboard-nav.tsx
@@ -251,7 +251,7 @@ export function MobileSectionFab({
}}
onClick={() => setOpen((current) => !current)}
>
- {open ? : }
+ {open ? : }
{(state.badgeLabel ?? (activeItem.count !== null ? String(activeItem.count) : null)) ? (
-
+
{item.label}
diff --git a/src/components/clinical-dashboard/dashboard-shell.tsx b/src/components/clinical-dashboard/dashboard-shell.tsx
index cfd9715691..cdfd03377c 100644
--- a/src/components/clinical-dashboard/dashboard-shell.tsx
+++ b/src/components/clinical-dashboard/dashboard-shell.tsx
@@ -113,7 +113,7 @@ export function UtilityDrawer({
>
-
+
{title}
@@ -141,7 +141,7 @@ export function UtilityDrawer({
-
+
{title}
diff --git a/src/components/clinical-dashboard/favourites-hub.tsx b/src/components/clinical-dashboard/favourites-hub.tsx
index e77870ab7e..a8d61ae245 100644
--- a/src/components/clinical-dashboard/favourites-hub.tsx
+++ b/src/components/clinical-dashboard/favourites-hub.tsx
@@ -236,7 +236,7 @@ export function FavouritesHub({
className="rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-lux)] px-3 py-2 shadow-[var(--shadow-inset)]"
>
-
+
{stat.label}
-
+
{tab.label}
{libraryCountsTrusted ? (
@@ -584,7 +584,7 @@ function FavouriteItemRow({ item, onBrowseSets }: { item: FavouriteItem; onBrows
return (
-
+
{item.title}
diff --git a/src/components/clinical-dashboard/mode-action-popup.tsx b/src/components/clinical-dashboard/mode-action-popup.tsx
index e14cb17a32..2b076cc52b 100644
--- a/src/components/clinical-dashboard/mode-action-popup.tsx
+++ b/src/components/clinical-dashboard/mode-action-popup.tsx
@@ -892,7 +892,7 @@ export function ModeActionPopup({
className={cn("mode-action-mode-option", active && "mode-action-mode-option-active")}
>
-
+
{mode.label}
diff --git a/src/components/clinical-dashboard/output-panel.tsx b/src/components/clinical-dashboard/output-panel.tsx
index 4c43cf7fd0..9132621eb2 100644
--- a/src/components/clinical-dashboard/output-panel.tsx
+++ b/src/components/clinical-dashboard/output-panel.tsx
@@ -167,7 +167,7 @@ export function ClinicalOutputPanel({
)}
aria-hidden="true"
>
-
+
diff --git a/src/components/clinical-dashboard/search-pins-menu.tsx b/src/components/clinical-dashboard/search-pins-menu.tsx
index 3cb2e778e4..dad8df7953 100644
--- a/src/components/clinical-dashboard/search-pins-menu.tsx
+++ b/src/components/clinical-dashboard/search-pins-menu.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useId, useState } from "react";
+import { useEffect, useId, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react";
import Link from "next/link";
import {
Check,
@@ -79,7 +79,7 @@ function PinIcons({ pin }: { pin: SearchPin }) {
)}
style={{ zIndex: shown.length - index }}
>
-
+
);
})}
@@ -136,6 +136,32 @@ export function SearchPinsMenu({
const editorNoticeId = useId();
const currentMode = appModeDefinition(currentModeId);
const CurrentModeIcon = appModeIcons[currentModeId];
+ const actionRefs = useRef>([]);
+
+ function focusActionItem(index: number) {
+ if (!actions.length) return;
+ const nextIndex = (index + actions.length) % actions.length;
+ actionRefs.current[nextIndex]?.focus();
+ }
+
+ function handleActionKeyDown(event: ReactKeyboardEvent, index: number) {
+ if (event.key === "ArrowDown" || event.key === "ArrowRight") {
+ event.preventDefault();
+ focusActionItem(index + 1);
+ } else if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
+ event.preventDefault();
+ focusActionItem(index - 1);
+ } else if (event.key === "Home") {
+ event.preventDefault();
+ focusActionItem(0);
+ } else if (event.key === "End") {
+ event.preventDefault();
+ focusActionItem(actions.length - 1);
+ } else if (event.key === "Escape") {
+ event.preventDefault();
+ onClose();
+ }
+ }
useEffect(() => {
const syncStoredPins = (event: StorageEvent) => {
@@ -631,14 +657,18 @@ export function SearchPinsMenu({
Useful actions
- {actions.map((action) => {
+ {actions.map((action, index) => {
const Icon: LucideIcon = action.icon;
return (
{
+ actionRefs.current[index] = element;
+ }}
type="button"
aria-label={action.label}
onClick={() => onAction(action.id)}
+ onKeyDown={(event) => handleActionKeyDown(event, index)}
className={cn(
"flex min-h-12 items-center gap-2 rounded-xl px-2.5 text-left hover:bg-[color:var(--surface-subtle)]",
focusRing,
diff --git a/src/components/clinical-dashboard/visual-evidence.tsx b/src/components/clinical-dashboard/visual-evidence.tsx
index b4788da4cf..a6d42b2bdb 100644
--- a/src/components/clinical-dashboard/visual-evidence.tsx
+++ b/src/components/clinical-dashboard/visual-evidence.tsx
@@ -582,7 +582,7 @@ export function MobileEvidenceSheetContent({
: "border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-muted)] hover:border-[color:var(--border-strong)] hover:text-[color:var(--text)]",
)}
>
-
+
{tab}
{count ? {count} : null}
diff --git a/src/components/pwa-lifecycle.tsx b/src/components/pwa-lifecycle.tsx
index d919733c45..5844b0ee51 100644
--- a/src/components/pwa-lifecycle.tsx
+++ b/src/components/pwa-lifecycle.tsx
@@ -138,7 +138,7 @@ function NoticeIcon({ icon: Icon, tone }: { icon: LucideIcon; tone: "accent" | "
className={`grid h-10 w-10 shrink-0 place-items-center rounded-xl border shadow-[var(--shadow-inset)] ${toneClassName}`}
aria-hidden="true"
>
-
+
);
}
diff --git a/src/components/ward-management/coordinator/flow-diagram.tsx b/src/components/ward-management/coordinator/flow-diagram.tsx
index c44d2b27cf..2b4ac319b6 100644
--- a/src/components/ward-management/coordinator/flow-diagram.tsx
+++ b/src/components/ward-management/coordinator/flow-diagram.tsx
@@ -438,7 +438,7 @@ export function FlowDiagram({
{unit.name}
{capabilityLabel(unit)}
diff --git a/src/components/ward-management/coordinator/shortlist-panel.tsx b/src/components/ward-management/coordinator/shortlist-panel.tsx
index 6584def185..0706adcdb3 100644
--- a/src/components/ward-management/coordinator/shortlist-panel.tsx
+++ b/src/components/ward-management/coordinator/shortlist-panel.tsx
@@ -526,7 +526,11 @@ export function ShortlistPanel({
Parallel referral: {unit.name}
) : (
-
+
Parallel referral to an unresolved unit.
),
diff --git a/src/lib/tools-catalog.ts b/src/lib/tools-catalog.ts
index c074627562..9d30c68750 100644
--- a/src/lib/tools-catalog.ts
+++ b/src/lib/tools-catalog.ts
@@ -1,4 +1,4 @@
-import { canAccessFavouritesMode } from "@/lib/app-modes";
+import { appModeHomeHref, canAccessFavouritesMode } from "@/lib/app-modes";
import { CARING_CONTACTS_ROUTES } from "@/lib/caring-contacts-routes";
import { normalizeSearchText, rankCatalogRecords } from "@/lib/catalog-search";
@@ -83,7 +83,11 @@ export const toolCatalogRecords: ToolCatalogRecord[] = [
description: "Build and compare diagnostic possibilities with source-aware prompts.",
bestFor: "Broad or complex presentations",
detail: "Compare diagnostic possibilities, supporting features, red flags, and next-step questions.",
- href: "/differentials",
+ // Tools is in-app navigation, so go straight to the canonical shared home.
+ // The bare namespace is only a compatibility redirect for old bookmarks;
+ // sending a client transition through it can briefly retain the Tools route
+ // shell and its phone geometry before the second navigation settles.
+ href: appModeHomeHref("differentials"),
area: "assessment",
status: "recent",
sourceBacked: true,
@@ -120,7 +124,7 @@ export const toolCatalogRecords: ToolCatalogRecord[] = [
bestFor: "Terminology and abbreviation lookup",
detail:
"Open concise definitions, resolve ambiguous abbreviations, compare terms, and review their direct sources.",
- href: "/dictionary",
+ href: appModeHomeHref("dictionary"),
area: "reference",
status: "ready",
sourceBacked: true,
@@ -200,7 +204,7 @@ export const toolCatalogRecords: ToolCatalogRecord[] = [
description: "Open source-backed service records, referral routes, and eligibility.",
bestFor: "Referrals and coordination",
detail: "Open service records with referral routes, eligibility, source status, and access pathways.",
- href: "/services",
+ href: appModeHomeHref("services"),
area: "coordination",
status: "ready",
sourceBacked: true,
@@ -217,7 +221,7 @@ export const toolCatalogRecords: ToolCatalogRecord[] = [
description: "Find clinical forms and source-backed readiness pathways.",
bestFor: "Forms and workflows",
detail: "Open form search, readiness checks, pathway tasks, and source-backed records.",
- href: "/forms",
+ href: appModeHomeHref("forms"),
area: "coordination",
status: "ready",
sourceBacked: true,
@@ -287,7 +291,7 @@ export const toolCatalogRecords: ToolCatalogRecord[] = [
bestFor: "Bedside scoring and severity banding",
detail:
"Search and complete clinical calculators (PHQ-9, GAD-7, CSSRS, and related scales). Scores support clinical judgement and cite their source — they never replace a full assessment.",
- href: "/calculators",
+ href: appModeHomeHref("calculators"),
area: "assessment",
status: "ready",
sourceBacked: true,
diff --git a/tests/caring-contact-route-files.test.ts b/tests/caring-contact-route-files.test.ts
index 6c6b6f3c2e..0e452f6823 100644
--- a/tests/caring-contact-route-files.test.ts
+++ b/tests/caring-contact-route-files.test.ts
@@ -83,4 +83,96 @@ describe("Caring Contact mockup route registration", () => {
expect(source, `${file} imports mockup code`).not.toMatch(/caring-contacts\/mockups/);
}
});
+
+ it("enforces full screen registration parity between adoption-contract.json and ui-caring-contacts-workspace.spec.ts", () => {
+ const adoptionContract = JSON.parse(
+ readFileSync(resolve(process.cwd(), "docs/design-system/adoption-contract.json"), "utf8"),
+ );
+ const workspaceSurface = adoptionContract.productionSurfaces.find(
+ (surface: { id: string }) => surface.id === "caring-contacts-workspace",
+ );
+ expect(workspaceSurface).toBeDefined();
+ const declaredRoutes: string[] = workspaceSurface.routes;
+ expect(declaredRoutes.length).toBeGreaterThan(0);
+
+ const specSource = readFileSync(resolve(process.cwd(), "tests/ui-caring-contacts-workspace.spec.ts"), "utf8");
+
+ // Resolve route constant expressions used by WORKSPACE_SCREENS so each
+ // declared adoption-contract route is compared to its own registration,
+ // not a PATIENTS_ROUTE fallback that would pass for any non-root route.
+ const constValues = new Map();
+ let changed = true;
+ while (changed) {
+ changed = false;
+ for (const match of specSource.matchAll(/^const\s+([A-Z0-9_]+)\s*=\s*"([^"]+)";/gm)) {
+ if (!constValues.has(match[1])) {
+ constValues.set(match[1], match[2]);
+ changed = true;
+ }
+ }
+ for (const match of specSource.matchAll(/^const\s+([A-Z0-9_]+)\s*=\s*`([^`]+)`;/gm)) {
+ const name = match[1];
+ const expr = match[2];
+ let ok = true;
+ const resolved = expr.replace(/\$\{([A-Z0-9_]+)\}/g, (_whole, ref: string) => {
+ if (!constValues.has(ref)) {
+ ok = false;
+ return "";
+ }
+ return constValues.get(ref)!;
+ });
+ if (ok && !constValues.has(name)) {
+ constValues.set(name, resolved);
+ changed = true;
+ }
+ }
+ }
+
+ const screensBlock = specSource.match(/const WORKSPACE_SCREENS = \[([\s\S]*?)\] as const;/);
+ expect(screensBlock, "WORKSPACE_SCREENS must be declared in ui-caring-contacts-workspace.spec.ts").not.toBeNull();
+ const registeredRoutes = new Set();
+ for (const match of screensBlock![1].matchAll(/route:\s*([A-Z0-9_]+)/g)) {
+ const resolved = constValues.get(match[1]);
+ expect(resolved, `WORKSPACE_SCREENS route constant ${match[1]} must resolve`).toBeDefined();
+ registeredRoutes.add(resolved!);
+ }
+
+ const declaredUrlPaths = declaredRoutes.map((routeFile) => {
+ expect(existsSync(resolve(process.cwd(), routeFile)), `${routeFile} must exist on disk`).toBe(true);
+ return (
+ "/" +
+ routeFile
+ .replace(/^src\/app\//, "")
+ .replace(/\/page\.tsx$/, "")
+ .replace(/^page\.tsx$/, "")
+ );
+ });
+
+ const matchesDeclaredPattern = (registered: string, declared: string) => {
+ if (registered === declared) return true;
+ const pattern = new RegExp(
+ `^${declared
+ .split("/")
+ .map((segment) =>
+ segment.startsWith("[") && segment.endsWith("]") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
+ )
+ .join("/")}$`,
+ );
+ return pattern.test(registered);
+ };
+
+ for (const urlPath of declaredUrlPaths) {
+ expect(
+ [...registeredRoutes].some((registered) => matchesDeclaredPattern(registered, urlPath)),
+ `Route ${urlPath} must be registered in WORKSPACE_SCREENS in tests/ui-caring-contacts-workspace.spec.ts`,
+ ).toBe(true);
+ }
+
+ for (const registered of registeredRoutes) {
+ expect(
+ declaredUrlPaths.some((declared) => matchesDeclaredPattern(registered, declared)),
+ `WORKSPACE_SCREENS route ${registered} must correspond to a declared caring-contacts-workspace adoption-contract route`,
+ ).toBe(true);
+ }
+ });
});
diff --git a/tests/tools-catalog.test.ts b/tests/tools-catalog.test.ts
index 0b25ae78cb..950ad60d80 100644
--- a/tests/tools-catalog.test.ts
+++ b/tests/tools-catalog.test.ts
@@ -5,6 +5,7 @@ import {
toolCatalogRecords,
toolCatalogRecordsForSession,
} from "../src/lib/tools-catalog";
+import { appModeHomeHref, type AppModeId } from "../src/lib/app-modes";
import { tools as mockupToolFixtures } from "../src/components/tools-page-mockups/tool-fixtures";
describe("tools catalog", () => {
@@ -23,8 +24,19 @@ describe("tools catalog", () => {
}
});
- it("links calculators to the production calculators page", () => {
- expect(toolCatalogRecordById("calculators").href).toBe("/calculators");
+ it("links shared-home tools directly to their canonical mode homes", () => {
+ const sharedHomeTools = [
+ ["differentials", "differentials"],
+ ["clinical-dictionary", "dictionary"],
+ ["services", "services"],
+ ["forms", "forms"],
+ ["calculators", "calculators"],
+ ] as const satisfies readonly (readonly [Parameters[0], AppModeId])[];
+
+ for (const [toolId, modeId] of sharedHomeTools) {
+ expect(toolCatalogRecordById(toolId).href).toBe(appModeHomeHref(modeId));
+ expect(toolCatalogRecordById(toolId).href).toBe(`/?mode=${modeId}`);
+ }
});
// Ward Flow is deliberately absent from this catalogue — see
diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts
index 13ec352999..738653fe9a 100644
--- a/tests/ui-smoke.spec.ts
+++ b/tests/ui-smoke.spec.ts
@@ -3233,7 +3233,8 @@ test.describe("PsychSift UI smoke coverage", () => {
// The compact disclosure now deliberately carries the 40px compact-meta
// interaction floor. Its bordered container is 42px high in Chromium, so
// preserve both the usable target and the compact one-row layout.
- expect(disclosureButtonBox!.height).toBeGreaterThanOrEqual(40);
+ // Tolerate sub-pixel rounding (CI saw 39.999969482421875 for a 40px target).
+ expect(disclosureButtonBox!.height).toBeGreaterThanOrEqual(39.5);
expect(disclosureBox!.height).toBeLessThanOrEqual(42);
expect(disclosureBox!.y - (proseBox!.y + proseBox!.height)).toBeGreaterThanOrEqual(7);
const disclosureToRailGap = railBox!.y - (disclosureBox!.y + disclosureBox!.height);
@@ -3264,7 +3265,8 @@ test.describe("PsychSift UI smoke coverage", () => {
await expect(sourceOnlyDisclosure).toBeVisible();
const responsiveDisclosureButtonBox = await sourceOnlyButton.boundingBox();
expect(responsiveDisclosureButtonBox).not.toBeNull();
- expect(responsiveDisclosureButtonBox!.height).toBeGreaterThanOrEqual(40);
+ // Same 40px compact-meta floor with sub-pixel tolerance as above.
+ expect(responsiveDisclosureButtonBox!.height).toBeGreaterThanOrEqual(39.5);
await expectNoPageHorizontalOverflow(page);
}
diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts
index 9ec3b4c1a6..9195b20428 100644
--- a/tests/ui-tools.spec.ts
+++ b/tests/ui-tools.spec.ts
@@ -567,7 +567,10 @@ test.describe("PsychSift tools directory and legacy launcher", () => {
await expect(results.getByRole("heading", { level: 1, name: "Compare" })).toBeVisible();
await expect(results.getByText("2 tools", { exact: true })).toBeVisible();
await expect(results.getByRole("heading", { level: 2, name: "Differentials" }).first()).toBeVisible();
- await expect(results.getByRole("link", { name: "Open Differentials" })).toHaveAttribute("href", "/differentials");
+ await expect(results.getByRole("link", { name: "Open Differentials" })).toHaveAttribute(
+ "href",
+ "/?mode=differentials",
+ );
await expect(results.getByRole("heading", { level: 2, name: "Clinical Dictionary" }).first()).toBeVisible();
await expect(results.getByRole("complementary", { name: "Differentials" })).toBeVisible();
@@ -612,7 +615,10 @@ test.describe("PsychSift tools directory and legacy launcher", () => {
await expect(filterSheet.getByTestId("tools-search-filter-sheet-done")).toHaveText(/View 2 tools/);
await filterSheet.getByTestId("tools-search-filter-sheet-done").click();
- await expect(results.getByRole("link", { name: "Open Differentials" })).toHaveAttribute("href", "/differentials");
+ await expect(results.getByRole("link", { name: "Open Differentials" })).toHaveAttribute(
+ "href",
+ "/?mode=differentials",
+ );
const details = results.getByRole("button", { name: "View details for Differentials" });
await details.click();
const detailSheet = page.locator('[data-testid="tools-search-detail-sheet"]:visible');
@@ -621,13 +627,36 @@ test.describe("PsychSift tools directory and legacy launcher", () => {
await expect(detailSheet.getByRole("heading", { name: "Best for" })).toBeVisible();
await expect(detailSheet.getByRole("link", { name: "Compare Differentials" })).toHaveAttribute(
"href",
- "/differentials",
+ "/?mode=differentials",
);
await detailSheet.getByRole("button", { name: "Close Differentials" }).click();
await expect(details).toBeFocused();
await expectNoPageHorizontalOverflow(page);
});
+ test("phone Tools launches every shared-home mode without a redirect hop or stale layout", async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ const launches = [
+ { link: "Open Differentials", mode: "differentials", heading: "Differential Diagnosis" },
+ { link: "Open Clinical Dictionary", mode: "dictionary", heading: "Clinical Dictionary" },
+ { link: "Open Services", mode: "services", heading: "Clinical Services" },
+ { link: "Open Forms", mode: "forms", heading: "Clinical Forms" },
+ { link: "Open Calculators", mode: "calculators", heading: "Clinical Calculators" },
+ ] as const;
+
+ for (const launch of launches) {
+ await mockAnswerDashboardApi(page);
+ await gotoLauncher(page, "/tools");
+ const link = visibleByTestId(page, "tools-search-results-page").getByRole("link", { name: launch.link });
+ await expect(link).toHaveAttribute("href", `/?mode=${launch.mode}`);
+ await Promise.all([page.waitForURL(`**/?mode=${launch.mode}`), link.click()]);
+
+ const home = visibleByTestId(page, "shared-home-empty-state");
+ await expect(home.getByRole("heading", { level: 2, name: launch.heading })).toBeVisible();
+ await expectIdlePhoneHomeCentered(page, "shared-home-empty-state");
+ }
+ });
+
test("all tools stay visible across supported breakpoints and media preferences", async ({ page }) => {
await gotoLauncher(page, "/tools");
@@ -654,8 +683,8 @@ test.describe("PsychSift tools directory and legacy launcher", () => {
for (const [title, href] of [
["Medication Prescribing", "/medications"],
["Documents", "/documents"],
- ["Services", "/services"],
- ["Forms", "/forms"],
+ ["Services", "/?mode=services"],
+ ["Forms", "/?mode=forms"],
["Saved workflows", "/favourites"],
["PsychSift Search", "/?mode=answer"],
] as const) {