@layer components {
@media (max-width: 767.98px) {
  .mobile-control-surface {
    padding: var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface);
  }
}
/* static/css/surfaces.css
 *
 * The surface layer — the room the tables live in.
 *
 * Same move as the table layer (tables.css), one level up. Measured before
 * writing: the admin templates contain 257 distinct hex colours (62 of them
 * blues), 93 distinct `class="card …"` combinations for what should be one
 * component, and two icon dialects (879 Font Awesome + 293 emoji, against a
 * house rule that says "no emojis in the design").
 *
 * Three ideas, in the order they matter:
 *
 *   1. ONE CARD ANATOMY. A stat row reads as designed when every card shares
 *      the same alignment, label position and number treatment. Ours has three
 *      different ones side by side.
 *   2. COLOUR ENCODES OR IT DOESN'T APPEAR. Today 1000 is blue and 1 is green
 *      for no reason — neither is good or bad news. Numbers are near-black body
 *      colour; colour is reserved for state that a person acts on.
 *   3. EVERYTHING SHARES AN EDGE. One container, one gutter, every section
 *      snapping to the same rails.
 *
 * Nothing applies until markup opts in, so adding the file changes no page.
 */

:root {
    --status-ribbon-rectangle-width: 88px;
}

/* ── The one card ─────────────────────────────────────────────────────────
   Replaces `card border-0 shadow-sm`, `card border-0 bg-light`,
   `card border-0 shadow-sm h-100 rounded-top-0` and 90 other spellings.
   Border, not shadow: shadows at this density stack into visual mud, and a
   hairline is what makes blocks share an edge cleanly. */
.panel {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-panel-strong);
    overflow: hidden;
}
/* Stacked panels in PLAIN FLOW get the gutter between them. Not inside a
   container that already spaces its children with `gap` — a layout primitive
   or a page root — where this margin landed ON TOP of the gap and pushed every
   panel after the first out of line with its neighbours (measured on the
   reference: the second and third boxes of a Grid sat 16px below the first). */
/* The record overviews are gap grids too (`components.css`, `record.css`): a
   voicemail's Details box sat 16px below the Recording box beside it. */
:not(.hd-stack, .hd-cluster, .hd-grid, .hd-sidebar, .page-shell, .task-workspace-page,
     .record-overview, .record-detail-overview-quiet) > .panel + .panel {
    margin-top: var(--page-gutter);
}
.panel.panel--inset { border: 1px solid var(--border); border-radius: var(--radius); }

.panel-head {
    display: flex;
    align-items: center;
    gap: var(--panel-pad-tight);
    padding: var(--panel-pad-tight) var(--panel-pad);
    border-bottom: 1px solid var(--border);
    font-size: 12px;
    font-weight: 600;
    color: var(--text-muted);
    background: var(--surface-sunken);
}
/* Secondary information in a head sits right, never competing with the title. */
.panel-head > .panel-head-meta {
    margin-left: auto;
    font-weight: 400;
    color: var(--content-text-subtle);
}
/* The head's controls END the strip — the `Actions` rule — whether or not a
   meta sits before them. Without their own slot they followed the title
   directly whenever a panel had no meta, so "Edit" sat against "Certification"
   on one panel and at the far end on the next (SCRUM-1499). */
.panel-head > .panel-head-actions {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}
.panel-head > .panel-head-meta + .panel-head-actions { margin-left: 0; }
.panel-body { padding: var(--panel-pad); }
.panel-body--flush { padding: 0; }   /* for a table that draws its own edges */

/* Opt-in sections keep one shared chrome owner. Short sections never stretch
   to match a taller neighbour. The bounded default above stays unchanged. */
.panel.panel--quiet {
    align-self: start;
    min-width: 0;
    background: transparent;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    overflow: visible;
}
.panel--quiet + .panel--quiet { margin-top: 0; }
.panel--quiet > .panel-head {
    padding: 0 0 var(--space-2);
    border-bottom: 0;
    background: transparent;
    color: var(--text);
    flex-wrap: wrap;
}
.panel-title { margin: 0; font: inherit; }
.panel--quiet > .panel-body { padding: 0; }

/* ── The one stat ─────────────────────────────────────────────────────────
   One anatomy: label above, number below, both left-aligned, equal widths.
   The number is body colour at size — NOT a hue. A figure is not a status;
   1000 being blue and 1 being green tells the reader nothing and trains them
   to ignore colour where it does mean something. */
.stat-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: var(--page-gutter);
}
.stat {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--panel-pad);
    box-shadow: var(--shadow-panel);
}
.stat-label {
    display: block;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--content-text-subtle);
}
.stat-value {
    display: block;
    margin-top: 2px;
    font-size: 22px;
    line-height: 1.1;
    font-weight: 600;
    color: var(--content-text);              /* body colour, deliberately */
    font-variant-numeric: tabular-nums;
}
.stat-note { font-size: 11px; color: var(--text-muted); }
/* The one exception: a figure the reader must act on. Rare by design — once
   three numbers are urgent, none of them is. */
.stat-value--alert { color: var(--indicator-danger); }

/* ── Shared edges ─────────────────────────────────────────────────────────
   Every top-level section of a page snaps to the same rails, so the eye reads
   one column of content rather than several floating blocks. */
.page-stack > * + * { margin-top: var(--page-gutter); }
.page-stack { padding: var(--page-gutter); }
@media (max-width: 767.98px) {
    /* Full-bleed on a phone: components own their padding (mobile.css), so the
       container adds none. */
    .page-stack { padding: 0; }
    .page-stack > * + * { margin-top: var(--panel-pad-tight); }
    /* FULL-BLEED PANELS ON A PAGE-STACK ONLY. This rule once read `.panel`
       and flattened every panel on every phone page, so inside a page frame's
       inset a panel became a strip with no sides, between a rounded band and
       rounded framed sections on the same page. A page-stack has no inset on
       a phone, so its panels run edge to edge and lose their sides; a panel
       anywhere else keeps its box. */
    .page-stack .panel { border-left: 0; border-right: 0; border-radius: 0; }
}

/* ── Icons ────────────────────────────────────────────────────────────────
   One dialect, one size, and none at all on section headings — "Top Paths"
   does not need a decoration. Icons earn their place when they identify an
   ACTION, not when they label a noun. */
.panel-head > .fas,
.panel-head > .far { display: none; }
.icon { font-size: 12px; width: 1em; text-align: center; color: currentColor; }

/* ═════════════════════════════════════════════════════════════════════════
   THE PAGE SHELL — the standard shape for an admin page (DECISIONS.md D17)
   ═════════════════════════════════════════════════════════════════════════

   An admin page is a TOOL, not an article. It fills its viewport, opens with
   one 44px strip, and scrolls inside its panes — the document itself never
   grows.

   The pattern this replaces, present on most pages: breadcrumb, then an <h4>
   masthead, then a row of stat cards, then tabs, then filters — 220px+ of
   preamble before the first row of what the person came for. The tab shell
   already names the page, so the breadcrumb and title were restating it.

   Derived from the CPO/CCM page, which arrived at this shape first and is
   noticeably better to use than its siblings.

       <div class="page-shell">
         <div class="page-head">
           <span class="page-title">IP Visit Logs</span>
           <span class="page-meta">1000 visits · 1 unique IP · 47 writes</span>
           <span class="page-actions"> … buttons … </span>
         </div>
         <div class="page-body"> … panes … </div>
       </div>
*/

.page-shell {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
    /* The horizontal twin of `min-height: 0`, and the one everybody forgets.
       A flex item defaults to `min-width: auto`, which refuses to shrink below
       its CONTENT — so when the Stedi header grew (its four selects went to 16px
       to stop iOS zooming), the shell widened to 974px inside a 390px parent and
       took the whole page sideways with it. Every descendant that tries to
       scroll its own overflow is powerless while an ancestor is still expanding:
       `.page-actions` had `overflow-x: auto` and `scrollWidth === clientWidth`,
       i.e. nothing to scroll, because it had been handed all the room it asked
       for. This is the same trap PAGE-MIGRATION §7.9 records for `.dt-scroll`. */
    min-width: 0;
    box-sizing: border-box;
    /* THE PAGE FRAME, from the tokens: the same inset and the same gap as
       `.task-workspace-page`, so every portal page sits the same distance from
       the shell. It was 8px here and 42px there. */
    padding: var(--page-inset-block) var(--page-inset-inline);
    gap: var(--page-gap);
    background: var(--surface-sunken);
}

/* The one 44px page strip. It was once byte-for-byte the same strip in three
   different files; this is the only place it is defined. */
.page-head {
    display: flex;
    align-items: center;
    gap: 12px;
    /* Fixed, NOT min-height: chrome must not change size with viewport width.
       It DOES change with pointer type, which is a different axis (D11: density
       keys on pointer, layout keys on width) — on touch `--control-height` is
       44px, so a 44px row leaves the control inside it no room at all and the
       button reads as a slab filling the row. */
    height: var(--page-head-h);
    flex-shrink: 0;
    padding: 0 var(--panel-pad);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-panel);
    position: relative;
    z-index: 3;
}
.page-title {
    font-weight: 650;
    font-size: .9rem;
    color: var(--content-text);
    white-space: nowrap;
    letter-spacing: -.01em;
}
/* Summary facts as one line of prose. Three numbers a person reads once and
   then ignores do not each deserve a bordered box; the vertical space belongs
   to the data. This is the piece that yields when the row runs out of room. */
.page-meta {
    font-size: .78rem;
    color: var(--text-muted);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.page-meta strong {
    color: var(--content-text);
    font-variant-numeric: tabular-nums;
}
/* Never wraps — a toolbar that grows a second line makes the header a
   different height at different widths, which reads as broken. The meta above
   truncates instead. */
/* A header row with no controls in it. See PageHead: the full height exists to
   hold a 44px touch control, and a row of text does not need it. */
.page-head--compact {
    height: var(--page-head-compact-h);
}

.page-actions {
    margin-left: auto;
    flex: 0 0 auto;
    display: flex;
    flex-wrap: nowrap;
    gap: 6px;
}

/* The only thing that scrolls. */
.page-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    min-width: 0;
    /* Rich workflows put several white components in this region. Keeping the
       region white flattened their borders into one continuous sheet; the
       shared sunken canvas makes the gaps carry hierarchy. Simple list pages
       render their DataTable directly in `.page-shell`, so they do not gain an
       unnecessary outer card from this rule. */
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: none;
}

@media (max-width: 767.98px) {
    /* Cards remain visually distinct on a phone, but the canvas itself cannot
       spend a desktop gutter on every edge: that pushed several first rows
       below the 140px work budget. */
    .page-head {
        height: 44px;
        padding: 0 var(--panel-pad-tight);
        gap: var(--panel-pad-tight);
    }
    /* On a phone the summary is the first thing to go — the actions and the
       title are what the person needs. */
    .page-head .page-meta { display: none; }

    /* A `<select>` in the toolbar is the second case `flex: 0 0 auto` gets
       wrong (the first was the search box, above). On touch the select renders
       at 16px to stop iOS zooming, so its intrinsic width grows — and because
       nothing inside a `0 0 auto` parent may shrink, the CCM Signing filter
       kept its content width and was clipped by the viewport edge with its
       chevron cut in half.

       Both bounds are load-bearing and they fix opposite failures. The CAP
       (in `vw`) stops the filter crowding out the title — the title says where
       you are, the filter only says what you are looking at. The FLOOR stops
       the other one: intrinsic sizing gave the CCM filter an 80px box, and at
       16px `Pending` ran underneath the chevron the browser paints on the
       right, which reads exactly like a clipped control. 7.5rem clears both.
       Measured, not eyeballed — the box was never actually outside the
       viewport (document overflow was 0 throughout); the collision was. */
    /* `:only-child` is the whole safety of this rule. The first version applied
       the floor to every select in a toolbar, and /admin/billing has FOUR
       in one `.page-actions` — 4 x 120px minimum in a non-wrapping row took its
       header to 855px inside a 390px viewport, a +584px overflow on a page that
       had been clean. Caught by the retired mobile sweep against its baseline, which was
       exactly the regression that check existed to catch.
       A lone filter can afford a floor; a toolbar of four must stay free to
       shrink, and its crowding is a different problem with a different fix. */
    /* THE ROOT CAUSE, stated once: `--control-font` forces 16px on every select
       at touch sizes to stop iOS auto-zooming (tokens.css). A `<select>` is
       sized by its longest option text, so that single change widens every one
       of them by roughly 70% — and `.page-actions` is `flex: 0 0 auto` and
       `nowrap`, so a toolbar of several selects has no way to absorb it.
       /admin/billing has FOUR: its header went to 1015px inside a 390px
       viewport (one select alone measured 323px), overflowing the page by
       744px. Caught by the retired mobile sweep against its baseline.

       So on touch the toolbar may shrink and, if it still does not fit, scroll
       WITHIN ITSELF — which keeps the overflow inside the 44px strip instead of
       handing it to the document, where it drags every row of the page sideways.
       `min-width: 0` is the load-bearing half: a flex item's default
       `min-width: auto` refuses to shrink below its content, so `flex-shrink`
       alone changes nothing. */
    .page-head .page-actions {
        flex: 0 1 auto;
        min-width: 0;
        overflow-x: auto;
    }

    /* `flex-shrink: 0` on the CHILDREN is what makes the scroll above real.
       With them shrinkable the toolbar dutifully scrolled while squeezing a
       facet select down to 20px — a control too narrow to read or to hit, which
       is a worse outcome than the overflow it replaced. The container absorbs
       the shortfall by scrolling; the controls keep a usable size. */
    .page-head .page-actions > * {
        flex: 0 0 auto;
    }

    .page-head .page-actions > .form-select,
    .page-head .page-actions > select {
        max-width: 42vw;
    }

    /* A LONE filter has room for a floor, and needs one: at 16px an
       intrinsically-sized select gave "Pending" an 80px box and the label ran
       under the chevron the browser paints on the right. A toolbar of four has
       no such room, which is why this is `:only-child` and the cap above is not. */
    .page-head .page-actions > .form-select:only-child,
    .page-head .page-actions > select:only-child {
        min-width: 7.5rem;
    }
}

/* ═════════════════════════════════════════════════════════════════════════
   SHARED COMPONENTS — one definition, several names

   These were each written two to four times across templates before anyone
   noticed they were the same component. The duplicates are deleted; the
   page-local class names survive as aliases so markup and JS are untouched.

   The rule for adding to this file: if a page needs a *different* component,
   give it a new name. If it needs the same component with one property
   changed, alias it here and keep the delta in the page.
   ═════════════════════════════════════════════════════════════════════════ */

/* ── The one selectable row ───────────────────────────────────────────────
   A row in a list you pick from: patients, visit notes, certifications. Was
   defined four times with three paddings and three copies each of the hover
   and active rules — all painting the same two colours.

   Deliberately does NOT set `display`. Some lists stack block children;
   forcing flex here would lay them out in a row. Layout is the caller's
   business, state is not. */
.item-row {
    padding: 6px 10px;
    border-bottom: 1px solid var(--divider);
    cursor: pointer;
    user-select: none;
    transition: background-color .14s ease, box-shadow .14s ease;
}
.item-row {
    /* Repeated rows are the content, so their whitespace compounds. The old
       10px inter-row gap plus 16px vertical padding spent nearly half of a
       two-line provider row on empty space. This keeps the card separation and
       a >=44px two-line row while fitting materially more work at 100% zoom. */
    margin: 2px 4px;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: none;
    /* Link rows are cards whose whole surface navigates. Do not let the
       browser's anchor decoration turn their identity and facts into a stack
       of underlined links; hover/focus already changes the card surface. */
    color: inherit;
    text-decoration: none;
}
.item-row:hover { background: var(--surface-sunken); }
/* A static row is not a control (RecordRow: "no role, no tab stop, no cursor"). The base rule
   above gives every row the pointer, so a row with nothing to open read as one that did
   nothing when tapped (appointments, "Visit forms", 11/12/2026). */
.item-row--static { cursor: default; }

/* One "this is the row you have open" tint. */
.item-row.active {
    background: var(--selected);
    box-shadow: inset 3px 0 0 var(--action-accent);
}

/* ── The row's OWN state, as a tint ───────────────────────────────────────
   Opt-in through ItemRow's `tone`, and deliberately only three buckets,
   chosen by what the reader must DO rather than by the value, so a sixth
   status slots into an existing bucket instead of needing a sixth colour.

       needs work        danger    (a CPO/CCM month still in draft)
       waiting on someone accent   (ready)
       finished          ok        (signed / billed)

   The row carries the colour so state is findable by scanning a long list;
   the WORD stays in `.item-meta`, which is what keeps the colour redundant
   rather than load-bearing. Do not add a fourth without that argument.

   `:is()` and not `:where()` — these must outrank `.item-row.active`'s own
   background, which `:where()`'s zero specificity would lose to. Selection
   keeps the left accent bar in every case: the tint says what the record
   is, the bar says which one you have open, and those are two questions.

   THE THREE ARE NOT EQUAL WEIGHTS (user, 2026-08-17: "red should dominate").
   Danger is the only one that has to interrupt a reader scrolling past, so it
   takes the full tint, a heavier edge colour AND a 3px bar; the other two are
   pale washes that say "not this one" from the corner of the eye. Weighting
   them equally made a list of work look like a list of colours. */
/* ── An arbitrary-colour wash (RecordRow `tint`) ─────────────────────────
   For a row whose colour is DATA, not one of the three buckets: the
   scheduling surfaces wash patient rows with the same bin colour their map
   pin wears (product decision 2026-08-27). Wash only — no edge bar, no
   heavier border — so it can never outshout `tone-danger`, and the higher
   `:not(.active)` specificity keeps selection's own tint and bar intact. */
.item-row.has-tint:not(.active) {
    background: color-mix(in srgb, var(--row-edge) 12%, var(--surface));
}
.item-row.has-tint:not(.active):hover {
    background: color-mix(in srgb, var(--row-edge) 22%, var(--surface));
}

.item-row.tone-danger { --row-edge: var(--indicator-danger); --row-tint: var(--indicator-danger-bg); }
.item-row.tone-accent { --row-edge: var(--action-accent); --row-tint: var(--action-accent-subtle); }
.item-row.tone-ok     { --row-edge: var(--indicator-success);     --row-tint: var(--indicator-success-bg); }
.item-row:is(.tone-danger, .tone-accent, .tone-ok) {
    background: var(--row-tint);
    border-color: color-mix(in srgb, var(--row-edge) 22%, var(--border));
}
/* Quieter than the token's own tint: the pale states must not compete. */
.item-row:is(.tone-accent, .tone-ok) {
    background: color-mix(in srgb, var(--row-tint) 55%, var(--surface));
}
.item-row.tone-danger {
    border-color: color-mix(in srgb, var(--row-edge) 45%, var(--border));
    box-shadow: inset 3px 0 0 var(--row-edge);
}
.item-row:is(.tone-danger, .tone-accent, .tone-ok):hover {
    background: color-mix(in srgb, var(--row-edge) 8%, var(--row-tint));
}
/* Open, and still red: selection deepens the fill and takes the border rather
   than replacing the bar, which is the row's state and not its selection. */
.item-row:is(.tone-danger, .tone-accent, .tone-ok).active {
    background: color-mix(in srgb, var(--row-edge) 12%, var(--row-tint));
    border-color: var(--row-edge);
}
.item-row.tone-danger.active { box-shadow: inset 3px 0 0 var(--row-edge); }

/* Muted is a WASH over the whole row, not a bordered edge state: the record
   is cancelled or retired, so its name reads struck and its facts fall back
   to the muted token. Deliberately NOT in the edge/tint set above, because
   there is no edge to draw — the opposite of emphasis. */
.item-row.tone-muted .item-name { text-decoration: line-through; }
.item-row.tone-muted .item-name,
.item-row.tone-muted .item-value,
.item-row.tone-muted .item-value-meta { color: var(--text-muted); }

/* ── The INSIDE of that row ───────────────────────────────────────────────
   `.item-row` deliberately sets no layout, which left every list to compose its
   own contents — and the CPO/CCM page, the one this standard is extracted from,
   composed the good one INSIDE ITS OWN TEMPLATE where nothing else could reach
   it. So "make it look like CPO/CCM" was impossible to satisfy by adopting a
   primitive: the primitive stopped at the row's border.

   The layout is lifted here verbatim from `templates/admin/billing/cpo_ccm_time.html`
   (the template's copies are deleted), so the reference page keeps its exact
   appearance and every other list can finally have it too:

       ┌──────────────────────────────────────────────┐
       │ Abubakar, Harold                          45 │   .item-name  .item-value
       │ ● ready · 99491 · 📄                         │   .item-meta
       └──────────────────────────────────────────────┘

   This is a LIST, not a table: no <thead>, no column grid, identical at 390px
   and at 1364px. That is the whole difference in feel — a table asks you to read
   a header to know what a cell means, a row like this says it in place. */
.item-row {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding-left: 10px;
    padding-right: 10px;
}

/* A ONE-LINE ROW THAT IS A CONTROL centres its line under a finger. On a
   coarse pointer tokens.css floors every `[role="button"]` at the touch
   target, taller than one line, and `flex-start` above stood the line on the
   row's top edge with the slack beneath it (a guide video's parts, SCRUM-1552).
   With no second line the name has nothing to keep level with but a one-line
   value, so centring moves nothing else. A row with a meta line keeps
   `flex-start`: its value stays level with the name. */
@media (any-pointer: coarse) {
    .item-row[role="button"]:not(:has(.item-meta, .item-value-meta)) { align-items: center; }
}

/* The leading identity mark on person rows — the shared Avatar, opted in per
   row via ItemRow's `avatar` prop (user, 2026-08-12: "maybe patient cards
   should have icons"). A fixed slot: the row's gap spaces it, and the body's
   truncation math is unchanged. */
.item-row .item-avatar { flex: 0 0 auto; }

/* Takes the remaining width. `min-width: 0` so the name may truncate rather
   than push the value off the row — the flex default is `auto`, which refuses. */
.item-body { flex: 1; min-width: 0; }

/* One line, ever. Variable content lives in a fixed slot and the slot wins:
   a row only gets messy when text length is allowed to move other things. */
.item-name {
    font-size: .82rem;
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Several facts on ONE line, joined by `·` by the caller — not one labelled
   line each. A label repeated down every row to say what the column always is
   is a label doing nothing. */
.item-meta {
    font-size: .72rem;
    color: var(--text-muted);
    margin-top: 2px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.item-row-wrap-meta .item-meta,
.item-row-wrap-all .item-name,
.item-row-wrap-all .item-meta {
    white-space: normal;
    overflow-wrap: anywhere;
}

/* A second secondary line. Same treatment as `.item-meta`; slightly dimmer so the
   two are ranked rather than competing. Used when a row's facts answer different
   questions ("which document" vs "who it is about") and flattening them into one
   `·`-joined line makes the row's identity depend on where the eye stopped. */
.item-sub {
    margin-top: 1px;
    color: var(--content-text-subtle);
}

/* The right-hand slot holding two facts of unequal weight — a status you scan for
   above a date you read only after you have stopped. Right-aligned so the statuses
   form a column the eye can run down, which is the whole point of putting the
   status here instead of in the meta line. */
.item-value-stack {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    text-align: right;
    min-width: 0;
}
.item-value-meta {
    font-size: .68rem;
    color: var(--content-text-subtle);
    margin-top: 1px;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* Header-only value placement. The ordinary value is a flex sibling beside
   the entire body. This intrinsic header row keeps the value beside identity
   while later structured fact lines retain the body's full width. */
.item-row-header {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
}
.item-row-header-copy { flex: 1; min-width: 0; }
.item-row-header > .item-value,
.item-row-header > .item-value-stack { flex: 0 0 auto; }

/* ── A page whose island owns the viewport ────────────────────────────────
   Bounds the content column to the frame's height so the island's PANES scroll
   and the document does not grow.

   Without it the shell had no definite height to resolve `height: 100%` against,
   so it grew to its content — 1,551px inside a 736px frame — and `.main-content`'s
   `overflow: hidden` then clipped the difference. MEASURED: 815px of rows on
   /admin/signing-queue and 415px on /admin/signing-requests were unreachable at
   391px wide. Not reachable by scrolling either: the iframe grows with its content
   and the shell around it is itself `overflow: hidden`, so there was no scrollable
   ancestor anywhere. Desktop was unaffected, which is why it survived review.

   `100dvh`, never `calc(100vh - Npx)`. That subtraction is the bug this codebase
   has now fixed four times (`signing_request.html`, `signing_inbox.html`,
   `ccm_signing.html`, `day_map.html`): the number is one navbar measured once at
   one width, and `100vh` on mobile Safari is the height with the URL bar hidden.
   `dvh` tracks the real viewport, and there is no chrome above `.main-content`
   inside the frame, so no subtraction is needed.

   Keyed on `:has()` so a page opts in simply by mounting a `.page-shell` island —
   the two templates that used to retype `overflow: hidden; padding: 0` in their own
   <style> blocks no longer need to, and the next such page gets it for free. */
.main-content:has(.page-shell) {
    height: 100dvh;
    overflow: hidden;
    padding: 0;
}

/* ── Status ribbon ────────────────────────────────────────────────────────
   A COLOUR-CODED status band. This is a deliberate, owner-approved exception to
   the UI guideline "use plain text for status instead of badges / no weird
   colored badges anywhere" — see the note in CLAUDE.md. It is recorded here so
   the next pass does not "fix" it back to plain text and churn the review cycle.

   What keeps it from being the thing that rule was written against:

     * **Five tones, not one per status.** They are grouped by what the status
       means FOR YOU — act on it, rework it, wait, done, closed — so a list of
       nine statuses reads as four or five colours you can learn, not confetti.
     * **Colour is never the only signal.** The label is always present, so the
       ribbon is redundant encoding rather than the sole carrier of meaning.
     * **Low-saturation tints**, not filled brand colours, so a hundred rows
       stay scannable and the one accent that matters still stands out.

   The palette is lifted from the `.sq-st-*` pills the old signing_queue.html
   already had, so this is that treatment promoted to a shared primitive rather
   than a new invention. */
/* The size is an open ruling. `.68rem` renders at 8.2 px under the compact root
   (admin, HHN), below `--fs-2xs` (9 px there), the smallest step of the type scale.
   Under the provider's standard root it renders at 10.9 px, the size it was drawn
   at. A global bump repaints the ribbon on many admin pages and truncates labels
   in the fixed-width rectangle (`--status-ribbon-rectangle-width`); a rule scoped
   to `density-compact` would not. Change it here, or not at all: never from a page. */
.status-ribbon {
    display: inline-block;
    padding: 1px 7px;
    border-radius: 999px;
    font-size: .68rem;
    font-weight: 600;
    line-height: 1.5;
    white-space: nowrap;
    letter-spacing: .01em;
    border: 1px solid color-mix(in srgb, currentColor 16%, transparent);
}
.status-ribbon--rectangle {
    box-sizing: border-box;
    width: var(--status-ribbon-rectangle-width);
    padding-inline: 7px;
    border-left: 3px solid currentColor;
    border-radius: 0;
    overflow: hidden;
    text-align: center;
    text-overflow: ellipsis;
}

/* Needs a staff decision now — an agency request, or a signature to review. */
.status-ribbon--act  { background: var(--status-act-bg); color: var(--status-act-ink); }
/* In progress on our side and nothing wrong: a draft waiting for its fields. */
.status-ribbon--work { background: var(--status-work-bg); color: var(--status-work-ink); }
/* It came BACK — we rejected it, or the provider declined to sign. Split out from
   `work` because the first version put draft, rejected and declined on one amber:
   in the Prepare stage, which is the one place status varies, every row looked
   identical and the colour coding distinguished nothing. A draft is the normal next
   step; a bounce is a problem, and only one of those wants red. */
.status-ribbon--stop { background: var(--status-stop-bg); color: var(--status-stop-ink); }
/* Someone else's turn. Neutral on purpose — there is nothing to do. */
.status-ribbon--wait { background: var(--status-wait-bg); color: var(--status-wait-ink); }
/* Released. */
.status-ribbon--done { background: var(--status-done-bg); color: var(--status-done-ink); }
/* Closed without release. Muted, but still readable — #9ca3af on this tint is
   about 2:1 and fails contrast, so the text is a step darker than "subtle". */
.status-ribbon--dead { background: var(--status-dead-bg); color: var(--status-dead-ink); }

/* ── Inline error ──────────────────────────────────────────────────────────
   Promoted from CPO/CCM's required-diagnosis message. A workflow error is an
   actionable interruption, not muted empty-state prose, and therefore keeps
   the same shape everywhere it appears. */
.inline-error {
    margin: 0;
    padding: var(--space-2) var(--space-3);
    border: 1px solid color-mix(in srgb, var(--status-danger) 22%, var(--border));
    border-left: 3px solid var(--status-danger);
    border-radius: var(--radius);
    background: color-mix(in srgb, var(--status-danger) 6%, var(--surface));
    color: var(--status-danger);
    font-size: var(--fs-sm);
    line-height: 1.4;
    text-align: left;
}

/* ── The one form field ────────────────────────────────────────────────────
   A label stacked over its control. SEVEN pages had written it —
   `.pd-field`, `.hd-field`, `.mh-field`, `.cc-field`, `.cp-field`,
   `.hf-field`, `.vd-field` — with three gap values and three label
   treatments between them. The canonical body is the most common of those
   copies (2026-08-18 count): flex column, .2rem gap, margin cleared because
   the element is usually a <label>, and the small-caps label.

   The label rule is `> span:first-child`, not `> span`, on purpose: a field
   legitimately contains OTHER direct spans — certificationForm renders the
   picked file's name as a hint span after the input — and an unguarded child
   selector would dress those as labels. The label is the first child in
   every migrated field; anything after the control is a `.field-hint`. */
.field { display: flex; flex-direction: column; gap: .2rem; margin: 0; }
.field > span:first-child {
    font-size: var(--fs-2xs);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--text-subtle);
}

/* Small print under or beside a control: helper prose, counts, timestamps,
   meta lines. The most common of the ~70 page-local copies of this pair
   (2026-08-18 count) — several of them written as var(--muted, …), a token
   nothing defines, so the fallback was doing all the work. Pages that need
   MORE than the pair (a state colour, a margin, a grid placement) keep a
   page class ALONGSIDE this one carrying only the extras. */
.field-hint { font-size: var(--fs-xs); color: var(--text-muted); }

/* ── Field navigation ─────────────────────────────────────────────────────
   Prev · "n of m · k required left" · Next. Used by BOTH signing surfaces:
   HDSign's rasterised PDF with absolute overlays, and HDForms' schema rendered
   into real inputs. Behaviour: static/js/signing/fieldNav.js.
   Provenance and the decode it came from: plans/ui-revamp/signeasy/FINDINGS-SIGNEASY-UI.md §2 */
/* ── Field navigation bar ────────────────────────────────────────────────
   Pinned to the bottom of the document pane, which is where a thumb is on a
   phone and out of the way of the page on a desktop. */
.field-nav {
    z-index: 20;
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 8px 12px;
    background: var(--field-nav-backdrop);
    backdrop-filter: blur(4px);
    color: var(--text-invert);
}
.field-nav-cta {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-height: var(--touch-target);
    padding: 4px 12px;
    border: 1px solid var(--chip-border);
    border-radius: var(--radius);
    background: transparent;
    color: var(--text-invert);
    font-size: 13px;
    cursor: pointer;
}
.field-nav-cta:hover { background: var(--nav-hover-subtle); }
.field-nav-info {
    font-size: 13px;
    color: var(--nav-fg-emphasis);
    text-align: center;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* The one state worth colouring: nothing required is outstanding. */
.field-nav-info.is-done { color: var(--field-success); }
/* A signed `.sig-field` is styled in frontend/src/pages/hdSign/hdSign.css. Its
   rules sat here, in the `components` layer, where the page's own `.sig-field`
   rule outranked them and they never showed (SCRUM-1524). */

/* ── Control type size, provider portal only ──────────────────────────────
   MEASURED with scripts/type_probe.py: `input.form-control` computed to 16px on
   a phone and 14px on a desktop throughout the provider portal. That is the
   original bug report — "the text size of inputs is variable depending on like
   if ur on mobile" — and it survived three attempts because each fixed a
   narrower scope than the complaint.

   Two sources, both need closing:
     * tokens.css forces 16px under `@media (any-pointer: coarse)` — the iOS zoom
       floor. Correct, required, and touch-only.
     * most controls are classed `form-control-sm`, which Bootstrap sets to
       `.875rem` → 14px.

   So the floor is stated unconditionally: on touch nothing changes, on desktop
   14 → 16, and the DEVICE stops being an input to type size.

   `body.provider-portal` because admin must not get this — its root is 12px and
   its tables are deliberately dense (and 290 of its controls carry pixel widths
   calibrated to that root). Scoped here rather than written in the portal's own
   template, which is what `check_bare_bootstrap.py` exists to prevent.

   `!important` for the reason tokens.css documents at length: 55 controls carry
   an inline `style="font-size:.8rem"`, and an inline declaration outranks any
   selector a stylesheet can write. */
/* `.hd-field-scaled` is excluded, and this is the one exemption that matters.
   A signing overlay field is not a form control that happens to sit on a page —
   its type size is DERIVED from the rendered page scale
   (`fieldRender.fitFieldText`, one factor for position, size and font, per
   FINDINGS-SIGNEASY-UI.md §1) so that what the provider sees is what
   `services/signing/pdf_stamp.py` will print.

   The floor below is `!important`, which beats the inline size that code sets. So
   the floor won and the derivation lost — SILENTLY, because a field pinned to 16px
   looks perfectly normal on a wide page. It only shows up where the page is small:
   REPORTED at ~300px wide, a date field at 16px wrapped to "07/2 9/20 26" and
   spilled out of its box. That was this rule, added during the type-size work,
   overriding the scaling code.

   The iOS zoom trade is accepted here on purpose: a field that renders below 16px
   will zoom the viewport when tapped, which is annoying. A field that renders at a
   size the paper will not use is a provider signing something other than what they
   were shown, which is not. */
body.provider-portal input:not([type="checkbox"]):not([type="radio"]):not(.hd-field-scaled),
body.provider-portal select:not(.hd-field-scaled),
body.provider-portal textarea:not(.hd-field-scaled),
body.provider-portal .form-control:not(.hd-field-scaled),
body.provider-portal .form-select:not(.hd-field-scaled) {
    font-size: 16px !important;
}

/* The one number, in a fixed slot so values align down the list and can be
   compared without reading them. Muted by default; `.is-strong` is the opt-in
   for "this one crossed the threshold" — on CPO/CCM that is a billable month. */
/* One trailing control. `flex: 0 0 auto` so it never squeezes the name. */
.item-action {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    margin-left: 6px;
}

.item-value {
    /* `0 0 46px` was right when the only value was two digits of minutes. A date
       is ~66px and overflowed the slot leftwards, into the name it was supposed
       to sit beside. `auto` basis with a 46px FLOOR keeps CPO/CCM's alignment
       identical (its values never reach 46px) and lets a date have its width. */
    flex: 0 0 auto;
    min-width: 46px;
    text-align: right;
    align-self: center;
    font-size: .95rem;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    color: var(--content-text-subtle);
}
.item-value.is-strong { color: var(--content-text-strong); font-weight: 600; }

/* ── The one bare button ──────────────────────────────────────────────────
   Icon/text control that sits inside chrome without a resting border. Matches
   Bootstrap `.btn-sm` metrics so it lines up with real buttons beside it.
   Its old page-local copy existed twice with different padding, and its own
   file had two `:hover` rules in a row where the second silently cancelled
   the first. */
.btn-bare {
    border: 1px solid transparent;
    background: none;
    padding: .25rem .5rem;
    border-radius: var(--radius);
    color: var(--text-muted);
    cursor: pointer;
    font-size: var(--fs-sm);
    line-height: 1.5;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.btn-bare:hover { background: var(--surface-sunken); color: var(--text); }

/* ── List controls ────────────────────────────────────────────────────────────
   A compact row that sits directly ABOVE the list it governs — not in the page
   header, which is about what the page is, not how this list is ordered.
   `flex: 0 0 auto` so it keeps its height while the list scrolls beneath it. */
.list-controls {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
    flex: 0 0 auto;
    padding: 6px var(--panel-pad);
    border-bottom: 1px solid var(--border);
    background: color-mix(in srgb, var(--surface) 94%, var(--action-accent-subtle));
}

.workflow-period-nav {
    display: flex;
    align-items: center;
    gap: 4px;
    flex: 0 0 auto;
    padding: 5px var(--panel-pad);
    border-bottom: 1px solid var(--border);
    background: var(--surface);
}
/* Inside a toolbar card the card is the frame: no rule, no inset, no ground. */
.workflow-period-nav--toolbar {
    padding: 0;
    border-bottom: 0;
    background: transparent;
}
.workflow-period-label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-align: center;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.workflow-period-nav > .hd-btn { flex: 0 0 auto; }
.workflow-period-nav > .workflow-period-label.hd-btn { flex: 1 1 auto; }

/* Every workflow opens the same calendar shell and supplies its own statuses.
   Tones stay low-colour and redundant with text: this is not a heatmap. */
.workflow-calendar-intro {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--cluster-gap);
    padding: 8px var(--panel-pad);
}
.workflow-calendar-intro { color: var(--content-text-subtle); font-size: .82rem; }
.workflow-calendar-legend { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 4px 10px; }
.workflow-calendar-legend > span { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.workflow-calendar-legend i { width: 8px; height: 8px; border-radius: 50%; }
.workflow-calendar-legend i.is-neutral { background: var(--content-text-subtle); }
.workflow-calendar-legend i.is-info { background: var(--action-accent); }
.workflow-calendar-legend i.is-warning { background: var(--indicator-warning); }
.workflow-calendar-legend i.is-danger { background: var(--indicator-danger); }
.workflow-calendar-legend i.is-success { background: var(--indicator-success); }
.sm-cal-day.workflow-calendar-day.is-status-neutral { border-left: 3px solid var(--content-text-subtle); }
.sm-cal-day.workflow-calendar-day.is-status-info { border-left: 3px solid var(--action-accent); }
.sm-cal-day.workflow-calendar-day.is-status-warning { border-left: 3px solid var(--indicator-warning); }
.sm-cal-day.workflow-calendar-day.is-status-danger { border-left: 3px solid var(--indicator-danger); }
.sm-cal-day.workflow-calendar-day.is-status-success {
    border-left: 3px solid var(--indicator-success);
    background: var(--indicator-success-bg);
}
.workflow-calendar-label {
    display: block;
    overflow: hidden;
    color: var(--content-text);
    font-size: .72rem;
    line-height: 1.2;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.workflow-calendar-content { display: block; width: 100%; min-width: 0; }
.workflow-calendar .sm-cal-grid { grid-auto-rows: minmax(76px, auto); }
.workflow-calendar-counts {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    margin-top: 3px;
    color: var(--content-text);
    font-size: .68rem;
    line-height: 1.1;
}
.workflow-calendar-counts > span {
    display: flex;
    align-items: center;
    gap: 4px;
    min-width: 0;
}
.workflow-calendar-status-label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.workflow-calendar-status-count {
    margin-left: auto;
    color: var(--content-text);
    font-variant-numeric: tabular-nums;
}
.workflow-calendar-counts > span:has(i.is-neutral),
.workflow-calendar-counts > span:has(i.is-neutral) .workflow-calendar-status-count {
    color: var(--content-text-subtle);
}
.workflow-calendar-counts i { width: 6px; height: 6px; border-radius: 50%; }
.workflow-calendar-counts i.is-neutral { background: var(--content-text-subtle); }
.workflow-calendar-counts i.is-info { background: var(--action-accent); }
.workflow-calendar-counts i.is-warning { background: var(--indicator-warning); }
.workflow-calendar-counts i.is-danger { background: var(--indicator-danger); }
.workflow-calendar-counts i.is-success { background: var(--indicator-success); }

/* NO ROOM IN A NARROW CELL FOR THE REPEATED STATUS WORD. The status word
   repeats the legend's noun in every cell, so below the calendar/mobile
   boundary the repeated word goes away while the tone dot, the numeric count,
   the legend, the selected/focus state and each day's full accessible name
   all survive. An ellipsized word was a page-wide clipped-region failure on
   the visit-coding reference's 390/360px open-sheet checkpoints (2026-08-30),
   and the fix belongs beside the internals it projects, not in a page CSS. */
@media (max-width: 767.98px) {
    .workflow-calendar-status-label { display: none; }
}

/* The pager: Previous · "Page 3 of 12" · Next, under the list it pages.
   The mirror of `.list-controls` — that one sits above the list and governs
   what is in it, this one sits below and governs which slice you are on, so it
   takes its border on the TOP edge.

   Written six times before this existed (`.en-pagination`, `.cw-pager`,
   `.cd-pager` and three copies of the same Bootstrap utility string in
   monitoring and hdforms), with four different label pairs — "← Prev"/"Next →",
   "Prev"/"Next", "Previous"/"Next" — which is the drift `check_shared_components`
   was written for. Definition lives here; the markup lives once in
   `frontend/src/components/Pager.tsx`.

   The POSITION LINE IS THE POINT. Row counts came off every list header on
   2026-08-10 (the list is the count), and "Page 3 of 12" is the one number that
   survived, because it is not a count of anything on screen — it says how much
   of the list you have NOT seen, which nothing else can. */
.pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .75rem;
    flex: 0 0 auto;
    padding: 6px var(--panel-pad);
    border-top: 1px solid var(--border);
}
.pager-position {
    font-size: var(--fs-sm);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* The Picker: a fact with an affordance. It states the current choice and opens
   a sheet to change it, rather than being a form control parked on the page
   (D13 rule 3 — no always-visible form controls outside a sheet). */
.picker {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-height: var(--control-height-dense);
    padding: 3px 9px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    font-size: var(--fs-sm);
    cursor: pointer;
    color: inherit;
    box-shadow: var(--shadow-sm);
    transition: border-color .15s ease, box-shadow .15s ease, background-color .15s ease;
}
.picker:hover { border-color: var(--pf-blue); background: var(--surface-sunken); }
.picker:focus-visible {
    outline: none;
    border-color: var(--pf-blue);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--pf-blue) 18%, transparent);
}
.picker-label { color: var(--text-muted); }
/* The VALUE is the fact worth reading; the label is only there to say what kind
   of fact it is. */
.picker-value { font-weight: 600; }

.picker-options { display: flex; flex-direction: column; }
.picker-option {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 1px;
    width: 100%;
    min-height: var(--control-height-dense);
    padding: 6px 4px;
    border: 0;
    border-bottom: 1px solid var(--border);
    background: none;
    text-align: left;
    cursor: pointer;
    font-size: .85rem;
    color: inherit;
}
.picker-option:last-child { border-bottom: 0; }
.picker-option:hover { background: var(--surface-sunken); }
/* Marking the CURRENT one, which is the rare case — not every row. */
.picker-option.is-current { font-weight: 600; color: var(--pf-blue); }
.picker-option-hint { font-size: .72rem; color: var(--text-muted); font-weight: 400; }

/* ═════════════════════════════════════════════════════════════════════════
   THE APP SHELL — a page that owns the viewport  (opt in with body.has-shell)
   ═════════════════════════════════════════════════════════════════════════

   A page wearing `.page-shell` needs an unbroken chain of definite heights
   above it, or `height: 100%` resolves against an auto-height ancestor, the
   shell collapses to its content and the DOCUMENT scrolls instead of
   `.page-body`. Every portal was solving that privately: the provider base had
   it inline, the HHN island template had `html, body { height: 100% }` of its
   own, and the admin page pins itself with `body.cpo-detail-locked`.

   THE MOBILE HALF IS THE PART THAT KEEPS BEING MISSED. `mobile.css` releases
   the viewport lock below md for every surface:

       html, body { height: auto !important; overflow: visible !important }

   That is right for a document page and wrong for an app shell, and a shell
   that does not answer it simply loses — traced on /provider/activity, <html>
   was 3,691px tall and scrolling, so the period nav, the search box and the
   docked action scrolled away with the rows while the list itself, 3,453px
   tall, had nothing left to scroll.

   `100dvh` NOT `100vh`: the release exists because `100vh` on mobile Safari is
   the height with the URL bar hidden, so a pinned shell sat under the browser
   chrome. `dvh` is the fix for that, which is what makes re-pinning safe rather
   than a re-run of the bug the release prevents.

   `html:has(…)` because <html> is not a descendant of <body> and cannot be
   reached from a body class — the same trick the admin CPO page uses. */
body.has-shell {
    display: flex;
    flex-direction: column;
    height: 100dvh;
    min-height: 0;
    overflow: hidden;          /* the document never scrolls; .page-body does */
}

/* Flex children default to `min-height: auto` and refuse to shrink below their
   content, which breaks every scrollport beneath them. */
body.has-shell > main,
body.has-shell [data-island] {
    flex: 1 1 auto;
    min-height: 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

@media (max-width: 767.98px) {
    /* Provider task pages scroll the document around a viewport-sized workspace.
       Keep their existing document flow: locking either root clips the workspace
       below the toolbar and prevents reaching its bottom on a phone. */
    html:has(body.has-shell):not(:has(body.provider-portal .task-workspace-page)),
    body.has-shell:not(.provider-portal:has(.task-workspace-page)) {
        height: 100dvh !important;
        min-height: 0 !important;
        overflow: hidden !important;
    }
}

/* ═════════════════════════════════════════════════════════════════════════
   PRINT — undo the shell, because paper has no viewport
   ═════════════════════════════════════════════════════════════════════════

   The shell's whole purpose is to pin a page to the viewport and scroll its
   panes internally (`body.has-shell` above: `height: 100dvh; overflow: hidden`).
   Every property that makes that work makes printing wrong, and nothing here had
   a print stylesheet — the only `@media print` in the codebase was one admin
   document template.

   MEASURED on /provider/actions before this block: 67 rows, the list's content
   3749px tall inside an 800px box, `body` fixed at 900px with `overflow: hidden`,
   and Chromium's print output **one page**. About 14 of 67 rows reached paper and
   the other 53 were silently clipped — which is what "the text doesn't render
   when printing" is. Print media changed nothing, because there was nothing to
   change.

   `!important` throughout, and it is the correct use of it: these rules exist
   solely to defeat the layout constraints declared elsewhere in this file, and
   several of those are themselves `!important` (the mobile block directly above).
   A print override that loses to the thing it overrides is decoration.

   Scoped to `@media print` so no screen rule can be affected. */
@page {
    /* Declared so the PAGE BOX is ours and not the browser's default. Without it
       Chrome and Safari each pick their own margin, so the same document printed
       from two browsers has different usable width — and usable width is what
       everything below depends on. */
    size: letter portrait;
    margin: 12mm;
}

@media print {
    /* 0. NOTHING MAY BE WIDER THAN THE PAGE BOX.
       This is the rule that makes printing device-independent, and it is the one
       the earlier attempt was missing.

       When any element forces the layout wider than the page box, the browser
       does not clip — it SHRINK-TO-FITS the whole document, and the scale factor
       is (page box ÷ content width). Content width follows the window on the
       fluid parts, so the same page printed from a narrow window and a wide one
       comes out at two different scales: "i lower width, size changes". Nothing
       in a stylesheet can be seen to change, because nothing does — the browser
       is scaling the finished layout.

       So: no fixed pane widths, no min-widths, no viewport units on paper. Each
       of these is correct on a screen and a scale-trigger on paper.

       Every value below exists elsewhere in this codebase for a screen reason:
       the 340px form list pane, the 320px signing sidebar and its inner
       squash-guard, `min-width: 340px` for a collapse transition that paper does
       not have. */
    html,
    body {
        width: auto !important;
        min-width: 0 !important;
        max-width: none !important;
    }
    .hf-form-sidebar,
    .page-shell,
    .rx-panel {
        width: auto !important;
        min-width: 0 !important;
        max-width: none !important;
        flex-basis: auto !important;
    }
    /* Viewport units measure the SCREEN, which has nothing to do with the sheet. */
    .sr-layout,
    .hf-layout {
        height: auto !important;
        min-height: 0 !important;
        max-height: none !important;
    }

    /* 0b. Truncation is a screen affordance. A list you scroll can ellipsise,
       because the reader can widen the window or hover. On paper the cut text is
       gone, and how much is cut depends on the width — the same inconsistency in
       a different form. Let it wrap instead: a taller row is recoverable, a
       truncated one is not. */
    .item-name,
    .item-meta,
    .item-value,
    .field-nav-info {
        white-space: normal !important;
        overflow: visible !important;
        text-overflow: clip !important;
    }

    /* 1. Release the viewport pin, at every level that participates in it. */
    html:has(body.has-shell),
    body.has-shell,
    body.has-shell > main,
    body.has-shell .provider-shell-inner,
    body.has-shell [data-island],
    .page-shell,
    .page-body,
    .rx-panel,
    .rx-rows,
    .item-list {
        height: auto !important;
        max-height: none !important;
        min-height: 0 !important;
        overflow: visible !important;
        /* A flex item that was sized to fill a fixed box must stop doing that, or
           it keeps the height it was given and clips again one level down. */
        flex: 0 0 auto !important;
        display: block !important;
    }

    /* 2. Sticky is meaningless on paper and Chromium repeats the element on every
          page or floats it over content. The bar states facts, so keep it in flow
          where it appears once, at the top. */
    .sel-strip,
    .list-controls {
        position: static !important;
    }

    /* 3. Navigation is not content. A navbar, a nav drawer, a scrim, a "jump to
          the next field" control and a page dock are all ways to MOVE, and paper
          cannot move — so each is ink spent on something the reader cannot use.
          The tab strip stays: it names which list this is and carries the counts,
          which on a sheet with no other title is the only thing identifying it. */
    /* The ANCESTOR, not the panel. The nav panel is a `.navbar-collapse`, and
       Bootstrap sets `.navbar-expand-lg .navbar-collapse { display: flex
       !important }` — specificity (0,2,0) against `.app-nav-surface`'s (0,1,0),
       so an `!important` here loses to an `!important` there and the navbar
       printed anyway (measured: still 55px tall in print media). Hiding an
       ancestor sidesteps the contest entirely: a hidden parent takes its
       descendants with it, whatever Bootstrap says about them. */
    nav.navbar,
    .provider-navbar,
    .app-nav-surface,
    .app-bar,
    .app-nav-toggle,
    .app-drawer-backdrop,
    .field-nav,
    .rx-sheet-backdrop {
        display: none !important;
    }

    /* 4. Do not split a row, a panel or a signature field across a page break.
          A row cut in half mid-height is the printed equivalent of clipped text —
          the reader cannot tell whether they have the whole record. */
    .item-row,
    .panel,
    .rx-row,
    .sig-field,
    .audit-line {
        break-inside: avoid;
        page-break-inside: avoid;   /* the pre-fragmentation spelling, still honoured */
    }

    /* 5. Backgrounds are not printed by default and their text colour assumes
          them. A dark surface with white text prints white-on-white — invisible,
          which is the worst failure mode here because it looks like missing data
          rather than a styling problem. */
    .field-nav,
    .field-nav-info {
        background: transparent !important;
        color: var(--print-ink) !important;
    }
}

/* ═════════════════════════════════════════════════════════════════════════
   THE NAV DRAWER — off-canvas navigation  (opt in with .app-drawer)
   ═════════════════════════════════════════════════════════════════════════

   Below the navbar's expand breakpoint, a Bootstrap `.navbar-collapse` opens
   DOWNWARD: the menu pushes the entire page down and a nine-entry nav becomes a
   tall stack above the content you were reading. The admin panel has never done
   that — its sidebar slides in from `left: -280px` over the page — and this is
   that behaviour as something any shell can opt into rather than a fourth
   private copy (admin inline, provider inline, HHN inline, …).

   Apply `.app-drawer` to the collapse element and `.app-drawer-backdrop` to a
   sibling overlay. Bootstrap still owns open/closed via `.show`; this only
   changes where the panel is.

   `display: block !important` because Bootstrap hides a closed `.collapse`
   outright — here the panel is always laid out and `left` moves it, which is
   also what makes the slide animate. `.collapsing` animates HEIGHT, which
   fights a horizontal drawer, so it is pinned. */

/* THE NAV ROW, ONCE. These rules were written twice — `.sidebar .nav-link` in
   admin/base.html and `.provider-navbar .nav-link` in provider/base.html — with
   character-identical colours, hover, active and transition. Two copies of the
   same four declarations is not a look shared, it is a look that agrees by
   coincidence until someone edits one of them.

   A portal opts in by putting `app-nav-surface` on the element that WRAPS the
   nav, plus `nav-rail` or `nav-bar` for the axis. Colour is a variable, so a
   portal that wants a different palette overrides four custom properties
   instead of restating the rules. */
.app-nav-surface {
    --nav-fg-strong: var(--text-invert);
}
.app-nav-surface .nav-link,
.app-nav-surface .dropdown-item {
    color: var(--nav-fg);
    transition: all .2s;
    border-radius: var(--radius);
    /* A ROW IS A FLEX ROW. Not decoration: `.nav-icon`'s fixed box and the
       label's ellipsis below are both no-ops in an inline formatting context —
       `overflow` does not apply to a non-replaced inline element, so the
       truncation would have silently done nothing and the icons would have kept
       their intrinsic widths. Declaring the row here is what makes the two
       rules underneath true rather than decorative. */
    display: flex;
    align-items: center;
}
.app-nav-surface .nav-link:hover { color: var(--nav-fg-strong); background-color: var(--nav-hover); }
.app-nav-surface .nav-link.active { color: var(--nav-fg-strong); background-color: var(--nav-active); }

/* A POPUP MENU IS NOT THE NAV SURFACE, and this is the fix for it rendering
   blank (user, 2026-07-30: *"the dropdown for the provider navbar when clicking
   the name is all white and blank"*).

   `--nav-fg` is white because the bar and the rail are dark, and the rule above
   deliberately covers `.dropdown-item` — the drawer's GROUP rows are dropdown
   items sitting directly on that dark panel, and they need it. But Bootstrap's
   `.dropdown-menu` is a white popup, so the account menu's items inherited white
   text on a white background: measured at `rgba(255,255,255,.85)` on
   `rgb(255,255,255)`. Present, clickable, and invisible.

   This is the exact bug class CLAUDE.md is written around — a rule authored for
   one surface reaching a component that never asked for it — and the fix is the
   one that rule prescribes: scope by the surface the element is actually ON,
   rather than adding another property to a reset list. Anything inside a
   `.dropdown-menu` is on a light popup, whatever contains it.

   `:not(.is-drawer)` for the same reason, one step further on — and the fix for
   the half this rule broke on its way in (user, 2026-07-30: *"profile and sign
   out … on mobile header they are black on dark blue"*). "Anything inside a
   `.dropdown-menu` is on a light popup" holds only while the menu IS a popup,
   and below the breakpoint it is not: the drawer flattens it onto the dark panel
   (`background: none`, further down this file), so the account rows became this
   bug's mirror image — dark text, this time on dark blue. The provider nav is
   flat, so those three rows are the only `.dropdown-item` in that panel that is
   not already a group caption, which is why it reads as "the profile and sign
   out buttons" rather than as a whole menu.

   Scope, again, by the surface the element is actually ON. `.app-nav-surface`
   and `.is-drawer` sit on the SAME element, so this asks the panel what it
   currently is rather than leaving source order to settle it against the
   drawer's own rows rule. */
.app-nav-surface:not(.is-drawer) .dropdown-menu .dropdown-item { color: var(--text); }
.app-nav-surface:not(.is-drawer) .dropdown-menu .dropdown-item:hover,
.app-nav-surface:not(.is-drawer) .dropdown-menu .dropdown-item:focus {
    color: var(--text);
    background-color: var(--surface-sunken);
}

/* The axis is the ONLY thing that legitimately differs between a rail and a
   bar: rows stack, so they gap vertically; tabs sit side by side, so they gap
   horizontally and need their own padding to become a target.

   BOTH are `:not(.is-drawer)`. Once either becomes a drawer the axis question is
   settled — it is a column — and the drawer's own row rule is the only one that
   should speak. Without the guard these kept applying inside the drawer at the
   same specificity as that rule, so the padding was decided by source order and
   `margin` and `font-size` were never overridden at all: rail rows got
   `margin: 2px 0` and bar rows `0 2px`, in what is supposed to be the identical
   panel. */
.app-nav-surface.nav-rail:not(.is-drawer) .nav-link { margin: 2px 0; }
/* Font Awesome glyphs carry their own intrinsic width (9-15px), so a column of
   rows with `<i>` then text starts every label at a different x and reads as
   ragged even though all thirty are one family at one size. A fixed box makes
   them a column and the labels share an edge. The margin lives here too, so the
   gap between glyph and label is not a utility class each call site picks. */
.app-nav-surface .nav-icon {
    display: inline-block;
    width: 16px;
    flex: 0 0 16px;
    text-align: center;
    margin-right: .5rem;
}

/* Account photos need the same label separation as navigation icons. */
.app-nav-surface .nav-link > .avatar {
    margin-inline-end: var(--space-2);
}

/* Labels truncate rather than wrap: a nav row that grows to two lines shifts
   every row below it, and the full text is already in the `title`. */
.app-nav-surface .nav-link-text {
    /* `min-width: 0` because a flex item refuses to shrink below its content by
       default, which is the same trap that broke the shell's scrollports. */
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.app-nav-surface.nav-bar:not(.is-drawer) .nav-link {
    margin: 0 2px;
    padding: .5rem .75rem;
    font-size: .9rem;
}

/* ═════════════════════════════════════════════════════════════════════════
   THE TWO FORM CONTROLS EVERY PAGE SHARES
   ═════════════════════════════════════════════════════════════════════════

   `.search-field` (components/SearchField.tsx) and `.select-field`
   (components/Select.tsx). Defined ONCE here, as `PageHead`'s classes are, so
   the component's job is to make the correct markup the easiest markup and the
   class names never reach page code where the next person retypes them
   slightly differently.

   WHAT THEY REPLACE, measured 2026-08-09: 37 search inputs wearing **35
   distinct page-local class names** and 15 different widths, and 128 selects
   each restating `form-select form-select-sm` plus a width of its own. The
   owner's words: the top bars "are just inconsistent … pages should have the
   same font sizes, and shared components and constants."

   ── THE WIDTH IS THE WHOLE RULE ────────────────────────────────────────────
   `min(22rem, 100%)` and `min-width: 0`, not a pixel width. `.page-actions` is
   `flex: 0 0 auto`, so nothing inside it can shrink: a fixed 190px field kept
   its 190px at 390px wide and pushed itself and its neighbours off screen —
   which is the bug `PageHead`'s `searchable` prop was added to work around.
   `min(…, 100%)` gives the field its comfortable size when there is room and
   lets it collapse when there is not, so the strip never wraps and the phone
   never overflows. A page that "needs it narrower" is re-opening that bug.

   Page CSS must not set `width` or `font-size` on either class — that is the
   drift these replaced. Layout classes (margins, order, flex placement) are
   fine and are what `className` on both components is for. */
.search-field,
.select-field {
    width: min(22rem, 100%);
    /* A flex item defaults to `min-width: auto` and refuses to shrink below its
       content — the horizontal twin of the `min-height: 0` trap, and the reason
       the fixed widths above could not be rescued by their container. */
    min-width: 0;
    font-size: var(--fs-sm);
}

/* A select is sized by its longest option, not by prose, so it earns a
   narrower comfortable width than a search box. Same shrink behaviour. */
.select-field { width: min(14rem, 100%); }
/* ── Shared control-surface adapter -----------------------------------------
   Bootstrap supplies control mechanics. :is() composes enough specificity for
   form controls and outline buttons in one paint block, without a second
   background declaration or background-color inheritance. */
:is(.form-control.form-control, .form-select.form-select, .control-surface) {
    --control-surface: var(--control-surface-default);
    border-color: var(--border);
    border-radius: var(--radius);
    background-color: var(--control-surface);
    color: var(--text);
    box-shadow: var(--control-shadow-inset);
    transition: border-color .15s ease, box-shadow .15s ease, background-color .15s ease;
}
.control-surface[data-control-surface="contextual"] {
    --control-surface: var(--control-surface-contextual);
}
/* A quiet surface: the field is a line of text inside a card that already has
   an edge (the paperwork toolbar's search). */
/* The provider selector outranks the portal's 16px field rule the same way the
   compact density below does; coarse pointers keep tokens.css's 16px floor. */
body.provider-portal .control-surface.control-surface.control-surface[data-control-surface="quiet"]:not(.hd-field-scaled),
.control-surface[data-control-surface="quiet"] {
    border: 0;
    box-shadow: none;
    background: transparent;
    padding: 0;
    min-height: 0;
    border-radius: 0;
    font-size: 12px !important;
}
/* A field density a caller asks for by attribute: the paperwork prototype's
   compact fields, 11px in a 37px control with the 7px corner. Coarse pointers
   keep the 16px floor: tokens.css declares it !important in an earlier layer,
   which wins. `!important` here because the form renderer writes sizes inline. */
/* `body.provider-portal` in the selector so this outranks the portal's 16px field rule above it. */
body.provider-portal [data-field-density="compact"] :is(.form-control.form-control.form-control, .form-select.form-select.form-select):not(.hd-field-scaled),
[data-field-density="compact"] :is(.form-control.form-control, .form-select.form-select) {
    font-size: 11px !important;
    min-height: 37px;
    padding: 10px 11px;
    border-radius: 7px;
    color: var(--text-muted);
    box-shadow: none;
}
[data-field-density="compact"] textarea.form-control { min-height: 77px; }
[data-field-density="compact"] .form-check-input { width: 15px; height: 15px; accent-color: var(--action-accent); }
:is(.form-control.form-control, .form-select.form-select, .control-surface):hover {
    border-color: var(--border-strong);
}
:is(.form-control.form-control, .form-select.form-select, .control-surface):focus {
    border-color: var(--action-accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--action-accent) 15%, transparent);
}
:is(.form-control.form-control, .form-select.form-select, .control-surface):disabled {
    --control-surface: var(--control-surface-disabled);
    color: var(--text-muted);
    opacity: 1;
}
:is(.form-control.form-control, .form-select.form-select, .control-surface).is-invalid {
    border-color: var(--indicator-danger);
}
:is(.form-control.form-control, .form-select.form-select, .control-surface).is-invalid:focus {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--indicator-danger) 15%, transparent);
}
.page-shell .btn,
.rx-sheet .btn {
    border-radius: var(--radius);
    font-weight: 500;
    letter-spacing: .005em;
    /* A button's label is one action, and a wrapped label is what makes a
       button read as taller-than-its-neighbours. Bootstrap's `.btn` does not
       set `white-space`, so a long caption ("Check delivery status") fell onto
       a second line and the control grew vertically. Buttons keep their label
       on one line; the toolbar is responsible for overflow, not the control.
       (Tabs already set this; this brings real buttons to the same rule.) */
    white-space: nowrap;
    transition: transform .12s ease, box-shadow .12s ease,
                background-color .12s ease, border-color .12s ease;
}
.page-shell .btn:not(:disabled):hover,
.rx-sheet .btn:not(:disabled):hover { transform: translateY(-1px); }
.page-shell .btn-primary,
.rx-sheet .btn-primary {
    background: var(--action-accent);
    border-color: var(--action-accent);
    box-shadow: none;
}
.page-shell .btn:focus-visible,
.rx-sheet .btn:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--action-accent) 20%, transparent);
}
.page-shell .btn:disabled,
.rx-sheet .btn:disabled { transform: none; box-shadow: none; }

@media (prefers-reduced-motion: reduce) {
    .page-shell *, .rx-sheet *, .dropdown-menu * {
        scroll-behavior: auto !important;
        transition-duration: .001ms !important;
        animation-duration: .001ms !important;
        animation-iteration-count: 1 !important;
    }
}

/* ═════════════════════════════════════════════════════════════════════════
   THE RAIL IS THIN, AND ITS ROWS ARE ICON-OVER-CAPTION
   ═════════════════════════════════════════════════════════════════════════

   MOVED HERE 2026-08-09 from `templates/admin/base.html`'s <style> block,
   where it was scoped to `.sidebar` — a class only the Jinja call site puts on
   the panel. That scoping is why the Wave 3 SPA cutover silently undid it: the
   React shell renders the same component with `panelClass="p-3"` and no
   `.sidebar`, so none of this reached it and `adminApp.css` sized the rail at
   260px instead. The result was the expanded sidebar that base.html's own
   comment says was deleted ("There is no expanded sidebar any more"), on all
   59 converted pages, while the 2 remaining classic pages kept the thin one.

   This is the CLAUDE.md bug class stated exactly: the look belongs in
   surfaces.css keyed on the shared hook (`.app-nav-surface.nav-rail`), never
   in a template <style> keyed on a class one substrate happens to apply.

   `:not(.is-drawer)` RATHER THAN `@media (min-width: 768px)`, which is what
   the moved copy used. The two are equivalent for admin today (its panel
   declares `data-app-nav-breakpoint="768"`), but the media query hardcodes one
   portal's breakpoint into a shared rule — and `.is-drawer` is the panel
   saying what it currently IS, which is the same reason the axis rules above
   are written this way. */
:root {
    /* One number, three consumers: this rail, base.html's `--sidebar-w` alias,
       and the fixed tab pane that must start exactly where the rail ends.

       72px keeps the icon-over-caption treatment while making the rail
       materially narrower (owner, 2026-08-13: 84px was still too wide).
       Captions may use their existing two-line clamp; the full label remains
       available from the link title. */
    --nav-rail-w: 72px;
}

.app-nav-surface.nav-rail:not(.is-drawer) {
    width: var(--nav-rail-w);
    min-width: var(--nav-rail-w);
    flex: 0 0 var(--nav-rail-w);
    /* No horizontal padding: the active row is a full-bleed band in this mode,
       so the rail cannot inset its own children. `!important` beats the `p-3`
       utility both call sites put on the panel, which Bootstrap declares
       important. */
    padding: .5rem 0 !important;
}

/* Icon above its own caption, centred. An icon-only rail makes people hover
   each glyph to re-learn what it is; ~14px of width buys never making them. */
.app-nav-surface.nav-rail:not(.is-drawer) .nav-link {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 5px;
    text-align: center;
    /* SCRUM-257, operator 2026-08-31: the side inset is gone, which is the
       only lever left that costs nothing. The rail is 72px by an explicit
       owner decision (2026-08-13: "84px was still too wide"), so widening it
       is not available; 3px each side was 6px of the 66px the captions had,
       and "Appointments" needs about 72px at this caption size. Full bleed was
       already the active row's treatment, so removing the inset also makes the
       padding agree with the band it paints. */
    padding: 10px 0;
    /* Full bleed: no radius, no side margin, so the active state reads as a
       band across the rail rather than a floating pill. Overrides the rail
       axis rule's `margin: 2px 0` above. */
    margin: 0;
    border-radius: 0;
    /* The badge below is `position: absolute`. Without this it resolves against
       the PANEL (base.html's `.sidebar` is `position: relative`) and pins to the
       top-right of the whole rail instead of its own row — and in the React
       shell, whose panel sets no position, it escapes further still. The
       classic copy relied on the panel and had this defect; it is fixed on the
       way in rather than reproduced. */
    position: relative;
}

/* SCRUM-3106. The `padding: .5rem 0 !important` on the panel above LOSES to
   the caller's Bootstrap `p-3`: Bootstrap is imported into the `vendor`
   layer, and for an `!important` declaration the EARLIEST layer wins, so
   no rule in `components` can beat it. Measured on /admin/: the panel kept
   1rem a side and every caption had 48px, not 72px, which is what broke
   "Managemen/t". The list takes the inset back with a negative margin
   instead, so the row is full bleed and the caption has the whole rail.
   On the list, not the link: the list scrolls (`overflow` is not visible),
   so a link wider than it is clipped. Keyed on `.p-3` so a panel without
   that inset is not pulled outside. */
.app-nav-surface.nav-rail.p-3:not(.is-drawer) .app-nav-list {
    margin-inline: -1rem;
}

.app-nav-surface.nav-rail:not(.is-drawer) .nav-link i {
    margin: 0;
    font-size: var(--fs-lg);
    /* Overrides `.nav-icon`'s 16px fixed box, which exists to make LABELS start
       at a common x — meaningless once the icon is centred above its caption. */
    width: auto;
    flex: 0 0 auto;
}

.app-nav-surface.nav-rail:not(.is-drawer) .nav-link-text {
    /* One step under `--fs-2xs` (0.75rem), which is the smallest token and is
       sized for dense table meta a reader scans, not for a caption they read
       once and then recognise by position and glyph. Stated as a literal rather
       than a new token: it is this rail's caption size, and a token would invite
       a second surface to adopt a size chosen for this narrow column. */
    font-size: .69rem;
    line-height: 1.15;
    letter-spacing: .01em;
    /* Wrap the complete label. The rail is a scrollable list, so an extra line
       is reachable content; clipping a destination name is not an acceptable
       navigation treatment. */
    display: block;
    /* SCRUM-3106: a word that still cannot fit breaks at a syllable with a
       hyphen (`<html lang="en">`) before `overflow-wrap` breaks it anywhere. */
    hyphens: auto;
    /* SCRUM-257. `overflow-wrap: anywhere` breaks INSIDE words, which is what
       gave "Appointm/ents" and "Credential/ing". It is kept anyway, and
       `break-word` was tried and REVERTED: at this width `break-word` declines
       to break a word that does not fit and the caption is then clipped by the
       `overflow: hidden` above — centred text, so it loses characters at BOTH
       ends ("ppointment", "redentialing"). An ugly wrap shows the whole label;
       a clip silently deletes it, and the second is the worse failure.

       The real fix is width, not wrapping: see the link padding below. */
    overflow-wrap: anywhere;
    white-space: normal;
}

.app-nav-surface.nav-rail:not(.is-drawer) .nav-link.active {
    background-color: var(--action-accent);
    color: var(--text-invert);
}

.app-nav-surface.nav-rail:not(.is-drawer) .nav-link .badge {
    position: absolute;
    top: 4px;
    right: 12px;
    margin: 0 !important;
}

/* THE UNREAD COUNT ON AN ICON (`NavBadge` in components/AppNav.tsx). One pill,
   three placements. It is a light mark on the dark nav ground rather than the
   accent, because the accent IS the active row's ground and a badge the same
   colour as the band it sits on is invisible exactly where the reader is.
   Two custom properties so a portal with a light bar can flip it. */
.app-nav-surface,
.app-nav-toggle {
    --nav-badge-bg: var(--text-invert);
    --nav-badge-fg: var(--pf-dark-blue, var(--surface-inverse));
}
.nav-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 9px;
    background: var(--nav-badge-bg);
    color: var(--nav-badge-fg);
    font-size: var(--fs-2xs);
    font-weight: 700;
    line-height: 1;
    letter-spacing: 0;
    white-space: nowrap;
    /* A hairline of the ground colour separates the pill from the glyph it
       overlaps in the rail; on a row it is simply invisible. */
    box-shadow: 0 0 0 1.5px var(--nav-badge-fg);
}
/* Drawer and bar rows are horizontal: the count sits at the row's end. */
.app-nav-surface .nav-link .nav-badge { margin-left: auto; }
/* The rail stacks icon over caption. Pin the count inside the row's right edge
   so extra digits grow inward instead of clipping outside the 72px rail. The
   row is `position: relative` (declared above, for `.badge`). */
.app-nav-surface.nav-rail:not(.is-drawer) .nav-link .nav-badge {
    position: absolute;
    top: 4px;
    right: 4px;
    margin: 0;
    background: var(--pf-bright-blue);
    color: var(--text-invert);
}
/* The hamburger carries the total while the drawer is closed. */
.app-nav-toggle { position: relative; }
.app-nav-toggle .nav-badge {
    position: absolute;
    top: 2px;
    right: 2px;
}

/* Section boundaries are a HAIRLINE, not a caption. At 68px of usable width a
   word like "SCHEDULING" either truncates to nonsense or drops under a readable
   size, and the icon plus its caption already says what each row is — the group
   only needs its edge marked. Same call VS Code, Slack and PF make.

   The heading text stays in the DOM (`font-size: 0`, not `display: none`) so the
   grouping is still announced to a screen reader; only its visual form changes. */
.app-nav-surface.nav-rail:not(.is-drawer) .nav-section-header,
.app-nav-surface.nav-rail:not(.is-drawer) .app-nav-rule {
    display: block;
    height: 1px;
    border: 0;
    padding: 0;
    margin: 8px 12px 6px;
    background: var(--nav-divider-subtle);
    opacity: 1;
    font-size: 0;
    color: transparent;
    overflow: hidden;
    /* A 1px hairline is not a control. Without this the React rail's caption —
       which carries `role="button"` and an onClick — stays clickable, so a
       stray click toggles a collapse the reader cannot see or undo. */
    pointer-events: none;
    cursor: default;
}
/* No rule above the first group — a divider directly under the logo separates
   nothing. */
.app-nav-surface.nav-rail:not(.is-drawer) .nav-section-header:first-of-type { display: none; }

.app-nav-surface.nav-rail:not(.is-drawer) .app-nav-brand { padding: .25rem; margin-bottom: .5rem; }
.app-nav-surface.nav-rail:not(.is-drawer) .app-nav-brand img { width: 40px; }
.app-nav-surface.nav-rail:not(.is-drawer) .app-nav-footer { text-align: center; padding: 8px 2px; }

/* A SECTION WHOSE CAPTION IS NOT A CONTROL MUST NOT STAY COLLAPSED.
   Collapse state is persisted to `localStorage['navSectionsCollapsed']` and
   replayed on every load, by both substrates. But the caption is a
   `pointer-events: none` hairline in the thin rail and a `pointer-events: none`
   caption in the drawer — so a section collapsed at one width becomes rows that
   are gone, in a panel with nothing left to click to bring them back.

   Stated for both, because both have the affordance removed. This also repairs
   the classic drawer, where `.nav-section-links.collapsed { max-height: 0 }`
   (base.html) applied at drawer width against an uncollapsible caption. */
.app-nav-surface.nav-rail:not(.is-drawer) .nav-section-links,
[data-app-nav].is-drawer .nav-section-links {
    max-height: none;
}

/* ═════════════════════════════════════════════════════════════════════════
   THE DRAWER — one off-canvas nav for every portal
   ═════════════════════════════════════════════════════════════════════════

   Gated on `.is-drawer`, which `static/js/core/appNav.js` puts on the panel
   while the viewport is below that panel's own `data-app-nav-breakpoint`. A
   media query cannot read a data attribute, and the two portals genuinely give
   way at different widths — admin's rail at 768 because its content needs the
   space sooner, provider's bar at 992 where a navbar-expand-lg collapses. The
   alternative was the same forty declarations written inside two media queries,
   which is the duplication this whole exercise is removing.

   The cost, stated rather than hidden: between first paint and the module's
   init the panel is not yet marked, so a phone can show one frame of the nav in
   its desktop form. If the module fails to load entirely the nav degrades to a
   plain visible list — wrong-looking, but every destination still reachable and
   obviously broken rather than quietly absent. */
[data-app-nav].is-drawer {
    position: fixed;
    top: 0;
    left: calc(-1 * var(--drawer-w, var(--drawer-width)));
    width: var(--drawer-w, var(--drawer-width));
    min-width: var(--drawer-w, var(--drawer-width));
    /* dvh, not vh: mobile Safari's vh includes the URL bar, so a 100vh panel is
       taller than the screen and its last rows sit under the chrome. */
    height: 100dvh;
    z-index: 1060;
    padding: 1rem .75rem;
    /* The admin rail never had this, so a nav longer than the screen simply
       could not reach its last rows in the drawer. */
    overflow-y: auto;
    transition: left .3s ease;
    /* Bootstrap hides a closed `.collapse` outright; here the panel is always
       laid out and `left` moves it, which is also what makes the slide animate. */
    display: block !important;
}
[data-app-nav].is-drawer.app-nav-open { left: 0; }

/* `.collapsing` animates HEIGHT, which fights a horizontal drawer. Retained
   because Bootstrap may still add it in markup that has not migrated. */
[data-app-nav].is-drawer.collapsing {
    height: 100dvh !important;
    transition: left .3s ease;
}

.app-drawer-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: var(--drawer-backdrop);
    z-index: 1055;
}
.app-drawer-backdrop.show { display: block; }

/* ── Card ─────────────────────────────────────────────────────────────────────
   Byte-identical in templates/admin/base.html and templates/provider/base.html
   before this. Not "similar" — the same four declarations, twice, which is a
   look that agrees by coincidence until someone edits one of them. */
.card {
    border: 1px solid var(--border);
    border-radius: var(--radius);
}
.card-header {
    background-color: var(--pf-light-teal);
    color: var(--pf-dark-blue);
    border-bottom: 1px solid var(--border);
    border-radius: 3px 3px 0 0 !important;
}

/* ── Buttons ──────────────────────────────────────────────────────────────────
   ONE primary colour, for every portal. This was defined only in
   templates/provider/base.html, so the provider and HHN portals used the brand
   blue while the admin panel — 42 of its 101 templates — sat on Bootstrap's
   default #0d6efd. Nobody chose that; admin simply never overrode it, and the
   most common control in the application had two colours depending on which
   portal you were standing in.

   `btn-outline-primary` is banned in admin by CLAUDE.md's UI guidelines and
   intentionally used in the provider/HHN portals; it is defined here so that
   where it IS used it matches, rather than being brand-coloured in one place
   and Bootstrap-coloured in another. */
.btn-primary {
    background-color: var(--pf-blue);
    border-color: var(--pf-blue);
}
.btn-outline-primary {
    color: var(--pf-blue);
    border-color: var(--pf-blue);
}

/* ── Dropdown ─────────────────────────────────────────────────────────────────
   ONE definition of what a menu looks like. This lived in
   templates/provider/base.html as a rule on the BARE element, which had two
   consequences: admin's dropdowns were left at Bootstrap's default (so the two
   portals' menus never matched), and provider's version leaked into every
   shared component that happens to use `.dropdown-menu` — including the nav
   drawer, where its `box-shadow` survived the drawer's reset and rendered each
   flattened group as a subtle box on a dark panel.

   Here it applies to both portals, and it sits ABOVE the drawer's override in
   this same file, so which one wins is a matter of file order and specificity
   rather than of which template happened to load first. */
.dropdown-menu {
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
    padding: var(--space-1);
}
.dropdown-menu .dropdown-item { border-radius: var(--radius-sm); }
.dropdown-item {
    padding: .5rem 1rem;
    font-size: .9rem;
}
.dropdown-item:hover { background-color: var(--pf-light-teal); }

/* ── The parts the shell emits ────────────────────────────────────────────────
   Every one of these was previously defined per portal against a per-portal
   class name (`.sidebar-logo`, `.sidebar-footer`, `.sidebar-divider`,
   `.mobile-menu-btn`, `#sidebarNav`) — which is why provider had no brand in its
   drawer, no footer at all, and a differently-shaped toggle. The shell emits one
   set of names so there is one place to answer "what does a nav footer look
   like". */

/* The panel is a column: brand and footer keep their size, the list takes the
   rest and is the only part that scrolls. */
.app-nav-surface.nav-rail,
[data-app-nav].is-drawer {
    display: flex;
    flex-direction: column;
}

/* THE ONLY SCROLLING PART. `min-height: 0` is required — a flex item's default
   `min-height: auto` refuses to shrink below its content, so without it the list
   pushes the footer off the bottom instead of scrolling.

   `display: block`, not flex: as a flex ITEM with a constrained height, a column
   flex container refused to produce scrollable overflow — children summed to
   1148px while clientHeight and scrollHeight both reported 757, so the
   overflowing rows were simply unreachable. `flex-shrink: 0` on the rows did not
   help. Block children stack the same way and overflow the way `overflow-y:auto`
   expects. */
.app-nav-list {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    scrollbar-width: none;
    display: block;
}
.app-nav-list::-webkit-scrollbar { display: none; }
/* A bar is a row, and its list must not steal the bar's height.
   `:not(.is-drawer)` is load-bearing, not defensive: `.nav-bar` and `.is-drawer`
   sit on the SAME element, so without it this rule keeps applying once the bar
   becomes a drawer — the list stays a horizontal flex row inside a 280px panel
   and the entries wrap into a second column. It also outranks the block default
   above it (0,3,0 vs 0,1,0), so that default never gets a chance to apply. */
.app-nav-surface.nav-bar:not(.is-drawer) .app-nav-list {
    display: flex;
    align-items: center;
    flex: 1 1 auto;
    overflow: visible;
    width: 100%;
}
/* In the drawer, both lists stack — the entries and the account block are one
   column, exactly as the rail's are. */
[data-app-nav].is-drawer .app-nav-list,
[data-app-nav].is-drawer .navbar-nav {
    display: block;
    width: auto;
}

.app-nav-footer {
    flex: 0 0 auto;
    padding: 8px 12px;
    font-size: .7rem;
    color: var(--nav-fg-quiet);
}
.app-nav-surface.nav-bar:not(.is-drawer) .app-nav-footer { display: none; }

.app-nav-rule {
    border: 0;
    border-top: 1px solid var(--nav-divider);
    margin: .5rem 0;
    opacity: 1;
}

/* The toggle. One shape, one position, one colour — admin's floating square was
   `.mobile-menu-btn`, provider's was a Bootstrap `.navbar-toggler` with a
   hamburger background-image, so the same control looked like two controls. */
/* THE MOBILE HEADER. One horizontal band of chrome: the drawer toggle, then the
   title of what you are looking at. Both portals had a band here already —
   admin's tab strip collapsed into one, provider's navbar kept its logo — but
   they held different things, and the toggle belonged to neither: it was
   `position: fixed` in the corner of BOTH, a square hovering over the page.

   The brand is not here. It is in the drawer this opens, so the 44px of header
   goes to the one piece of information that changes: where you are. */
.app-bar {
    display: none;
    align-items: center;
    /* No left padding: the toggle sits flush at the screen edge, which is where
       the admin bar has always put it and where a thumb reaches. The title takes
       its own gap via the flex gap below. */
    padding: 0;
    gap: .25rem;
    min-height: var(--touch-target);
    background: var(--pf-dark-blue);
}
body.has-app-drawer .app-bar { display: flex; }

/* ...and the bar's OWN chrome collapses when it does.

   A `bar` portal nests its panel inside a real header element (provider's
   `<nav class="navbar">`), because on desktop that header IS the row of tabs.
   Once the panel becomes a drawer it goes `position: fixed` and leaves the
   flow — but the header around it does not, so its brand and padding stay on
   screen and the page shows TWO bars: the app-bar above, the emptied navbar
   below it.

   Hiding the header outright is not available: the panel is its descendant, and
   `display: none` on an ancestor hides a fixed-position child too. So the
   header stays in the box tree at zero height and gives up its own content. */
body.has-app-drawer .app-nav-bar-chrome {
    padding-top: 0;
    padding-bottom: 0;
    min-height: 0;
}
body.has-app-drawer .app-nav-bar-chrome > .container-fluid,
body.has-app-drawer .app-nav-bar-chrome > .container {
    padding: 0;
}
/* The brand is not lost — it is at the top of the drawer this opens. */
body.has-app-drawer .app-nav-bar-chrome .navbar-brand { display: none; }

/* The gem chip is not lost either, but unlike the brand it is not decoration —
   so it does not just disappear, it changes rows. `.app-bar-chip` is the
   desktop-row mount (see provider/base.html); the app-bar above carries its
   own second mount that is only ever visible while `.app-bar` itself is
   (`body.has-app-drawer .app-bar { display: flex }`, above), so exactly one
   copy is ever on screen. Without this rule the collapsed navbar keeps just
   enough height for the chip's own padding and renders as a stray second bar
   under the app-bar, floated right by the `ms-auto` that used to have a
   brand and a nav panel on its left to balance against. */
body.has-app-drawer .app-nav-bar-chrome .app-bar-chip { display: none; }

/* Pushes an `app_bar` caller-slot child (the gem chip) to the row's right
   edge, past the title — the same job `ms-auto` does in the desktop navbar. */
.app-bar-end {
    margin-left: auto;
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    padding-block: var(--space-2);
    padding-inline-end: max(var(--space-3), env(safe-area-inset-right));
}

.app-nav-toggle {
    flex: 0 0 auto;
    /* 44px, not 40: this is the primary navigation control on a touch screen,
       so it takes the full WCAG 2.5.5 / --control-height target. */
    width: var(--touch-target);
    height: var(--touch-target);
    border-radius: var(--radius);
    background: none;
    border: none;
    color: var(--text-invert);
    display: none;
    align-items: center;
    justify-content: center;
}
.app-nav-toggle:hover { background: var(--nav-hover); }
/* Shown only while the nav is actually a drawer. The class is on the PANEL, so
   this reads "a toggle whose panel is a drawer" via the shared body state that
   appNav.js keeps in sync. */
body.has-app-drawer .app-nav-toggle { display: flex; }

/* THE DRAWER HAS A BRAND AT THE TOP. Admin's rail carries the logo inside the
   panel, so its drawer opened with the mark; provider's lives in the top bar,
   which the 280px panel covers when it slides over — so its drawer opened with
   a bare list and no way to tell which product you were in.

   Drawer-only by default: on a bar at desktop width the panel IS the row of
   tabs, and a second logo would sit inline next to the first. */
/* PINNED CHANNELS, above the sections (SCRUM-1293).
   Operator, 2026-09-24: "can messages be a pinned tab at the top? just a small
   icon will be fine for it."

   A ROW, NOT A COLUMN, so a second pin costs no vertical space in a rail whose
   sections already fill it. `flex-wrap` rather than a scroller: three pins at
   the narrowest rail is two short rows, which reads, where a hidden overflow
   would silently drop one.

   THE TARGET IS 40px, not the icon's own size. WCAG 2.2 SC 2.5.8 floors a
   pointer target at 24px and this repo has already had to fix a 24x12 switch
   for it; a pin is the smallest control in the chrome and is the one most
   likely to be got wrong. */
.app-nav-pinned {
    display: flex;
    flex-wrap: wrap;
    gap: .25rem;
    justify-content: center;
    margin-bottom: .75rem;
    flex: 0 0 auto;
}
.app-nav-pin {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 40px;
    min-height: 40px;
    /* NO FALLBACK. `var(--radius-sm, .375rem)` would be a second design value
       no token owner controls, and the two would drift the moment the token
       moved. The token exists; if it ever does not, the missing radius is the
       honest symptom. */
    border-radius: var(--radius-sm);
    text-decoration: none;
    color: var(--nav-fg);
    opacity: .85;
}
/* THE SAME TOKENS THE ROWS USE. A pin is the same destination as the row below
   it, so a hand-mixed rgba here would have it highlight differently from the
   thing it points at — and `tokens.css` owns design values precisely so that
   cannot happen. */
.app-nav-pin:hover,
.app-nav-pin:focus-visible {
    opacity: 1;
    color: var(--nav-fg-strong);
    background-color: var(--nav-hover);
}
/* AN OUTLINE, NOT A BOX-SHADOW. The shared ring is a box-shadow in the
   components layer, and a pages/utilities rule that sets `box-shadow` on the
   same element wins on layer order and kills it silently — found on the
   message actions the same day (SCRUM-1289). An outline cannot be lost that
   way. */
.app-nav-pin:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
.app-nav-pin.active {
    opacity: 1;
    color: var(--nav-fg-strong);
    background-color: var(--nav-active);
}
/* The label is the accessible name only: a pin that showed its word would be
   a nav row, which is what the section below already is. */
.app-nav-pin .nav-link-text { display: none; }

.app-nav-brand {
    display: block;
    text-align: center;
    padding: .25rem;
    margin-bottom: .75rem;
    text-decoration: none;
    flex: 0 0 auto;
}
.app-nav-brand img {
    width: 72px;
    height: auto;
    display: block;
    /* Geometric centring is also the OPTICAL centre here: the artwork occupies
       x 14..725 of a 740px canvas — 14px of transparent padding each side,
       symmetric to within half a pixel. `margin-inline` rather than the parent's
       text-align, because the <img> is display:block and text-align does not
       centre a block box. */
    margin-inline: auto;
}
/* A BAR already shows its logo in the bar; a second one inline would sit next to
   the first. It appears only once the panel becomes a drawer that covers it. */
.app-nav-surface.nav-bar:not(.is-drawer) .app-nav-brand { display: none; }
[data-app-nav].is-drawer .app-nav-brand img { width: 56px; }

/* Full-width rows with real targets — desktop pill spacing reads as a cramped
   list in a 280px panel. */
[data-app-nav].is-drawer .nav-link,
[data-app-nav].is-drawer .dropdown-item {
    /* Every property an orientation rule might have set, stated here, so the
       drawer row is fully determined rather than partly inherited from whichever
       axis this panel has at desktop width. */
    margin: 2px 0;
    padding: .65rem .5rem;
    font-size: .9rem;
    min-height: var(--touch-target);
    display: flex;
    align-items: center;
    border-radius: var(--radius);
}

/* GROUPS GO FLAT IN A DRAWER. On desktop a group is a dropdown on a bar and a
   collapsible section on a rail, and both are right there. In a 280px panel
   both are wrong: a popup floating above a slide-out panel is two overlays for
   one choice, and an expanding section turns a five-item menu into a box you
   must open before you can see where you are.

   The admin rail already answered this at exactly this width — it drops the
   chevrons and renders section headers as a 1px hairline — but the answer lived
   in one portal's stylesheet, so the provider bar never learned it. One flat
   list, groups separated by a rule, every destination one tap away. */
[data-app-nav].is-drawer .dropdown-toggle::after { display: none; }

/* THE RULE GOES ABOVE THE CAPTION, in both orientations.
   This was wrong and visibly so: on a bar the caption and its rows share one
   wrapper (`.nav-item.dropdown`), so a border on the wrapper lands above the
   caption — correct. On a rail they are SIBLINGS, header then links, so the
   same border on `.nav-section-links` landed BETWEEN the caption and its own
   rows. One portal read "line, then category, then rows", the other "category,
   then line, then rows", and the category looked attached to the wrong group. */
[data-app-nav].is-drawer .nav-item.dropdown,
[data-app-nav].is-drawer .nav-section-header {
    border-top: 1px solid var(--nav-divider-subtle);
    margin-top: .4rem;
    padding-top: .4rem;
}

/* The group name becomes a caption, not a control — `pointer-events: none` so
   tapping it does nothing rather than toggling something invisible. */
[data-app-nav].is-drawer .nav-item.dropdown > .dropdown-toggle,
[data-app-nav].is-drawer .nav-section-header {
    pointer-events: none;
    min-height: 0;
    padding: .15rem .5rem .25rem;
    font-size: .68rem;
    text-transform: uppercase;
    letter-spacing: .05em;
    opacity: .55;
}
/* No caption above the first group — a heading directly under the logo
   separates nothing. */
[data-app-nav].is-drawer .nav-section-header:first-of-type { display: none; }

/* A menu that is no longer floating must stop LOOKING floating. Every property
   here is undoing a specific one of Bootstrap's (or a portal's) "this is a popup
   over the page" cues; miss one and the group reads as a card inside the panel.
   That is exactly what happened: the provider portal styles the bare element
   (`.dropdown-menu { border; border-radius; box-shadow }`) so the reset cleared
   border/background but left the shadow and the corner radius, and each group in
   the drawer rendered as a subtle box on the dark panel.

   Resetting a fixed list of properties is the fragile part of this arrangement —
   a portal can add a sixth declaration tomorrow and reopen the bug. The durable
   fix is for portals to stop styling bare Bootstrap elements globally; see the
   frontend handoff. */
[data-app-nav].is-drawer .dropdown-menu {
    display: block !important;
    position: static !important;
    transform: none !important;
    float: none;
    width: auto;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    padding: 0;
    margin: 0;
    background: none;
}

/* Rows inside a group are rows, indistinguishable from a top-level one — the
   rule and the caption carry the grouping, so an indent or a tinted box would
   be a third signal saying the same thing. */
[data-app-nav].is-drawer .dropdown-item:hover,
[data-app-nav].is-drawer .dropdown-item:focus,
[data-app-nav].is-drawer .dropdown-item.active {
    background: var(--nav-hover);
    color: var(--nav-fg-strong);
}
/* Sign out is a ROW, not a warning. It was red in the provider drawer and plain
   in the admin one — the same action, two treatments, in two panels a user
   moves between. The house rule is no colour for the common case (see
   CLAUDE.md's UI guidelines), and the divider above it already separates it
   from the routine items, so the colour was carrying nothing the layout wasn't.
   `text-danger` is dropped at the source now; this override stays as the guard
   for any that comes back via Bootstrap markup. */
[data-app-nav].is-drawer .dropdown-item.text-danger { color: var(--nav-fg) !important; }
[data-app-nav].is-drawer .dropdown-divider { display: none; }

/* --- .sign-shell: the two-pane signing shell -------------------------------
   HDSign (`.sr-layout`, document + sidebar) and HDForms (`.hf-form-view`,
   content + sidebar) are the same anatomy written twice, and BOTH of the last
   two rounds' bugs in this area were "the rule existed in only one copy":
   `.hf-form-view` did not stack on a phone until round 11, and `.sr-layout`
   carried a stale `calc(100vh - 62px)` until round 13. This is the one
   definition. Plan: plans/signing/TSX-SIGNING-SURFACES.md, Stage 1.

   IT CARRIES THE STACKING CONTRACT AND NOTHING ELSE. The first Stage 1 was
   withdrawn for absorbing things that only looked shared, so, explicitly:

     display  NOT here — HDSign is always flex; HDForms is `none` until
              `.active`, and a primitive forcing flex would make a hidden form
              view permanently visible.
     height   NOT here — HDSign sets `100dvh` because that page is deliberately
              off the app shell (`no_shell`), where `flex: 1 1 auto` was tried
              and measured 1378px tall in a 900px viewport. HDForms is
              `flex: 1 1 0` inside a definite-height parent. Opposite mechanisms,
              both correct for their page.
     width    NOT here — 320px collapsible vs 300px fixed.

   THE BREAKPOINT IS 768px, NOT the app's usual 767.98px, and that is deliberate.
   The two copies disagreed (HDSign 768, HDForms 767.98) and unifying on the
   Bootstrap boundary was measured first: at exactly 768px HDSign flipped from
   stacked to two panes. 768x1024 is iPad portrait, these are the surfaces a
   provider signs on, and two panes there is a 320px sidebar beside a 448px
   document. Stacking is the better shape for signing at that width, so the
   union keeps it: HDSign is unchanged, and HDForms gains stacking at one width
   where it previously did not. Extending the roomier layout is the safe
   direction; taking it away from the signing page is not. */
@media (max-width: 768px) {
  .sign-shell {
    flex-direction: column;
    /* The COLUMN is the scrollport. `height: auto` was tried on both pages and
       is fatal under `body.has-shell`: a fixed-height `overflow: hidden` box
       clips anything taller with no scrollbar to say so. */
    overflow-y: auto;
    min-height: 0;
  }
}

/* ── Type scale utilities ─────────────────────────────────────────────────────
 * The companion the --fs-* scale never had. tokens.css defines the sizes, but
 * with no class to apply them the only way to size a one-off piece of markup
 * was an inline `style="font-size:.85rem"` — which is exactly the debt
 * standards.d/design-tokens.sh counts, and it stayed high because following the
 * rule required editing a stylesheet for every label.
 *
 * Deliberately NOT named .fs-1…6: Bootstrap already owns those (heading sizes),
 * and redefining a bare Bootstrap class is the global-namespace bug CLAUDE.md
 * opens with. These are the token names, so the class and the variable cannot
 * drift apart.
 */
.fs-2xs { font-size: var(--fs-2xs); }
.fs-xs  { font-size: var(--fs-xs); }
.fs-sm  { font-size: var(--fs-sm); }
.fs-base{ font-size: var(--fs-base); }
.fs-md  { font-size: var(--fs-md); }
.fs-lg  { font-size: var(--fs-lg); }
.fs-xl  { font-size: var(--fs-xl); }
.fs-2xl { font-size: var(--fs-2xl); }

/* Structured citation input shared by credential and payer policy editors. */
.source-links-editor {
    display: grid;
    gap: .5rem;
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
}
.source-links-editor legend {
    float: none;
    width: auto;
    margin: 0;
    font-size: var(--fs-xs);
    font-weight: 600;
    color: var(--text-muted);
}
.source-links-editor__help,
.source-links-editor__empty {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--fs-sm);
}
.source-links-editor__list { display: grid; gap: .4rem; }
.source-links-editor__head,
.source-links-editor__row {
    display: grid;
    grid-template-columns: minmax(9rem, .7fr) minmax(14rem, 1.3fr) auto;
    gap: .5rem;
    align-items: center;
}
.source-links-editor__head {
    padding-inline: .1rem;
    color: var(--text-muted);
    font-size: var(--fs-xs);
    font-weight: 600;
}
.source-links-editor > .btn { justify-self: start; }

@media (max-width: 767.98px) {
    .source-links-editor__head { display: none; }
    .source-links-editor__row { grid-template-columns: minmax(0, 1fr) auto; }
    .source-links-editor__row input:first-child { grid-column: 1 / -1; }
}

/* Admin shell rules moved out of its early inline style. This stylesheet loads
   later, so the shell no longer needs template-local importance merely to beat
   source order. Importance remains only where a Bootstrap utility is itself
   important. */
html.in-iframe .sidebar,
html.in-iframe #sidebar {
    display: none !important;
    width: 0 !important;
    min-width: 0 !important;
    max-width: 0 !important;
    visibility: hidden !important;
}
html.in-iframe .app-nav-toggle,
html.in-iframe .app-drawer-backdrop {
    display: none !important;
}
.patient-tab.drag-clone {
    box-shadow: var(--shadow-drag) !important;
    opacity: 0.9 !important;
}

  /* A contact is a small card of its own (operator, 2026-09-29: "i do like
     the contacts being in like card containers ... just make sure it doesnt
     overflow or look ugly"). SIZED TO ITS CONTENT, left-aligned, never wider
     than its column: the old version was a fixed 20rem button with its
     content centred, which read as a wide empty margin on the left. A long
     name or number truncates inside the card (`.record-identity` owns that)
     rather than pushing past it. */
  .contact-card {
    --bs-btn-hover-bg: transparent;
    --bs-btn-active-bg: transparent;
    --bs-btn-hover-color: inherit;
    --bs-btn-active-color: inherit;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: center;
    gap: var(--space-2);
    box-sizing: border-box;
    min-inline-size: 0;
    inline-size: fit-content;
    max-inline-size: min(100%, 28rem);
    min-block-size: 3rem;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    color: inherit;
    text-align: start;
    white-space: normal;
    overflow: hidden;
  }
  button.contact-card { cursor: pointer; font: inherit; }
  button.contact-card:enabled:hover {
    border-color: var(--border-strong);
    background: var(--selected);
  }
  .contact-card:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
  }
  .contact-card__links {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-3);
    font-size: var(--fs-sm);
  }

  /* A LINKED RECORD IS SHOWN, NOT MENTIONED (SCRUM-1257). Unlike .contact-card,
     which is an identity control inside a frame its container owns, this one
     DOES carry its own bound: it is a different record from the one the pane is
     about, and without an edge it reads as one more fact of the record you are
     already looking at. No accent and no status colour — a linked record is
     ordinary, and colour here would imply something is wrong with it. */
  .linked-record {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-1);
    inline-size: 100%;
    /* CAPPED, because a card is a record and not a banner. Photographed on
       2026-09-24 inside the fax Document tab, where the pane is full width:
       each card ran the whole 900px for a filename and one fact, and read as
       an empty box rather than as a thing to click. It still fills a narrow
       column (the Details panel) and a phone. */
    max-inline-size: 40rem;
    min-inline-size: 0;
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    /* THE SURFACE, not the subtle grey (2026-09-29). Grey on the pane's white
       read as a disabled box, which is the opposite of "a thing to open". */
    background: var(--surface);
    color: inherit;
    text-align: start;
    text-decoration: none;
    white-space: normal;
  }
  .linked-record__kind {
    color: var(--text-muted);
    font-size: var(--fs-xs);
  }
  .linked-record__name { font-size: var(--fs-md); font-weight: 600; overflow-wrap: anywhere; }
  .linked-record__name--unknown { font-weight: 400; color: var(--text-muted); }
  /* EACH FACT IS A LABEL OVER ITS VALUE. Side by side on one line they ran
     together ("Patient Holloway Type Visit Note Portal Demo"), and a reader
     could not tell where one ended. */
  .linked-record__facts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
    margin-block-start: var(--space-1);
    font-size: var(--fs-sm);
  }
  .linked-record__fact { display: inline-flex; flex-direction: column; min-inline-size: 0; }
  .linked-record__fact-label { color: var(--text-muted); font-size: var(--fs-xs); }
  .linked-record__marks {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    margin-block-start: var(--space-1);
  }
  /* The whole card is the target, so it carries the 24px minimum rather than
     relying on a link inside it. */
  .linked-record--open { min-block-size: 24px; cursor: pointer; }
  .linked-record--open:hover { border-color: var(--border-strong); box-shadow: var(--shadow-sm); text-decoration: none; }
  .linked-record--open:hover .linked-record__name { color: var(--action-accent); text-decoration: underline; }
  .linked-record--open:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
  }
  button.linked-record--open { border: 1px solid var(--border); }

/* Quiet chrome for workspaces; dark rail consumers keep their existing default. */
.app-nav-surface.app-nav-light {
  --nav-fg: var(--text-muted); --nav-fg-strong: var(--action-accent);
  --nav-fg-quiet: var(--text-subtle); --nav-hover: var(--surface-subtle);
  --nav-active: transparent; --nav-divider: var(--border);
  --nav-badge-bg: var(--selected); --nav-badge-fg: var(--action-accent);
  background: var(--surface);
}
/* The prototype's link row, on either tone: words only, 13px, centred, a 3px
   rule in the current link's own colour running the bar's full height. Opt in
   with `app-nav-underline` on the panel. */
.app-nav-surface { --nav-account-role: ''; }
.app-nav-underline.nav-bar .app-nav-list { margin: 0 auto; gap: 30px; }
.app-nav-underline.nav-bar .nav-link { font-size: 13px; font-weight: 500; border-radius: 0; min-height: 72px; padding: 0; gap: 7px; background: transparent; }
.app-nav-underline.nav-bar .nav-link .nav-icon { display: none; }
.app-nav-underline .nav-link.active { font-weight: 500; background: transparent; box-shadow: inset 0 -3px currentColor; }
.app-nav-underline .nav-badge { font-size: 10px; padding: 2px 5px; border-radius: 4px; height: auto; min-width: 0; }
/* The account at the right: name over a role, the avatar after them. The role
   is the portal's word, passed as `--nav-account-role`. */
.app-nav-underline.nav-bar .app-nav-account .nav-link { flex-direction: row-reverse; gap: 10px; min-height: 0; font-size: 12px; }
.app-nav-underline.nav-bar .app-nav-account .nav-link .nav-link-text { display: block; }
.app-nav-underline.nav-bar .app-nav-account .nav-link .nav-link-text::after { content: var(--nav-account-role); display: block; margin-top: 4px; font-size: 10px; font-weight: 400; color: var(--nav-fg-quiet); }
.app-nav-underline.nav-bar .app-nav-account .avatar { width: 36px; height: 36px; font-size: 11px; }
.app-nav-underline.nav-bar .app-nav-account .dropdown-toggle::after { display: none; }
}
