From f8701a524f0eb22decc64ce1f626bdafe91751af Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 10:19:21 +0000 Subject: [PATCH 01/11] Tighten the mode-home hero rhythm and put its glyphs on the icon scale The Answer home read as a loose, unresolved stack on a phone and its glyphs looked soft. Measured in Chromium rather than judged by eye, four of those impressions were defects in code. Reserve the phone copy height once, on the pair ----------------------------------------------- ModeHomeHero reserved two line-boxes on the heading AND two on the subtitle (`max-sm:min-h-[2lh]` on each) and centred each element in its own reserve. With a one-line title, half of each element's unused reserve collected between them: at 393px the subtitle sat 27.1px from its own title while the medallion sat 6px away, so the pair that should read as one unit was spread wider than the group boundary above it. Reserving once on the wrapping group puts the slack outside the pair, which now holds its declared 4px (2.4px of rendered text gap) at every width. The reserve is also banded to the measured wrap points instead of assuming the 320px worst case everywhere. Rendering all 13 ui-copy pairs across 320-440px gives 2+2 lines below 323px, 2+1 to 411px and 1+1 from 412px, so the old flat reserve spent 20px of dead space at 393px and 49px at 430px for no stability gain. Cross-mode height is unchanged within each band, so the toggle still cannot shift the composer. Scale the medallion continuously -------------------------------- The tile stepped 48 -> 48 -> 56 at sm/lg, which served the 768-1023px tablet band the phone size while the heading beside it had already grown to 34.56px. --spacing-hero-medallion tracks the viewport the way --text-hero does. The 3rem floor is load-bearing: ui-tools.spec.ts pins the tile at exactly 48px at 390px across seven mode homes, and the floor holds through 430px. Icon quality ------------ The phone composer forced its glyphs to 1.1rem (17.6px); lucide draws on a 24-unit grid with a 2px stroke, so every stroke landed on a sub-pixel boundary and the "+" and send glyphs rendered soft. They now use --spacing-icon-lg (20px), the same size they already had from 431px up. The hero glyph, header Menu/ChevronDown/X/MessageSquarePlus, the mode pill and the popup "+" move onto size-icon-* (the hero's 28px lg step was not on the scale at all), and the two largest/smallest glyphs get size-matched strokeWidth so absolute stroke weight stops swinging 1.17px-2.33px across one screen. Privacy notice -------------- The wrapped link spent a full 48px tap box on a 16px line. The negative margin is bottom-only: split symmetrically it overhangs the line above by 8px, and because that line is 16px tall the overhang reaches its centre, so tapping the "Do not enter patient-identifiable information." sentence navigated to /privacy. Bottom-only keeps the 48px target ui-accessibility.spec.ts measures, reclaims the same 16px, and leaves the sentence inert. Governance wording and the /privacy href are untouched. That 16px is why --spacing-mode-home-composer-phone drops to 6.625rem: the settled phone composer block measures 106px, so the old 122px left an empty band. Every width now reserves exactly its natural height. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VX8tTx165iiKXz78uHb8J1 --- src/app/globals.css | 65 ++++++++++++++++++- .../master-search-header.tsx | 13 ++-- .../clinical-dashboard/mode-action-popup.tsx | 2 +- src/components/mode-home-page-skeleton.tsx | 8 ++- src/components/mode-home-template.tsx | 34 +++++----- src/components/privacy-input-notice.tsx | 16 ++++- src/lib/tailwind-merge.ts | 1 + tests/search-route-ownership.test.ts | 2 +- tests/shared-home-empty-state.dom.test.tsx | 52 +++++++++++++++ tests/ui-overlay-css-contract.test.ts | 5 +- 10 files changed, 167 insertions(+), 31 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 85b876d7c9..ce6ef3dca0 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -63,9 +63,22 @@ client host adopts them. Without this structural reserve, Therapy moved the entire action stack when hydration relocated the composer (CLS 0.248). These are layout tokens, not a second v2 colour/theme source. */ - --spacing-mode-home-composer-phone: 7.625rem; + /* Re-measured after the privacy notice stopped spending a full 48px tap box + on its own wrapped line: the settled phone composer block is 106px, so the + old 7.625rem (122px) left a 16px empty band under it. The wide value was + already exact (88px) and the notice is a single 16px line from sm up. */ + --spacing-mode-home-composer-phone: 6.625rem; --spacing-mode-home-composer-wide: 5.5rem; + /* Hero medallion. Scales continuously with the viewport the way --text-hero + does, instead of stepping 48→48→56 at sm/lg: the old steps served the + 768–1023px tablet band the phone size while the heading beside it had + already grown to 34.56px. The 3rem floor is load-bearing — it must still + resolve to exactly 48px at 390px, where ui-tools.spec.ts pins the tile + across seven mode homes — and it holds that floor through 430px. The cap + is reached at 800px. */ + --spacing-hero-medallion: clamp(3rem, 1.5rem + 4vw, 3.5rem); + /* Icon glyph size scale. Named spacing tokens so icon sizing is a documented scale (size-icon-md is the 16px default) instead of raw `h-4 w-4` literals, and so a step can carry a responsive variant: `size-icon-md sm:size-icon-lg`. @@ -878,6 +891,46 @@ summary::-webkit-details-marker { } } +/* Shared-home hero copy reserve. + * + * The mode toggle rewrites the title and subtitle in place, so the copy block + * must already be as tall as the tallest copy any of the 13 modes can produce + * or the composer below it jumps. That reserve used to sit on the heading and + * the subtitle *individually* (`min-h-[2lh]` on each), which is why the pair + * read as two unrelated lines: with a one-line title, half of each element's + * unused reserve collected between them. Measured at 393px, the heading sat + * 27.1px from its own subtitle while the medallion sat 6px away — the small + * text was further from the large text than the group boundary above it. + * + * Reserving once, on the wrapping group, keeps the pair at its declared 4px + * and moves the slack to the outside of the pair where it belongs. + * + * The bands are measured, not estimated — every title/subtitle pair in + * src/lib/ui-copy.ts rendered in this hero across 320–440px: + * ≤322px 2 title lines + 2 subtitle lines ("Which specifier fits?") + * 323–411 2 title lines + 1 subtitle line ("What explains the pattern?") + * ≥412 1 + 1 — every mode fits on one line + * A single flat reserve therefore had to assume the 320px worst case at every + * phone width, which spent 20px of dead space at 393px and 49px at 430px for + * no stability gain. tests/shared-home-empty-state.dom.test.tsx pins the band + * structure so a copy edit that pushes a mode onto another line fails loudly + * rather than silently reintroducing the jump. */ +:root { + --mode-home-copy-reserve: calc(2 * var(--text-hero) * var(--leading-display) + 0.25rem + 1.25rem); +} + +@media (max-width: 322px) { + :root { + --mode-home-copy-reserve: calc(2 * var(--text-hero) * var(--leading-display) + 0.25rem + 2 * 1.25rem); + } +} + +@media (min-width: 412px) { + :root { + --mode-home-copy-reserve: calc(var(--text-hero) * var(--leading-display) + 0.25rem + 1.25rem); + } +} + .mode-home-composer-slot { width: min(100%, clamp(19rem, 90vw, 52rem)); max-width: calc(100vw - 1rem - var(--safe-area-left) - var(--safe-area-right)); @@ -1572,10 +1625,16 @@ summary::-webkit-details-marker { min-width: 2.75rem; } + /* 1.1rem was 17.6px: lucide draws on a 24-unit grid with a 2px stroke, so a + fractional box lands every stroke on a sub-pixel boundary and the "+" and + send glyphs render visibly soft. --spacing-icon-lg is 20px — integer, on + the icon scale, and the same size these glyphs already use from 431px up. + 20px inside the 44px button leaves the pinned tap target and the dock + height this block exists to protect untouched. */ .chat-composer-icon-button svg, .chat-send-button svg { - height: 1.1rem; - width: 1.1rem; + height: var(--spacing-icon-lg); + width: var(--spacing-icon-lg); } } @media (prefers-reduced-motion: no-preference) { diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 71adb711e4..e866f210eb 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -1911,7 +1911,7 @@ export function MasterSearchHeader({ className="grid min-h-tap min-w-tap shrink-0 place-items-center rounded-full text-[color:var(--text-muted)] transition hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)] sm:h-12 sm:w-12" aria-label="Clear search question" > -