Unify the card layer behind one category-identity registry - #2060
Conversation
Icons and category colour were spread across ten independent maps and two of them disagreed, so the same tool rendered differently depending on which screen reached it. - `launcherIconById` (applications-launcher-page.tsx) carried 13 tool ids; `iconByToolId` (tools-search-results-page.tsx) carried 8 with a different fallback, so `guidelines`, `care-plans`, `safety-plan`, `calculators` and `monitoring` showed a real glyph on the launcher and a generic `Grid2X2` in search results. - Colour diverged the same way: the launcher tinted by tool area, the results page painted every tile `--type-source`, so one list read as a single purple family while the other grouped the same tools into five. - `ShieldCheck` was assigned to `guidelines`, to `risk-safety`, and to the "Source-backed" status chip — three unrelated meanings, one glyph, reachable on a single card. - `appIconTone` overrode the area map per id, routing `differentials` and `forms` to a tone key named `differentials`, so the advertised "category colour" was not a category colour. `src/lib/category-identity.ts` is now the single source of truth. It is framework-free (string glyph keys, no lucide) following the `semantic-tone.ts` precedent, so data and server modules can name a glyph without the render layer; `category-identity-icons.ts` is the only place that binds keys to components, resolving through `createElement` as `factsheets-icons.ts` does to satisfy `react-hooks/static-components`. `ToolCatalogRecord.id` is narrowed from `string` to a `ToolCatalogId` union, so `Record<ToolCatalogId, …>` cannot be under-filled: adding a tool without choosing a glyph is now a type error rather than a silent runtime fallback. `appModeIcons` keeps its name and shape but is derived rather than hand-maintained, so its "keep in sync" comment is now a property of the type. Accent delivery is `data-category-accent` → `--cat-accent`/`--cat-soft`/ `--cat-border` in globals.css rather than interpolated class names, which Tailwind's scanner cannot see, and rather than inline styles, which bypass the theme contract. Every accent aliases an existing non-semantic triad (`--type-*`, `--tone-*`), so light, dark and forced-colors need no new declarations. `risk-safety` loses its permanent danger-red tile: red asserted caution about a route rather than about a patient, spending the loudest colour in the system on a navigation target. Safety is carried by the now-unique shield glyph and by the danger-toned selected state, which is a real state. Gates: typecheck, lint, `npm run test` (643 files, 6882 passed / 4 skipped), check:design-system-contract, check:icon-scale, check:type-scale — all green. No provider-backed check was run and none is required; no RAG surface touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XGhewT2mmRoRTynjWfF1Vn
`categoryTheme()` drew two of its four category accents from the SEMANTIC
palette: Therapies on `--success-text`/`--success-bg` and Tests & procedures on
`--warning-text`/`--warning-bg`.
Those tokens carry meaning. `src/lib/semantic-tone.ts` defines six tones where
the colour IS the claim — warning means "pause, check, adjust, review", success
means a check passed — and `docs/clinical-badge-system-guide.md` states the rule
as "meaning drives the colour, never the other way round". Applying warning-amber
to an entire category of patient handouts asserted caution about content that
nothing had reviewed, and it did so on the largest surface the factsheet has: the
hero band. It also spent a colour the badge system needs, so a genuine caution
badge had to compete with its own page chrome.
All four categories now sit on non-semantic identity triads, sourced from
`FACTSHEET_CATEGORY_IDENTITY`:
Medications --clinical-accent -> --type-form
Conditions --tone-indigo -> --type-source
Therapies --success-text -> --type-service
Tests & procedures --warning-text -> --type-table
Medications moves too, for a different reason: it was the same blue as every
selection state, focus ring and evidence marker on the page, so the biggest
category was the one with no identity of its own.
`FactsheetTheme`'s shape is unchanged, so the ~20 call sites passing these as
inline style values are untouched. `FactsheetCategory` is now a re-export of the
registry's union, so the accent map and the content model cannot disagree about
what the categories are.
Three guards added to tests/design-token-contract.test.ts, each mutation-verified:
- no `[data-category-accent]` rule may reference a semantic token
(verified: pointing "table" at --warning-text fails the test)
- `categoryTheme` must stay derived via `categoryAccentVars` and return no
semantic token (verified: reinstating the old --warning-* return fails it)
- `CategoryAccent` may not declare a semantic member, which is what makes the
mistake unrepresentable at every call site rather than caught per site
Gates: typecheck, lint, `npm run test` (643 files, 6885 passed / 4 skipped),
check:design-system-contract — all green. No provider-backed check run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XGhewT2mmRoRTynjWfF1Vn
…orks Four cards that sit side by side in the same shell had drifted on every axis: ToolCard rounded-lg --shadow-card p-4 border + tint + hover lift ServiceCard rounded-xl --shadow-inset p-3 sm:p-4 border + ring/35 CalculatorCard rounded-lg --shadow-inset p-4 border + --shadow-soft Factsheet card rounded-xl --shadow-card - inline-style border-t-[3px] Two radii, three resting elevations, and four different "this one is selected" encodings, three of those expressed as fractional opacity on a token colour — which is unreviewable, because the contrast an alpha lands on depends on whatever surface happens to sit behind it in each theme. `src/components/card-recipes.ts` holds the shared definitions: `cardSurface`, `cardInteractive`, `cardSelected`, `cardAccentEdge`, `cardPadding`, and one exported `focusRing` (that string is currently redeclared as a local const in a dozen files and inline in dozens more). Recipes rather than a registered component: COMPONENTS.md §0.4 measures 157 production importers of ui-primitives.tsx against 31 product imports across the whole 54-component registry, and ledger #266 says adoption is demand-driven, "never a race to 54/54". A function also sidesteps element polymorphism — these cards are variously <article>, <button> and <Link>. A new module rather than growing ui-primitives.tsx, which §0.4 already lists as over-budget and slated to split. Two deliberate departures from what the four cards did: - `--shadow-inset` is dropped as a card elevation. It is the design-system bevel, and SPEC §4.7 says an inset well uses a border or inset shading, not both; pairing it with a border is why the services and calculator cards read flatter than the tool cards beside them. Resting is border + --e1. - The recipes name `--e1`/`--e2`/`--e3` directly rather than the `--shadow-card` / `--shadow-soft` / `--shadow-hover` role aliases. TOKENS.md schedules those for retirement "inside the recipes first", and the contract ratchet counts every use — a new consolidating module reaching for one would move the count the wrong way. Caught by the gate on the first attempt (114 -> 115); now 113, unchanged from baseline. `specifierCard` and `formulationCard` were byte-identical copies of the same string; both now name the shared recipe. This moves those two modes' cards from `--surface` to `--surface-raised`, which is the SPEC §177 correction (true-white cards against the near-white page) and is a visible, intended change. Consumers for `cardInteractive`/`cardSelected`/`cardAccentEdge` land in the following commit. Gates: typecheck, lint, `npm run test` (643 files, 6885 passed / 4 skipped), check:design-system-contract, check:knip — all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XGhewT2mmRoRTynjWfF1Vn
The launcher rendered two components for the same content — `ToolCard` and `MobileToolRow` — and they had already drifted: different resting elevation (--shadow-card vs --shadow-inset), different selected tint (/50 vs /55), and a hover lift on one but not the other. They are now one component with a `density` prop. Both test ids are kept: `application-card-*` and `application-row-*` are each asserted by ui-tools and ui-smoke, and ui-smoke is blocking at zero retries. Verified in Chromium at 390px that the row renders visible and the card hidden, with all 13 of each still in the DOM. Craft changes, launcher and tools search results: - The fake button is gone. `Details` was a <span> painted as a solid accent button INSIDE the card's own <button>: it read as a nested control, was announced as nothing, was the loudest element on the card, and — being identical on every card — distinguished nothing. The card is the control; a chevron on the decoration tier says so, taking the category accent on hover so the affordance points at the card's own family. - On the search results page the same button is real (that card is an <article> and not itself clickable), so it stays — but as `floatingControl` rather than a filled accent. Thirteen filled primaries down one list were thirteen primary actions, none of them the page's actual primary action. - Titles move from `text-base font-extrabold` to `text-lg font-semibold`. At extrabold they matched the section heading above them, so a grid of cards read as a wall of headings. Size carries the hierarchy; weight stops trying to. SPEC §4.6 puts card titles at --text-lg. - "Best for:" was a bold inline run inside the body copy, giving a label the same emphasis as the clinical text it labels. It is a kicker, so it uses the shared `eyebrowText` recipe. - The magnifier beside "Best for" on the results page is dropped: "Best for" is not a search, and it spent an accent-coloured glyph on a label. - The selected rail on a results row takes the tool's own category accent rather than the product blue, so it agrees with the tile beside it. - `min-h-[9.25rem]` and `min-h-[5.25rem]` are replaced by content height with a `min-h-tap` floor. Production tap targets stay at min-h-12. - The local `focusRing` const in both files now imports the shared one. `risk-safety` keeps a danger-toned SELECTED state via the new `cardSelectedDanger` — selection is a real state, unlike the permanent red tile retired earlier in this branch. `legacyShadowAliases` fell 113 -> 111 as the cards moved off --shadow-card and --shadow-inset onto the --e ladder. Gates: typecheck, lint, `npm run test` (643 files, 6885 passed / 4 skipped), check:design-system-contract, check:icon-scale, check:type-scale — all green. Chromium: check:playwright-browser-revision was failing on the known #255 drift (1194 installed against the pinned 1234); the pinned revision was installed, the check now reports OK, and ui-tools + ui-hydration + the ui-smoke tools assertions were run locally against it. Five ui-tools failures were investigated and are pre-existing: all four reproduce on origin/main with this work stashed (two /services/13yarn composer cases, two service-detail cases, and the tools mobile detail-sheet case), and the tools one passes when driven manually, so it is environmental rather than a regression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XGhewT2mmRoRTynjWfF1Vn
…alculators and services Factsheets - The three inline `style` objects per card are gone. An inline value cannot be remapped by the dark or forced-colors blocks, so the old cards carried their light-mode tint into both. Verified in Chromium: the four accents now remap correctly in dark, and under forced-colors they flatten to Canvas/CanvasText as they should — identity colour is decoration, and the glyph and category chip carry the meaning in high contrast. - The category browse pills take the same accents, so a pill and the cards it filters to now agree. - Card titles move to `text-lg font-semibold`, and hover tints the title with the card's own category accent rather than the product blue. Calculators - The directory tile was grey until a card opened, so a closed directory showed five domains rendered identically and the domain was findable only by reading the chip. It now carries the domain accent at rest — which is what the chip beside it has always said in words. - `CALCULATOR_DOMAIN_ACCENT` deliberately gives `risk` (suicide risk) an identity accent, not `--danger`. The label already says "Suicide risk", and an instrument is not itself a warning; a red tile on a directory row would claim urgency about a tool rather than about a patient. - Open/closed states move onto `cardSelected` and the --e ladder. Services - Adopts `cardSurface` + `cardSelected`, retiring the fourth "this one is selected" encoding (`ring-1 …/35` — an alpha on a token colour, so what it contrasted against depended on whatever surface sat behind it per theme). - The leading tile deliberately stays a RANK rather than becoming a category glyph: this is a ranked referral list, the number is what the "Best fit" pill refers to, and it doubles as the shortlist checkmark. Services has no single category axis either — records carry facets — so there would be nothing honest to put there. Ratchets moved the right way: legacyShadowAliases 111 -> 107, edge conflicts 19 -> 18. Deferred, unchanged: therapy-compass/therapy-card.tsx (own SVG icon set, own control recipes, own IconTile, and the open rawPadding/rawGap debt from #261), the differentials-home card family, and the forms detail cards — those are detail-panel compositions rather than category-bearing list items. Gates: typecheck, lint, `npm run test` (643 files, 6885 passed / 4 skipped), check:design-system-contract — all green. Chromium inspection at 390/1440 in light, dark and forced-colors against the pinned revision 1234. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XGhewT2mmRoRTynjWfF1Vn
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 31 minutes Limit details: You’ve used all 1 included review currently available under your plan. You completed 99 included PR reviews in the past 7 days; at that activity level, included reviews refill at 1 review per hour. Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (24)
Comment |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
The DSM-5 Diagnosis mode is where "a unique icon per category" stops being the right answer, so this commit does the part that is clearly correct and deliberately does not guess at the part that is not. Repeated glyph, removed - dsm-home-page.tsx gave all five "Browse categories" pills `BookOpenCheck` — the mode's own hero glyph, repeated once per category. An icon identical across every item in a set distinguishes nothing: it costs the space of an icon, competes with the label doing the actual work, and repeats the glyph already shown in the hero directly above. Without one, ModeHomeTemplate renders a tone dot, so the pills keep a leading mark. Not replaced with per-category icons, on purpose - There are 17 populated DSM-5 categories. Pictograms for diagnostic chapters are a clinical-content decision rather than a design one, and several would be actively wrong to guess at: the puzzle piece conventionally reached for on neurodevelopmental disorders is rejected by many autistic people, and any glyph chosen for the paraphilic, gender-dysphoria or sexual-dysfunction chapters risks reading as a judgement about the people those criteria describe. A wrong icon on a diagnostic class is worse than no icon. Card recipe adopted on the search surface - The results panel and the comparison aside move onto `cardSurface`, retiring `--shadow-soft` and a `--shadow-inset`-plus-border pairing (SPEC §4.7: an inset well uses a border or inset shading, not both). - The selected-row fill drops its `/55` alpha — the last of the four selected encodings this branch has been retiring. An alpha on a token colour is unreviewable because what it contrasts against depends on the row stripe behind it, which differs per theme. Rows keep no shadow of their own: they sit inside the results panel, and SPEC §4.7 forbids a child heavier than its parent, which is also why `cardSelected` is not used here. - The compare toggle moves from `rounded-xl` to `rounded-lg`, matching every other tile in the product. legacyShadowAliases 107 -> 106. Gates: typecheck, lint, `npm run test` (643 files, 6885 passed / 4 skipped), check:design-system-contract — all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XGhewT2mmRoRTynjWfF1Vn
…dc' into claude/card-review-optimize-h0pidc
The vendored DSM export gives every category a `css_class` (e.g. "gmod") and a raw hex `color`. Nothing in the app read either field and no `.gmod`/`.gpsy` CSS exists, so this looked like dead data in a file — but it was dead data on the wire. Measured against the running app before this change, `/dsm/search` served all of them in its payload: C43232 1 (Psychotic Disorders — red) 2E9952 1 (OCD & Related — green) C49A00 1 (Mood Disorders — amber) gmod 1 and zero after. `dsmCategories` is handed to `DsmSearchPage` as a prop, so the whole palette was being serialised into the page and shipped to every browser. Three of those hexes sit on the semantic palette. Had anything ever rendered them, three diagnostic categories would have worn the danger, success and warning hues `semantic-tone.ts` reserves for claims about safety — the same defect this branch removed from factsheets, where an entire category of patient handouts wore caution-amber no review had produced. Being raw hex they also could not be remapped by the dark-theme or forced-colors blocks the way a `--type-*` / `--tone-*` token is. Fixed at the two boundaries that matter: - `DsmCategory` no longer declares `css_class` or `color`, so reading `category.color` is a compile error rather than a live wire. - `dsmCategories` projects to the three fields the app uses instead of spreading the raw export. The type change alone was not enough and the new runtime guard caught that: the objects kept the keys at runtime and the payload kept the hexes. The projection is what actually removes them. The JSON itself is deliberately untouched. It is a snapshot of the upstream `dsm-5-diagnosis` repository with no generator in this repo, so editing it would diverge the snapshot from its source and be overwritten by the next export. `tests/dsm-category-colour-boundary.test.ts` holds all of it, including a check that the export still ships the colours — if upstream drops them the guard fails loudly rather than quietly passing against nothing. Also hardens the one Playwright assertion this surfaced through. A Production UI run saw the safety-plan privacy notice resolve to two identical <p> elements. Nothing renders it twice: the copy appears once in patient-safety-plan.tsx:904, the panel carries `data-safety-plan-copy` once at :870, /safety-plan mounts the component once, and 240 DOM samples across six loads through the hydration window never saw more than one. That run caught a transient second tree during navigation. The test now asserts `toHaveCount(1)` on the panel before reading text out of it, so it waits for a settled tree instead of sampling mid-swap — tightening the assertion rather than loosening it, and stating the single-panel invariant the suite previously only assumed. Gates: typecheck, lint, `npm run test` (650 files, 6996 passed / 4 skipped), check:design-system-contract, and the safety-plan and DSM Playwright specs — all green. `/dsm`, `/dsm/search` and `/dsm/search?q=` all serve 200 after the projection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XGhewT2mmRoRTynjWfF1Vn
…dc' into claude/card-review-optimize-h0pidc
Resolves the one real conflict GitHub reported (`mergeable_state: dirty`, base at dc7e518): `tests/design-token-contract.test.ts`. Both sides appended a new `describe` block to the end of the same file and the two shared a trailing `});` pair, so git could not tell them apart: - ours: "category accents stay out of the semantic palette" — the three guards keeping identity accents off the danger/warning/success/info families. - theirs: "responsive breakpoint tokens (Task #336)" — the --bp-* / --breakpoint-* assertions. There is no semantic overlap between them, so both are kept and each is closed explicitly rather than sharing a terminator. Verified by content, not by the merge exiting cleanly: the file now declares both describes, and the suite runs 40 tests where ours alone ran 39 and theirs added 1 — so neither side was silently dropped. Gates on the merged tree: typecheck, lint, `npm run test` (651 files, 7016 passed / 4 skipped). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XGhewT2mmRoRTynjWfF1Vn
`Advisory UI` failed on this PR's head with `ui-tools-search-mode-mockup.spec.ts › phone filter sheet follows the shared local-filter behavior`. Attribution was established before any fix was attempted: the spec fails under `--project=chromium-mockups` on BOTH this branch and `origin/main` (dc7e518), so it is pre-existing and not caused by the card work. It belongs in the ledger rather than inside this PR. It surfaced only now because the `ui-advisory` lane fires on `advisory_ui_changed` and had been `skipped` on every earlier run of this PR; the merge from main switched it on. It is non-blocking by design — `continue-on-error: true`, and absent from `pr-required`'s `needs:` list — and `verify:ui` excludes `@mockup`, which is why a 429-pass local run never reached it. Worth noting for whoever picks it up: the failing assertion is a filter *count* (`2 showing`), not the layout overflow I first suspected, and the mockup renders the production `ToolsSearchResultsPage` — so if the count is genuinely wrong it affects `/tools`, not just design scratch. The queued row says not to edit the expected number to match observed output without first establishing which is correct. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XGhewT2mmRoRTynjWfF1Vn
|
Overlap advisory (part of a cross-PR conflict sweep). This branch has two overlaps with other PRs:
Generated by Claude Code |
Second conflict resolution, against main at 3261272: `src/components/formulation/formulation-ui.tsx`. Main narrowed the `information-page-shell` import to `InformationPageShell` alone after removing the `InformationPageBreadcrumbs` usage from this file; this branch had added a `cardSurface` import to the same block. Both are kept, with main's narrower shell import taken — checked rather than assumed: after the merge `InformationPageBreadcrumbs` appears exactly once in the file, on the import line alone, so restoring it would have left a dead import and failed lint. Gates on the merged tree: typecheck, lint, `npm run test` (651 files, 7016 passed / 4 skipped). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XGhewT2mmRoRTynjWfF1Vn
|
Checked back on the merge-conflict risk flagged earlier against Generated by Claude Code |
…last week With the clone deepened to full history (4,922 commits back to 2026-05-19), every symbol this branch removes can finally be dated instead of guessed at. Of the 59 top-level declarations removed, 56 date to 2026-05, 06 or 07. Three did not, and all three are restored here by reverting their files to the base commit: - `pruneExpiredRetrievalLogs` (src/lib/answer-telemetry.ts), added 2026-08-21 in d745d15 "resolve 29 audit findings across clinical safety, privacy, worker, and api domains". A retrieval-log retention helper from a privacy audit is exactly the kind of thing that has no caller yet because the caller is the next step. - `therapyNeedsReviewCount` (src/lib/therapies.ts), added 2026-08-19 in adf93a7 "ship Therapy in production with its review state disclosed" (#2150). - `cardPadding` (src/components/card-recipes.ts), added 2026-08-18 in 981d85d "card review optimize" (#2060), a design-token recipe. `src/lib/therapies.ts` is reverted whole rather than surgically, so `therapyRecordExists` comes back with it; it is four lines and the file belongs to a feature that shipped two days ago. After this commit no symbol removed by this branch was introduced later than 2026-08-12, verified by re-deriving the removed-declaration list from the diff and dating each one with `git log --reverse -S`. Verification: typecheck clean, lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKbNfTZM1vzUTRsuAS4Mxv
Summary
Six commits, each independently revertible, that give the product one card layer and one category-identity registry.
18b8601— one category-identity registry. Icons and category colour were spread across ten independent maps and two of them disagreed.launcherIconById(launcher) carried 13 tool ids;iconByToolId(tools search results) carried 8 with a different fallback, soguidelines,care-plans,safety-plan,calculatorsandmonitoringshowed a real glyph on one screen and a genericGrid2X2on the other. Colour diverged the same way — the launcher tinted by tool area, the results page painted every tile--type-source.ShieldCheckwas assigned toguidelines, torisk-safety, and to the "Source-backed" chip: three unrelated meanings, one glyph, reachable on a single card.src/lib/category-identity.tsis now the single source of truth, framework-free so data and server modules can name a glyph without pulling in the render layer.ToolCatalogRecord.idis narrowed fromstringto aToolCatalogIdunion, so an unmapped tool is a type error rather than a silent runtime fallback.a5804cc— stop painting content categories in semantic colours.categoryTheme()drew two of its four factsheet accents from the semantic palette: Therapies on--success-textand Tests & procedures on--warning-text. Those tokens carry meaning — warning is "pause, check, adjust, review" — so an entire category of patient handouts wore caution-amber on its hero band without any review having produced that judgement. All four categories move to non-semantic identity triads. Three guards added, each mutation-verified.1ddb420— the shared card recipe. Four cards that sit side by side had drifted across two radii, three resting elevations, and four different "this one is selected" encodings, three of those expressed as fractional opacity on a token colour.src/components/card-recipes.tsholds the shared definitions; the two byte-identical private forks inspecifier-ui.tsxandformulation-ui.tsxnow name it.a16437e— elevate the tools cards.ToolCardandMobileToolRowwere two components rendering the same content and had already drifted; they are now one component with adensityprop, keeping both test ids. The launcher's "Details" affordance was a<span>painted as a solid accent button inside the card's own<button>— a nested control announced as nothing, identical on every card. Removed in favour of a chevron.e2154d1— DSM-5 Diagnosis. All five "Browse categories" pills on the DSM home carried the sameBookOpenCheck— the mode's own hero glyph, repeated once per category, distinguishing nothing. Removed. Deliberately not replaced with one glyph per chapter: there are 17 populated DSM-5 categories against 11 non-semantic accents, so any assignment invents relationships that do not exist, and pictograms for diagnostic chapters are a clinical-content decision rather than a design one — the puzzle piece conventionally reached for on neurodevelopmental disorders is rejected by many autistic people, and a glyph chosen for the paraphilic, gender-dysphoria or sexual-dysfunction chapters risks reading as a judgement about the people those criteria describe. The search surface adopts the card recipe and drops the last/55selected-state alpha.efed8b5— roll out to factsheets, calculators and services. Factsheets' three inlinestyleobjects per card are replaced by the shared accent attribute; calculators gain a domain accent at rest; services adopts the one selected encoding.Verification
npm run verify:pr-localUnit suite:
Test Files 643 passed (643) | Tests 6885 passed | 4 skipped (6889).npm run verify:uiwhen UI, routing, styling, browser behavior, reduced-motion, or forced-colors behavior changedcheck:playwright-browser-revisionwas failing on the known#255drift (chromium-1194 installed against the pinned 1234). The pinned revision was installed and the check now reportsOK (installed), so the gate ran against the pinned browser rather than being delegated.test-results/.last-run.jsonrecords{"status": "passed", "failedTests": []}, and the log carries 429 individual passes. Cited that way on purpose: the counted line and the run record are the evidence, not the wrapper's exit status. This run covers the six card commits at2d6e26b; the two latermainmerges that branch automation has since pushed are covered by CI's own Production UI jobs on the current head.One earlier local run of this gate is deliberately not cited as evidence. It reported
1 failed / 428 passedwhile the wrapper process reported exit 0 — the exact "exit code 0 alone is not proof" case AGENTS.md documents — andtest-results/.last-run.jsonrecorded"status": "failed". Its single failure was a Playwright strict-mode violation from two identical privacy paragraphs resolving inside[data-safety-plan-copy]on/safety-plan. That copy is rendered once inpatient-safety-plan.tsx:904, the element appears once at:870, and the route mounts the component once atsrc/app/safety-plan/page.tsx:12— so the duplication is a runtime/hydration artefact, consistent with the pre-existing hydration mismatch this app already logs on load.patient-safety-plan.tsxis not in this diff.Design-system gates, with the ratchets moving the right way rather than being held:
legacyShadowAliasesfell 113 → 106 and edge conflicts 19 → 18 as the cards moved off--shadow-card/--shadow-soft/--shadow-insetonto the--eladder.check:icon-scaleandcheck:type-scaleboth pass.check:bundle-budgetmeasured against a fresh build: production, mockups and all five per-route budgets within tolerance.Chromium inspection at 390px and 1440px in light, dark and forced-colors, on
/tools,/?mode=tools,/factsheetsand/services. Computed values confirmed the accents resolve per category (--type-servicetealrgb(43,100,116)against--type-tablegreenrgb(47,107,87)), remap correctly in dark, and flatten toCanvas/CanvasTextunder forced-colors — which the previous inlinestylevalues could not do.Five
ui-toolsfailures seen while iterating were investigated rather than assumed: all five reproduce onorigin/mainwith this work removed (two/services/13yarncomposer cases, two service-detail cases, and one tools mobile detail-sheet case). They occur only when the suite is pointed at a dev server; none of them recurs underverify:ui, which builds and serves its own isolated production bundle.Not run, and not required by the changed paths:
npm run eval:retrieval:quality,npm run eval:rag,npm run eval:quality,npm run verify:release,npm run check:supabase-project. No retrieval, ranking, selection, chunking, scoring, or answer-generation path is touched, and all of these are provider-backed.Risk and rollout
ToolCatalogRecord.id, which is compile-time. The visible risk is that four factsheet categories, five tool areas, five calculator domains and two mode card surfaces change colour; a wrong accent is cosmetic and cannot alter clinical content, ordering, or access.specifierCardandformulationCardmove from--surfaceto--surface-raised, which is the SPEC §177 correction and is a deliberate visible change.efed8b5reverts the surface adoption only;a5804ccalone restores the previous factsheet accents;18b8601is the only one with a type-level dependency, so revert it last.Clinical Governance Preflight
scripts/pr-policy.mjsclassifies this diff asclinicalRisk: false,ragRanking: false,ui: true, so this section is not required. It is completed anyway because the change alters how a patient-facing factsheet is coloured, and the point of the change is to remove a false safety signal.Clinical KB Database(sjrfecxgysukkwxsowpy)No clinical decision-support behaviour changed: this diff renders no clinical content, changes no source metadata or review status, and touches no retrieval, answer, ingestion, or document-access path. The one clinically meaningful effect is subtractive — a colour that asserted caution about a whole category of patient handouts, without any review having produced that judgement, no longer does so.
Notes
The separation this branch introduces is worth stating plainly, because it is the rule the guards enforce: a category is a family of content; a semantic tone is a claim about safety.
src/lib/semantic-tone.tsowns six tones where the colour is the claim, anddocs/clinical-badge-system-guide.mdstates it as "meaning drives the colour, never the other way round". Category accents are therefore drawn only from the non-semantic--type-*and--tone-*families, andCategoryAccenthas no semantic member — which makes the mistake unrepresentable at every call site rather than caught one site at a time.Two related decisions follow the same reasoning and are visible changes:
risk-safetyloses its permanent danger-red tile. Red there asserted caution about a route, not about a patient, and spent the loudest colour in the system on a navigation target. Safety is carried by the now-unique shield glyph and by a danger-toned selected state, which is a real state.risk(suicide risk) domain takes an identity accent rather than--danger. The label already says "Suicide risk", and an instrument is not itself a warning.Deliberately deferred, and unchanged by this branch:
therapy-compass/therapy-card.tsx(its own SVG icon set, its own control recipes, its ownIconTile, plus the openrawPaddingLiterals/rawGapLiteralsdebt recorded in ledger#261), thedifferentials-home.tsxcard family, andforms/form-detail-page.tsx— the last two are detail-panel compositions rather than category-bearing list items, so they gain least from this work.Services keeps a rank number in its leading tile rather than gaining a category glyph. It is a ranked referral list, the number is what the "Best fit" pill refers to, it doubles as the shortlist checkmark, and services records carry facets rather than a single category — so there would be nothing honest to put there.
One finding surfaced while doing the DSM work and left untouched, since it is data rather than presentation:
src/data/dsm-clinical-content.jsonships 18 per-category hex colours (#C43232red for Psychotic Disorders,#2E9952green for OCD & Related,#C49A00amber for Mood Disorders). They are typed onDsmCategory, butDsmDiagnosis.categoryisPick<DsmCategory, "key" | "label">, nothing undersrc/readscolororcss_class, and no.gmod/.gpsyCSS exists — so the palette is dead. If it were ever wired up it would reintroduce exactly the semantic-collision defecta5804ccfixes, and being raw hex it would also miss the dark and forced-colors remaps. Worth either deleting or converting to tokens in its own change.🤖 Generated with Claude Code
https://claude.ai/code/session_01XGhewT2mmRoRTynjWfF1Vn