/* static/css/tokens.css — the single source of truth for colour, type and spacing.
 *
 * WHY THIS FILE EXISTS
 * --------------------
 * Before this file, the admin panel carried ~6,800 lines of per-page inline <style>
 * containing 373 distinct hardcoded hex colours and 100 distinct font-size values.
 * Two grey ramps were in use simultaneously — Tailwind's (#6b7280, #e5e7eb, #9ca3af,
 * #d1d5db, #374151) and Bootstrap's (#6c757d, #dee2e6, #f8f9fa, #212529) — which is
 * the main reason pages "feel inconsistent" even when each one looks fine alone.
 *
 * CLAUDE.md declares Bootstrap 5 the only UI library, so the Bootstrap ramp wins and
 * the Tailwind greys are mapped onto it below.
 *
 * HOW TO USE
 * ----------
 * New and migrated CSS must use var(--…) instead of a literal hex or font-size.
 *   color: var(--text-muted);        not  color: #6b7280;
 *   font-size: var(--fs-sm);         not  font-size: .78rem;
 *   border: 1px solid var(--border); not  border: 1px solid #dee2e6;
 *
 * Do not add new tokens for one-off values — pick the nearest existing token.
 * The scale is deliberately small; that is the point.
 */

@layer tokens {
:root {
    /* ── Grey ramp (Bootstrap 5 aligned) ──────────────────────────────────
       The Tailwind greys previously in use map here as:
         #374151 → --gray-700   #6b7280 → --gray-600   #9ca3af → --gray-500
         #d1d5db → --gray-400   #e5e7eb → --gray-300

       THAT TABLE RECORDS A RENAME, NOT A VALUE EQUIVALENCE, and the difference
       has already sent one migration the wrong way. The ramp was re-spaced
       after it was written (see the note below on panels reading as one white
       sheet), so the right-hand names now carry different colours from the
       left-hand literals: #e5e7eb against --gray-300 #cfd8e3 is 22/255 on a
       channel and 1.16:1 in luminance, and #374151 against --gray-700 #495057
       is 18/255. Swapping a literal for the token beside it here CHANGES WHAT
       RENDERS.

       Value equality is the only safe licence for that swap, and prose is not
       a declaration. Read the value at its declaration below, not the name in
       this table.                                                            */
    --gray-000: #ffffff;
    /* The canvas and component edges need to remain distinguishable at 100%
       zoom. The previous four steps were so close that adjacent panels, tab
       strips and work panes read as one white sheet on ordinary displays. */
    --gray-100: #f1f4f8;
    --gray-200: #e7ecf2;
    --gray-300: #cfd8e3;
    --gray-400: #bcc8d5;
    --gray-500: #adb5bd;
    --gray-600: #6c757d;
    --gray-700: #495057;
    --gray-800: #343a40;
    --gray-900: #212529;

    /* ── Semantic surfaces & text ─────────────────────────────────────────
       Prefer these over raw greys — they carry intent, so a future re-theme
       is a change here rather than a sweep of every template.               */
    --surface:        var(--gray-000);
    --surface-sunken: var(--gray-100);
    --surface-subtle: #f7f8fa;
    /* Canonical targets for the retired surface-sheet dialect. Values remain
       literal where rem-based or near-match tokens would change rendering. */
    --page-gutter: 16px;
    --cluster-gap: 12px;
    --panel-pad: 14px;
    --panel-pad-tight: 8px;
    --action-accent: #245fc7;
    --action-accent-subtle: #eaf2ff;
    /* The accent under a pointer. 5.94:1 white-on-accent at rest and darker
       still here, so a filled primary clears AA in both states rather than
       only at rest — a hover that drops below the threshold is a control that
       becomes unreadable exactly while it is being used. */
    --action-accent-strong: #1c4da3;
    /* The faces, chosen by the portal shells (adminApp.css, providerPortal.css). The new
       look swaps both for Inter in static/css/next/tokens-next.css. */
    --font-sans: 'DM Sans Variable', system-ui, -apple-system, 'Segoe UI', sans-serif;
    --font-display: 'Manrope Variable', var(--font-sans);
    --content-text: #1f2933;
    --content-text-subtle: #67707a;
    --indicator-success: #16a34a;
    --indicator-warning: #ca8a04;
    --indicator-danger: #dc2626;
    --indicator-success-bg: #eef8f1;
    --indicator-warning-bg: #fff8e6;
    --indicator-danger-bg: #fdf0f0;
    /* Exact-value retirement targets. These roles preserve the rendered
       values of the final legacy callers while moving ownership here. */
    --indicator-danger-deep: #b91c1c;
    --preview-disabled: #cfd3d6;
    --content-text-strong: #111;
    --document-canvas: #525659;
    --sheet-backdrop: rgba(15, 23, 42, .48);
    --drawer-backdrop: rgba(0, 0, 0, .5);
    --field-nav-backdrop: rgba(17, 17, 17, .82);
    --print-ink: #000;
    --schedule-divider: #dee2e6;
    --nav-hover-subtle: rgba(255, 255, 255, .12);
    --nav-fg-dim: rgba(255, 255, 255, .7);
    --nav-fg-disabled: rgba(255, 255, 255, .6);
    --nav-fg-quiet: rgba(255, 255, 255, .4);
    --nav-fg-emphasis: rgba(255, 255, 255, .9);
    --nav-divider: rgba(255, 255, 255, .3);
    --nav-divider-subtle: rgba(255, 255, 255, .18);
    --month-hover-bg: #f0f4ff;
    --month-selected-bg: #2563eb;
    --signature-applied-bg: rgba(220, 252, 231, .7);
    --signature-applied-border: #22c55e;
    --field-success: #86efac;
    --shadow-calendar-hover: 0 2px 8px rgb(31 41 51 / .08);
    --shadow-panel-strong: 0 1px 2px rgba(31, 50, 81, .05);
    --shadow-drag: 0 4px 12px rgba(0, 0, 0, .3);
    --drawer-width: 280px;
    --table-control-border: #e5e7eb;
    --table-hover-bg: #f3f7fd;
    --filter-panel-bg: #f9fafb;
    --table-focus-ring: 0 0 0 2px rgba(66, 155, 223, .15);
    --table-head-sticky-bg: #f8fafc;
    --table-head-dense-bg: #f6f8fb;
    --table-border: #e1e6ec;
    --table-filter-label: #374151;
    --table-row-hover: rgba(66, 155, 223, .08);
    --scroll-cue-background:
        linear-gradient(to right, #fff 30%, rgba(255,255,255,0)) left center,
        linear-gradient(to left, #fff 30%, rgba(255,255,255,0)) right center,
        radial-gradient(farthest-side at 0 50%, rgba(0,0,0,.16), transparent) left center,
        radial-gradient(farthest-side at 100% 50%, rgba(0,0,0,.16), transparent) right center;
    /* One adapter joins Home Doctors surface roles to Bootstrap controls.
       Context changes the custom property; controls never depend on the
       non-inherited background-color property of an ancestor. */
    --control-surface-default:    var(--surface);
    --control-surface-contextual: var(--surface-sunken);
    --control-surface-disabled:   var(--surface-sunken);
    --control-shadow-inset:       inset 0 1px 1px rgba(31, 41, 51, .025);
    --bs-body-bg:                 var(--surface);
    --bs-body-color:              var(--text);
    --bs-border-color:            var(--border);
    --bs-secondary-bg:            var(--surface-sunken);
    /* Divider inside a list — lighter than --border, which edges a panel.
       Was hardcoded as #f0f0f0 in four component definitions. */
    --divider: #e3e9ef;
    /* "This row is open." The one selection tint; was #e8f0fe in four places. */
    --selected: #edf3ff;

    --border:         var(--gray-300);
    --border-strong:  var(--gray-400);

    /* Both muted tiers sit one step darker than they read naturally, and the
       reason is contrast, not taste. `--text-subtle` was --gray-500 (#adb5bd),
       which is 2.07:1 on --surface — WCAG AA needs 4.5:1 for normal text, and
       all 119 of its uses were `color:` on real content (empty-state messages,
       <dt> field labels), never decoration. Moving it to --gray-600 clears AA
       at 4.69:1; --text-muted moved with it, to --gray-700 (8.18:1), because 22
       files use BOTH tiers and pointing subtle at muted's old value would have
       flattened a distinction they rely on. Do not lighten these back.
       Pinned by scripts/check_contrast.py. */
    --text:        var(--gray-900);
    --text-muted:  var(--gray-700);
    --text-subtle: var(--gray-600);
    --text-invert: var(--gray-000);
    /* --text-disabled: the light grey for a control whose state RESTING, not
       an error — the "not yet" pill in pfNotes. Lighter than --text-subtle
       because it never carries a load-bearing word: the pill next to it says
       the state in text, and colour is redundant decoration (see Stage.tsx). */
    --text-disabled: #c4c8cc;

    /* ── INVERSE SURFACES: dark chrome, and the text and edges that sit on it ──
       WHY THESE EXIST. `--text-invert` shipped without the surface it inverts
       AGAINST, so a component needing dark chrome — the HDSign forms-editor
       action bar, the CCM document-viewer pane, the gem-shop stage — had a
       foreground token and no background or border token, and reached into the
       grey ramp for both. That is how a light-surface text token ended up on a
       near-black background: measured 2026-09-27, `--text-subtle` on --gray-800
       is 2.45:1 and on --gray-900 is 3.29:1, against the 4.5:1 WCAG AA needs for
       normal text. `scripts/check_contrast.py` could not see it, and says so in
       its own label: it gates ONE surface, `--surface`.

       THREE DARK LEVELS, because three are in use — one name each rather than
       three raw ramp references nobody can audit. Measured 2026-09-27:

         foreground      on inverse  on raised  on backdrop
         --gray-500        7.43:1      5.55:1      3.94:1     border only
         --gray-400        9.08:1      6.77:1      4.81:1     <- clears AA on all
         --gray-300       10.71:1      7.99:1      5.68:1
         --text-invert    15.43:1     11.51:1      8.18:1

       So --text-inverse-muted is --gray-400: the DIMMEST step that clears 4.5:1
       on every surface in this family, including the lightest. --gray-500 would
       have been dimmer and fails on the backdrop at 3.94:1, which is the trap
       this whole block exists to close — a token that passes on the surface you
       tested and fails on the one you did not. --border-inverse stays --gray-500
       because a control boundary needs 3:1, not 4.5:1, and 3.94:1 clears it.

       The old border was --gray-700: 1.89:1 on inverse and 1.41:1 on raised,
       well under 3:1, so the forms-editor name input had an edge nobody saw. */
    --surface-inverse:        var(--gray-900);
    --surface-inverse-raised: var(--gray-800);
    --surface-backdrop:       var(--gray-700);
    --border-inverse:         var(--gray-500);
    --text-inverse-muted:     var(--gray-400);

    /* A neutral fill one step stronger than --surface-sunken: a row hover, a
       palette item hover, a progress groove. Five call sites reached for
       --gray-200 directly because this had no name. NOT --divider, which is a
       LINE colour at a near-identical value — painting a fill with a line token
       is the same category error as painting a background with --text-muted. */
    --surface-hover: var(--gray-200);
    /* Canvas ink: the near-black a <canvas> strokes when it paints a rendering
       no stylesheet reaches (the provider profile typed-name card and the
       signing pad). Minted at the literal it replaces and read through the
       semantic palette adapter so the two cannot drift (R3, #194). */
    --ink: #1a1a1a;
    /* Mask stop ink: pure black for the opaque stops of a horizontal fade
       mask (the monitoring tab strips scroll-snap edge). A mask composites
       alpha, not colour, so black is the full-opacity stop and any opaque
       colour would render identically; minted at the literal it replaces in
       case a later mask wants the same stop (R1, #186). */
    --mask-ink: #000;

    /* ── Navy chrome (admin rail & shell band) ────────────────────────────
       The light-on-navy foreground, emphasis and hover treatments for the
       dark chrome frame: the admin left rail (`.admin-rail-surface`) and the
       band the shell tops its content with (`.admin-tab-strip`). Declared
       once, here: surfaces.css's `.app-nav-surface` restates its own values
       for the nav panel, and the admin shell consumes the shared tokens for
       its band.
       `--nav-fg` is white at .85 so a row reads as resting, never shouting;
       `--nav-fg-strong` brightens a hover/selection; `--nav-hover` fills the
       row under the cursor. Not one-off: it is the opposite half of the navy
       frame, so it gets a name.
       Contrast: white charged to rgba .85 on `--pf-dark-blue` reads ~12:1. */
    --nav-fg:        rgba(255, 255, 255, .85);
    --nav-fg-strong: var(--text-invert);
    --nav-hover:     rgba(255, 255, 255, .15);
    --nav-active:    rgba(255, 255, 255, .25);

    /* ── Status (Bootstrap semantic colours) ──────────────────────────────
       These drive TEXT. The one place status is colour-coded as a band is
       `.status-ribbon` in surfaces.css, which carries its own tinted palette
       and its own justification — it does not use these.                    */
    --status-primary: #0d6efd;
    --status-success: #198754;
    --status-danger:  #dc3545;
    --status-warning: #ffc107;
    --status-info:    #0dcaf0;
    /* Warning deep/text: the darker ambers the sheets use when the yellow
       `--status-warning` band reads too weak on a light text run. Minted at
       the exact literals they replace, so a swap is pixel-preserving.      */
    --status-warning-deep: #b8860b;
    --status-warning-text: #856404;
    --status-progress: #fd7e14;
    /* Ribbon tint pairs, the `.status-ribbon` band in surfaces.css. That sheet
       carried these as raw hex; minted at the exact literals they replace, so
       a swap is pixel-preserving (R1, #209). `--status-*-bg` is the tint,
       `--status-*-ink` the text on it. `wait` and `dead` share a tint
       deliberately and keep separate names so their inks can differ.
       `--status-applied-ink` is the second green (rgb 21,128,61 against done's
       22,101,52), and it is a separate token on purpose: collapsing two greens
       for one concept would move a pixel with no visual pass to confirm it, so
       convergence is the design owner's decision, not a migration side effect
       (open question #209, §8). Its name follows `.sig-field.applied`, the site
       where the value actually lives. */
    --status-act-bg:   #dbeafe;
    --status-act-ink:  #1e40af;
    --status-work-bg:  #fef3c7;
    --status-work-ink: #92400e;
    --status-stop-bg:  #fee2e2;
    --status-stop-ink: #991b1b;
    --status-wait-bg:  #f3f4f6;
    --status-wait-ink: #4b5563;
    --status-done-bg:  #dcfce7;
    --status-done-ink: #166534;
    --status-dead-bg:  #f3f4f6;
    --status-dead-ink: #646c78;  /* darkened #6b7280 -> 4.82:1 on #f3f4f6 (was 4.39, AA fail) */
    --status-applied-ink: #15803d;
    /* The ".st-standin" warn surface on the settings page. These three were
       only ever referenced through var() fallbacks — `var(--warn-border, …)` —
       and the literal fallback was the value that rendered, so naming took the
       fallback's exact value and let the fallback be dropped (R2, #168). */
    --warn-border: #f0c36d;
    --warn-bg:     #fdf6e3;
    --warn-fg:     #7a5b00;

    /* ── Chart roles (the Monitoring kit, SCRUM-1779) ─────────────────────
       Every chart colour on the Monitoring page is a ROLE, never a hex in a
       tab file: CSS reads `var(--viz-*)`, ECharts reads `vizRoles()` from
       `components/palette/chart.ts`, whose fallbacks `palette/chart.test.ts`
       pins to these lines. Values and contrast are the accepted reference
       (plans/monitoring/reference/spec/KIT.md section C1, appendices A and B).

       data    every mark by default (9.1:1 on white)
       focus   the one mark the answer names; the action accent
       stop    came back wrong or stopped; red is news, never "low"
       soft    context marks, 2.5:1, so always labelled or twinned
       faint   ghosts and the large remainder of a share
       band    the usual range; hatch is "not measured here", one meaning
       seq     heat cells light to dark; cat-* only when categories are the
               point, at most five plus other, fixed by entity
       ink, mute  chart text: values and names, ticks and definitions
       warn    Live's stall amber only (cat-2 mixed 70% into ink); no other
               amber state on the page                                       */
    --viz-ink:  #1a2236;
    --viz-mute: #5f6b7d;
    --viz-data:  #3b4866;
    --viz-soft:  #9aa5b8;
    --viz-faint: #d5dbe5;
    --viz-focus:        var(--action-accent);
    --viz-focus-strong: var(--action-accent-strong);
    --viz-focus-subtle: var(--action-accent-subtle);
    --viz-stop:      #a61b29;
    --viz-stop-soft: #f2c6cc;
    --viz-warn:      #906022;
    --viz-band:   #edf1f6;
    --viz-grid:   #eef1f5;
    --viz-axis:   #cfd8e3;
    --viz-closed: #f7f8fa;
    --viz-hatch:  #b3bdcc;
    --viz-water:  #e6edf6;
    --viz-seq-1: #d6e3f7;
    --viz-seq-2: #8fb2e9;
    --viz-seq-3: #2f66c4;
    --viz-seq-4: #163a7c;
    --viz-cat-1: #2e4a9a;
    --viz-cat-2: #c27a1a;
    --viz-cat-3: #18876c;
    --viz-cat-4: #8257b8;
    --viz-cat-5: #4a9bd6;
    --viz-cat-other: #aab4c3;
    --viz-benchmark: #8f99aa;
    --viz-tip:      var(--viz-ink);
    --viz-tip-link: #9cc0ff;

    /* ── Provider gems & shop chips (light-on-dark patient context) ────────
       The gems and gacha chips paint white-on-dark overlays that sit on top of
       a dark patient-banner surface, not on a page background — so they mint
       their own white-with-alpha fills and the two rarity accent colours, at
       the exact literals they replace (R1 residue, #194). */
    --chip-bg:             rgba(255, 255, 255, .10);
    --chip-border:         rgba(255, 255, 255, .28);
    --chip-hover-bg:       rgba(255, 255, 255, .18);
    --chip-streak-divider: rgba(255, 255, 255, .25);
    --chip-gem:            #7dd3fc;
    --gacha-rarity-4:      #2f6fbd;
    --gacha-rarity-5:      #7b4ba3;

    /* ── Practice Fusion brand ────────────────────────────────────────────
       Restricted to PF integration pages — see standards.d/design-tokens.sh.    */
    --pf-blue:        #3B67AC;
    --pf-bright-blue: #429BDF;
    --pf-dark-blue:   #252F61;
    --pf-light-blue:  #77B8E3;
    --pf-light-teal:  #ECF6FC;
    --pf-light-gray:  #f2f2f3;
    --pf-teal:        #63BFBD;
    /* Hover state for --pf-teal. A token rather than a one-off literal because
       a hover shade is part of the brand pair — leaving it hardcoded is how the
       two drift apart when the base colour is re-themed. */
    --pf-teal-dark:   #52a9a7;
    --pf-orange:      #EC6B31;
    --pf-gray:        #707070;

    /* ── Type scale ───────────────────────────────────────────────────────
       Compact portal shells opt into the 12px root below, so the rem values
       resolve to the px shown in the comments. This replaces the
       previous sprawl of .68/.72/.75/.78/.8/.82rem + 9/10/11/12/13px.       */
    --fs-2xs:  0.750rem;   /*  9px — dense table meta, timestamps           */
    --fs-calendar-compact: 0.583rem; /* 7px — full status words in 7-col phone grids */
    --fs-xs:   0.833rem;   /* 10px — secondary labels                       */
    --fs-sm:   0.917rem;   /* 11px — table body, form help                  */
    --fs-base: 1.000rem;   /* 12px — default body text                      */
    --fs-md:   1.083rem;   /* 13px — emphasised body                        */
    --fs-lg:   1.167rem;   /* 14px — card titles, section headers           */
    --fs-xl:   1.333rem;   /* 16px — page headings (h5)                     */
    --fs-2xl:  1.667rem;   /* 20px — page title (h4)                        */

    --fw-normal:   400;
    --fw-medium:   500;
    --fw-semibold: 600;

    --lh-tight: 1.25;
    --lh-base:  1.5;

    /* ── Spacing ──────────────────────────────────────────────────────────
       4px-based; matches Bootstrap's spacer steps closely enough to mix.    */
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-5: 1.5rem;
    --space-6: 2rem;

    /* ── Composition: the distances a page HAS, named by what they mean ────
       `--space-*` is the scale. These are the four relationships, and they are
       what a caller of the layout primitives names: `gap="group"`, not `8px`.

       Measured 2026-09-28: the 85 private page stylesheets carried 1,760 raw px
       literals across 116 distinct values, and the top nine were 8px ×154,
       6px ×124, 4px ×118, 10px ×90, 12px ×84, 3px ×66, 14px ×44, 5px ×37,
       7px ×32 — nine gaps for four relationships, because there was no word for
       any of them. `frontend/src/components/layout/Layout.tsx` is the door that
       makes the word cheaper to write than the number.

       `--space-5` and `--space-6` deliberately have no relationship name. A
       distance that large is the page frame's own decision, not a statement
       about how two things relate. */
    --gap-tight:   var(--space-1);   /* parts of one thing: icon and label   */
    --gap-group:   var(--space-2);   /* things that act together: a toolbar  */
    --gap-section: var(--space-3);   /* parts of a page about one subject    */
    --gap-frame:   var(--space-4);   /* between the page's major regions     */

    /* THE PAGE FRAME: how far every portal page sits from the shell around it.
       One answer for every page. Measured 2026-09-28 over the 64 admin routes,
       the first painted box sat 8px from the rail on the `.page-shell` pages
       and 42px on the `.task-workspace-page` ones — two insets for one shell,
       which is what made moving between Payers and Billing feel like changing
       applications. These are the scale's two frame-sized steps, the ones this
       file keeps for exactly this decision; in rem, so the provider portal's
       16px root gives it the roomier frame its density promises.
       On a phone `static/css/mobile.css` re-points both at smaller steps:
       layout keys on width, and that file is where width lives. */
    --page-inset-block:  var(--space-5);   /* 18px admin · 24px standard */
    --page-inset-inline: var(--space-6);   /* 24px admin · 32px standard */
    /* Between the band and the content, and between any two regions of a
       page: the page root's `gap`, so no band carries a margin of its own. */
    --page-gap:          var(--gap-frame);

    /* The reading measure: the widest a column of prose or a form may be.
       Characters, not pixels, so it tracks the font rather than the screen and
       stays correct under both density roots. */
    --measure: 68ch;
    /* The narrowest an auto-fit grid column may be before the grid drops one.
       In `rem`, so it moves with the density root; a px value here would opt
       every grid out of the two-root contract (SCRUM-1453). */
    --grid-min: 18rem;

    /* ── Radius & elevation ───────────────────────────────────────────────*/
    /* Three levels, used by shape rather than by page:
       compact marks · controls/inner groups · cards/sheets/page surfaces. */
    --radius-sm: 4px;
    --radius:    8px;
    --radius-lg: 12px;
    /* A control's own corner, between the compact mark and the card. Named
       separately from `--radius-sm` because it is a decision about CONTROLS —
       every button, input and select rounds by the same amount, and a re-theme
       changes that here without touching a card or a badge. */
    --radius-control: 6px;

    --shadow-sm: 0 1px 2px rgba(31, 41, 51, 0.06), 0 1px 1px rgba(31, 41, 51, 0.03);
    --shadow:    0 4px 12px rgba(31, 41, 51, 0.08), 0 1px 3px rgba(31, 41, 51, 0.05);
    --shadow-lg: 0 18px 48px rgba(15, 23, 42, 0.18), 0 4px 12px rgba(15, 23, 42, 0.08);
    /* Hover and floating-panel shadows, minted at the exact literals they
       replace so the swap is pixel-preserving (R1 residue): a card lift on
       hover (".ad-card:hover"), a soft standing-panel edge (".ptd-panel"),
       a tooltip shadow (".flow-tooltip"), and the monitoring metrics edge
       (".mon-metrics"). Kept at their original values; they are page-accents,
       not a new elevation ladder. --shadow-panel (.045 alpha) and
       --shadow-metrics (.04 alpha) are deliberately separate: the two boxes
       render at different alphas, and ADR 0009 allows a mint only at the
       exact value that renders today. */
    --shadow-card-hover: 0 3px 10px rgb(15 23 42 / 8%);
    --shadow-panel:      0 1px 2px rgba(31, 50, 81, .045);
    --shadow-tooltip:    0 2px 6px rgba(0, 0, 0, .12);
    --shadow-metrics:    0 1px 2px rgba(31, 50, 81, .04);

    /* ── Touch targets ────────────────────────────────────────────────────
       44px is THE floor, and it is one number. Apple HIG 44pt and WCAG 2.5.5
       agree on it, and the coarse-pointer block at the end of this file points
       `--control-height` here so a control cannot be floored at a second
       "44px" that somebody later edits alone.

       It applies under COARSE POINTER, not under a narrow viewport: a touch
       laptop at 1440px needs finger targets and a zoomed desktop does not.
       See the density-layer note at the end of this file.                    */
    --touch-target: 44px;

    /* ── Focus ────────────────────────────────────────────────────────────
       ONE focus treatment for every control. Before these, focus was spelled
       per component — `outline: 2px solid var(--status-primary)` in
       `disclosure.css`, a box-shadow ring in the table, and nothing at all in
       most page stylesheets, which leaves the UA default: a ring that is
       invisible against half this app's surfaces.

       It is an `outline`, not a `box-shadow`: an outline is never clipped by
       an ancestor's `overflow: hidden`, and a focused control inside a scroll
       pane is precisely where the ring has to survive. */
    --focus-ring-width: 2px;
    --focus-ring-offset: 2px;
    --focus-ring-color: var(--action-accent);

    /* The tallest a floating list — a menu, a combobox popover — may be before
       it scrolls itself. A library that measures the real distance to the
       viewport edge should win over this, so call sites read it as the FALLBACK
       of the measured value: `var(--radix-…-available-height, var(--overlay-max-height))`.
       It is the value before anything has been measured, which is a real state:
       the first paint, and every test that runs without layout. */
    --overlay-max-height: 60vh;

    /* ── The fine-pointer half of the four pointer-keyed control tokens ──────
       These four are re-declared under `@media (any-pointer: coarse)` at the
       end of this file. Until 2026-08-21 they existed ONLY there, so under a
       mouse every `var(--control-min)` resolved to nothing and the FALLBACK
       argument was silently doing the work: `min-height: var(--control-min,
       24px)` was the only place the mouse value existed at all.

       That made 14 fallbacks load-bearing rather than dead weight, which is
       the opposite of what a fallback is for, and it hid the mouse value
       inside an argument instead of naming it here. Each value below is the
       fallback those call sites already used, so this changes nothing that
       renders; it moves the number to where a reader looks for it.

       `--control-height` split into two named fine-pointer values on #209:
       its call sites passed three mouse values (32px, 36px, 44px), which was
       one name doing three jobs. The 44px sites point at `--control-height`
       and the 32px sites at `--control-height-dense`, each value the call
       site's former fallback argument, named here so a reader sees it. A
       one-off 36px `.cpo-list-dock .btn` used to live in surfaces.css but was
       deleted on the dead-CSS pass — it emitted nothing, so no live call site
       reads `--control-height` from it anymore. */
    --page-head-h: 48px;
    --page-head-compact-h: 30px;
    --control-height: 44px;
    --control-height-dense: 32px;
    --control-min: 24px;
}

/* ── Shell density contexts ───────────────────────────────────────────────────
 * TWO CONTEXTS, NAMED, AND SET BY THE SHELL — `compact` and `standard`.
 *
 * `compact` is the dense operational scale: admin and HHN, where a biller works
 * long lists on a large monitor. `standard` is the deliberately larger provider
 * and physician scale. A PAGE NEVER PICKS ONE. The shell document carries the
 * class.
 *
 * Why the class is on <body> but the rule targets <html>: rem resolves against
 * <html>, so setting the body's inherited font size changes no rem dimension.
 * `:has()` is what lets the shell keep declaring density where it declares
 * everything else about itself.
 *
 * The 12px root used to live privately in templates/admin/base.html. Standalone
 * SPA documents inherit no template, so the admin SPA and the HHN portal fell
 * back to the browser's 16px default and inflated every rem dimension by 33%.
 * Density is an explicit capability now, and both ends of it are named. */
html:has(body.density-compact) {
    font-size: 12px;   /* nocheck — defines the rem scale, cannot use a rem token */
}

/* `standard` is also the DEFAULT: a document that names no density gets the
   browser's own root size, which is what the provider and physician portals
   have always taken. It is declared anyway, because "the provider portal is
   whatever happens when nobody opts in" is not a contract a component can read. */
html:has(body.density-standard) {
    font-size: 16px;   /* nocheck — defines the rem scale, cannot use a rem token */
}

/* ═════════════════════════════════════════════════════════════════════════
   THE BROWSER DOES NOT GET TO INVENT FONT SIZES
   ═════════════════════════════════════════════════════════════════════════

   Mobile Safari and Chrome-on-Android both run "text autosizing" (a.k.a. font
   boosting): they inflate text in a block based on that BLOCK'S width relative
   to the viewport, on the assumption that small text in a narrow column was not
   meant for a phone. Nothing in this codebase had ever turned it off.

   With a 12px root, essentially all of our text is what those heuristics target.
   And the boost is computed PER BLOCK and PER DEVICE, so the same label rendered
   at different sizes in different panes, on different phones, at different
   window sizes — which is what two rounds of bug reports described:

     "the text size of inputs in hd form is variable depending on like if ur on
      mobile and resolution"        → the per-block, per-device boost
     "the text in the phone view is way too big"   → the boost itself

   It is worth being precise about what was NOT the cause, because a lot of
   effort went at the wrong layer: the `--fs-*` scale is fine, and the
   `@media (any-pointer: coarse)` control override below is correct and
   necessary. Neither could produce sizes that vary by container width. Only the
   browser can do that.

   `100%` means "render the size I authored" — it is NOT `user-scalable=no`.
   Pinch-zoom, the browser's own font-size preference, and OS-level Larger Text
   all keep working; every mainstream CSS reset ships this line for exactly this
   reason.

   ── KNOWN CONSEQUENCE, and the follow-up it implies ──────────────────────
   Autosizing was accidentally compensating for the 12px root on phones. With it
   off, authored sizes render as authored, which means the smallest of them
   (65 `font-size: .72rem`/`.8rem` literals in templates/provider/, i.e. 8.6–9.6px)
   are now honestly too small on a phone rather than dishonestly readable.

   The fix for THAT is a 16px root plus an authored touch scale — the standard
   advice, "never shrink the root; get density from components". It was tried
   before and reverted because 73 form controls carry pixel widths calibrated to
   12px (see static/css/mobile.css). Re-measured 2026-07-29: 290 of those are in
   templates/admin/, and only 4 are in templates/provider/. So the blocker is an
   ADMIN blocker, and the provider portal can take the proper fix on its own.
   Needs a screenshot pass at both densities, so it is not bundled in here. */
/* ── The prototype's pixel sizes ──────────────────────────────────────────────
   The provider portal's accepted design (plans/ui-cohesion, 2026-09-08) is drawn
   in fixed pixels, not in steps of the rem scale above. Named here so any
   portal's page sheet can say which one it draws; the values never change
   with density. */
:root {
  --fs-p8: 8px;
  --fs-p9: 9px;
  --fs-p10: 10px;
  /* The half steps and 14px are the Monitoring kit's (KIT.md section F):
     panel titles and chart labels 10.5, tooltip lines 11.5, tabs, needs line
     and table rows 12.5, the answer sentence 14. */
  --fs-p10-5: 10.5px;
  --fs-p11: 11px;
  --fs-p11-5: 11.5px;
  --fs-p12: 12px;
  --fs-p12-5: 12.5px;
  --fs-p13: 13px;
  --fs-p14: 14px;
  --fs-p15: 15px;
  --fs-p16: 16px;
  --fs-p17: 17px;
  --fs-p18: 18px;
  --fs-p22: 22px;
  --fs-p23: 23px;
  --fs-p25: 25px;
  --fs-p26: 26px;
  /* At a glance's headline figures (glance.md, the band). */
  --fs-p32: 32px;
}

html {
    -webkit-text-size-adjust: 100%;   /* iOS Safari, older Android */
    text-size-adjust: 100%;           /* standard property */
}

/* ── Density layer ────────────────────────────────────────────────────────────
 * Two density modes, switched by INPUT MECHANISM rather than screen width.
 * This is the split every mature design system makes (SAP Fiori cozy/compact,
 * Salesforce Comfy/Compact, Carbon size variants):
 *
 *   layout   → keys on viewport width  (@media max-width, in mobile.css)
 *   density  → keys on pointer type    (@media any-pointer: coarse, here)
 *
 * They are different questions. A touch laptop at 1440px needs big targets and a
 * wide layout; a phone needs big targets and a stacked layout. Keying density on
 * width gets the first case wrong.
 *
 * `any-pointer` rather than `pointer`: it reports coarse if ANY available input
 * is coarse, which is the safe direction for hybrids.
 *
 * Desktop is deliberately left alone — the 12px root and dense controls exist
 * because billers work long lists on large monitors. Only touch is changed.
 */
@media (any-pointer: coarse) {
    :root {
        /* The control height under a finger IS the target floor. It points at
           the one token rather than repeating `44px`, so the floor cannot be
           raised in one of the two places and left in the other. */
        --control-height: var(--touch-target);
        /* A finger gets the dense floor too: 32px is a legal fine-point
           target on a desktop, but under a coarse pointer the same control is
           a WCAG 2.5.5 target like every other, so it floors alongside it. */
        --control-height-dense: var(--touch-target);
        /* The header grows with the control it must hold. Shrinking the button
           instead would trade a WCAG 2.5.5 target for tidiness. */
        --page-head-h: 56px;
        --page-head-compact-h: 34px;
        /* The WIDTH floor for an icon-only control, NOT a height floor. WCAG
           2.2 SC 2.5.8 asks for 24x24; the height comes from --touch-target. */
        --control-min: 24px;
        --control-pad-y: 0.5rem;
        --control-pad-x: 0.75rem;
        --control-font: 16px;     /* see below — not a rem */
    }

    /* 16px is not a style choice. iOS Safari auto-zooms the viewport whenever a
       focused input renders below 16px. With a 12px root every Bootstrap input
       resolves to ~10.5px, so *every field in the app* zoomed on iPhone — the
       page jumps, then the user has to pinch back out.
       An absolute px value is required: a rem would re-inherit the 12px root.
       Raising the root globally was tried and reverted; 73 controls carry pixel
       widths calibrated to 12px. This changes type only, and only on touch. */
    /* `.hd-field-scaled` IS EXEMPT, and this exemption is older than it looks.
       A signing overlay field's type size is DERIVED from the rendered page scale
       (static/js/signing/fieldRender.js: one factor for position, size and font)
       so the provider sees the size `services/signing/pdf_stamp.py` will actually
       print. The `!important` below beats the inline size that code sets — so the
       floor won and the derivation lost, on every touch device, silently, since
       this block was written.

       MEASURED on the co-sign surface at a 463px-tall page: the JS set
       `font-size: 5.26px`, the computed value was `16px`. It is invisible on a
       wide page, where 16px happens to look reasonable; on a small one a date
       field wrapped to "07/2 9/20 26" and spilled out of its box, which is how it
       was finally reported.

       The iOS trade is accepted here deliberately: a field under 16px zooms the
       viewport when tapped, which is irritating. A field rendered at a size the
       paper will not use means a provider signs something other than what they
       were shown, which is not acceptable at any level of irritation. */
    input:not([type="checkbox"]):not([type="radio"]):not(.hd-field-scaled),
    select:not(.hd-field-scaled),
    textarea:not(.hd-field-scaled),
    .form-control:not(.hd-field-scaled),
    .form-select:not(.hd-field-scaled) {
        /* `!important` on this ONE property, and the reason is mechanical rather
           than stylistic: 55 form controls across 12 templates carry an inline
           `style="font-size:.8rem"` (9.6px at this root), and an inline
           declaration outranks every selector a stylesheet can write. Specificity
           cannot win here — this is the case `!important` exists for.

           Without it the rule below silently lost on exactly the pages that
           needed it most, and the audit kept reporting controls at 8.64px while
           this block looked correct. Scoped to font-size because that is the one
           with a trap-door failure: iOS zooms in on focus and does not zoom back
           out, leaving the page wider than the screen.

           The real fix is deleting those inline styles — tracked as debt, not
           done here, because it would also change DESKTOP density on 12
           templates and desktop is deliberately dense (see the note above).
           `scripts/standards.d/design-tokens.sh` already ratchets inline styles
           down; when that count reaches zero this `!important` can go. */
        font-size: var(--control-font) !important;
        min-height: var(--control-height);
        padding: var(--control-pad-y) var(--control-pad-x);
    }

    /* A SELECT KEEPS ITS CHEVRON GUTTER. The shorthand above flattens the
       right-hand padding Bootstrap reserves for `.form-select`'s background
       arrow, so on a touch screen the arrow lands on the chosen text: measured
       2026-10-02 on /admin/staff-tasks at 390 px, 16 px of text under it. The
       scheduling pool sort had patched the same defect for itself alone
       (`features/scheduling/scheduleWorkspace.css`). 2.25rem is Bootstrap's
       own indicator padding. A listbox (`multiple`, `size`) draws no arrow. */
    .form-select:not(.hd-field-scaled):not([multiple]):not([size]) {
        padding-right: 2.25rem;
    }

    /* Bootstrap 5.3 exposes runtime custom properties for buttons but NOT for
       form controls (those are Sass-time only), which is why inputs above need
       direct property overrides while buttons can be themed.

       PADDING ONLY. The height floor moved into the one rule below, because
       two rules setting `min-height` on overlapping selectors is what produced
       the 24px defect this block used to sit above. */
    .btn {
        --bs-btn-padding-y: var(--control-pad-y);
        --bs-btn-padding-x: var(--control-pad-x);
        /* Bootstrap buttons are inline-block. Once the touch floor makes that
           box taller than its line-height, the label stays on the first line
           instead of centring in the target. The review census caught this on
           certification, HHN, provider-document and signing actions. */
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    /* THE TARGET FLOOR. One rule, one value, every interactive target.
       ═══════════════════════════════════════════════════════════════════════
       This block used to floor the small variants at `--control-min` (24px)
       AFTER `.btn` above floored them at 44px. Both selectors are (0,1,0), so
       the later one won and EVERY `.btn-sm` in the app measured 24px under a
       finger. The visual harness measured it on nine distinct controls across
       the portals — `Create certification`, `Cancel`, `Dismiss error`,
       `Sign out` — all at 24.0px at 390x840 with a coarse pointer. It was the
       largest single defect class in the tree, and the cause was rule order in
       this file rather than anything a page or a component did.

       So the small variants are floored WITH the base one, not after it. A
       dense row action is still visually compact — it keeps its `-sm` padding
       and type — but the box a finger has to hit is the accepted 44px.

       `--control-min` stays, as the WIDTH floor it always was: a 44px-tall
       chevron that is 12px wide still fails WCAG 2.2 SC 2.5.8's 24x24, and
       icon-only header buttons and month-nav chevrons are exactly that shape.

       `.btn-bare` is here because surfaces.css defines it as ONE control ("the
       one bare button"), and only its old page-local alias was ever floored. A
       control cannot be shared vocabulary in one sheet and an exception in the
       other; that split is how a floor develops holes.

       `.nav-link` and `[role="tab"]` are here because the harness found them
       under the floor too (`a.nav-link` and the billing queue's `.sx-view` tab
       buttons, both 24.0px) and NOTHING covered them. They are targets by
       meaning, not by class, which is why the selector keys on the ARIA role:
       a page that hand-rolls a tab strip still gets the floor, and the shared
       component it should be using gets it for free. */
    .btn,
    .btn-sm,
    .table .btn,
    .btn-link,
    .btn-bare,
    .nav-link,
    [role="tab"],
    [role="button"] {
        min-height: var(--touch-target);
        min-width: var(--control-min);
    }

    /* Standalone action links are targets too. WCAG's "inline" exception covers
       links inside a sentence, not a bare `open profile` sitting on its own.
       They take the same floor; only the box model differs, because an inline
       anchor ignores `min-height` until it stops being inline. */
    a.action-link,
    td > a:only-child,
    .btn-link {
        display: inline-flex;
        align-items: center;
        min-height: var(--touch-target);
    }

    /* Checkboxes and radios are the one control the block above deliberately
       excludes — they render no text, so `--control-font` would only distort
       them. But Bootstrap sizes them in `em`, so the 12px root leaves them
       9-16px square: measured across the app, `.form-check-input` appeared at
       9x9, 11x11, 12x12 and 16x16, all under the 24px AA floor and all of them
       the smallest target on their page.

       `.form-check` places the input with a NEGATIVE margin equal to its own
       `padding-left` (1.5em ≈ 18px at this root). Growing the box without
       growing that pair makes the input overlap its own label, so the two move
       together. `.form-switch` is excluded: it is a pill, and a 24px min-width
       would square it off. */
    .form-check-input {
        min-width: var(--control-min);
        min-height: var(--control-min);
    }

    .form-check:not(.form-switch) {
        padding-left: 32px;
    }

    .form-check:not(.form-switch) > .form-check-input {
        margin-left: -32px;
    }

    .form-switch > .form-check-input {
        min-width: 40px;
    }
}

/* ═════════════════════════════════════════════════════════════════════════
   THE NEW LOOK (SCRUM-3254)
   ═════════════════════════════════════════════════════════════════════════
   The accepted overhaul reference (plans/ui-cohesion/REFERENCE-2026-11-04-look.md),
   applied only where a portal shell wrote data-look="next" on <html> for a reader who
   turned the new look on (app/ui_look.py). This sheet owns these values like every other
   design value. Text sizes are pixels so the 12 px admin root keeps every width;
   scripts/check_contrast.py judges this block on top of the current one. */
:root[data-look="next"] {
    /* The grey ramp stays private to tokens.css; the new look sets the named tokens the
       pages read. One cool-neutral scale: canvas #f5f6f8, sunken #f0f2f5, line #e3e7ec,
       strong line #cfd6de, muted text #586273 (6.2:1 on white), text #111827. */
    --surface-sunken: #f5f6f8;
    --surface-subtle: #f0f2f5;
    --surface-hover: #f6f8fb;
    --border: #e3e7ec;
    --border-strong: #cfd6de;
    --text: #111827;
    --text-muted: #586273;
    --text-subtle: #586273;
    --content-text: #111827;
    --content-text-strong: #111827;
    --content-text-subtle: #586273;
    --surface-inverse: #111827;
    --surface-inverse-raised: #1f2733;
    --surface-backdrop: #3d4756;
    --border-inverse: #98a2b0;
    --text-inverse-muted: #cfd6de;
    --action-accent-subtle: #eaf1fd;
    --selected: #eaf1fd;
    --table-border: #e3e7ec;
    --table-control-border: #cfd6de;
    --table-hover-bg: #f6f8fb;
    --table-row-hover: #f6f8fb;
    --table-head-sticky-bg: #ffffff;
    --table-head-dense-bg: #ffffff;
    --table-filter-label: #586273;

    /* Status words reach 4.5:1 on their own tints. */
    --indicator-success: #137a3a;   /* 4.83:1 on its tint */
    --indicator-success-bg: #e8f5ed;
    --indicator-warning: #92600a;
    --indicator-warning-bg: #fdf3da;
    --indicator-danger: #b91c1c;
    --indicator-danger-bg: #fdeeee;
    --status-danger: #b91c1c;
    --nav-fg-quiet: rgb(255 255 255 / 62%);
    /* The labelled sidebar (SCRUM-3255): its ground, rows, group names, counts, tips. */
    --nav-surface: #172038;
    --nav-fg: #b8c2d6;
    --nav-fg-label: #7c88a1;
    --nav-hover: rgb(255 255 255 / 7%);
    --nav-active: rgb(255 255 255 / 12%);
    --nav-divider: rgb(255 255 255 / 8%);
    --nav-count-bg: rgb(255 255 255 / 11%);
    --nav-count-fg: #dfe5f0;
    --nav-tip-bg: #0b1220;
    /* The parts' badge pairs (SCRUM-3256 report): info ink on its tint (6.0:1), and the
       neutral badge's tint, which takes --text (12.9:1). */
    --indicator-info: #1f56b8;
    --indicator-info-bg: #eaf1fd;
    --badge-neutral-bg: #eef0f3;

    --font-sans: 'Inter Variable', system-ui, -apple-system, 'Segoe UI', sans-serif;
    --font-display: var(--font-sans);
    --fs-2xs: 12px;
    --fs-xs: 12px;
    --fs-sm: 13px;
    --fs-base: 14px;
    --fs-md: 14px;
    --fs-lg: 16px;
    --fs-xl: 18px;
    --fs-2xl: 20px;
    --fs-p8: 12px;
    --fs-p9: 12px;
    --fs-p10: 12px;
    --fs-p10-5: 12px;
    --fs-p11: 12px;
    --fs-p11-5: 12px;
    --fs-p12: 13px;
    --fs-p12-5: 13px;
    --fs-p13: 14px;
    --fs-p14: 14px;
    --fs-p15: 16px;
    --fs-p16: 16px;
    --fs-p17: 18px;
    --fs-p18: 18px;
    --fs-p22: 20px;
    --fs-p23: 20px;
    --fs-p25: 20px;
    --fs-p26: 20px;
    --fs-p32: 24px;
    --lh-tight: 1.3;

    /* Cards and panels get a 1 px edge and no shadow; only floating layers cast one. */
    --radius-sm: 4px;
    --radius-control: 6px;
    --radius: 8px;
    --radius-lg: 12px;
    --shadow-panel: none;
    --shadow-metrics: none;
    --shadow-panel-strong: none;
    --shadow-card-hover: 0 1px 2px rgb(17 24 39 / 6%);
    --shadow-sm: 0 1px 2px rgb(17 24 39 / 5%);

    --page-inset-block: 16px;
    --page-inset-inline: 24px;
}
}
