/* ============================================================
   NBC Terminal Management System — THE centralized stylesheet
   National Bus Company of Belize

   One file serves login.html, app.html and every view in public/views/.
   The design system is the Nucleus theme (cal.com-derived --cal-* tokens),
   copied from nucleus-scheduler so the two products look like one family —
   same geometry, same type scale, same spacing. What differs is the ink:
   the tokens carry the National Bus Company badge palette.

   ONE THEME. There is no dark mode and no theme toggle. The inherited
   Nucleus dark theme worked by INVERTING the brand token (near-black button
   in light, white in dark); once the primary button is pinned to NBC blue
   that inversion means nothing, and a half-branded dark theme with no way
   out is worse than none. color-scheme: light tells the browser to render
   form controls light even when the operator's OS is dark.

   DEPTH, SOFTLY. The reference is the ticket wizard (public/ticket-flow.css)
   and the owner's three screens — a Tesla dashboard, a task board, a
   proposals console: a soft blue-grey canvas, white cards that FLOAT on it
   with a hairline AND a layered shadow, rounded corners (14px cards, 10px
   controls, pills for badges), air kept tight, ONE saturated blue for the
   action (lit as a gradient on its fills), the logo's azure-to-navy for
   the two dark surfaces (the sidebar and the accent card — one gradient
   family) and a selected segment, emerald and gold for status, pearl beige
   for the warm summary surfaces, red only for error. The palette is the
   badge's own — the owner asked for it "more HD", so it is lit, not new.
   Depth is a LADDER and every surface sits on one rung of it:
     rest      --shadow-card     a card, tile, table, panel on the canvas
     raised    --shadow-lift     the same thing under a hand (hover)
     floating  --shadow-overlay  a menu, a popover, a toast, a notify card
     modal     --shadow-modal    a modal, an overlay, the sale's Complete
   A resting card is ALWAYS hairline + --shadow-card; the two together are
   the depth, and neither alone is. Motion explains what just happened: a
   staggered rise on entry (.rise), the dashboard filling from the top down
   (.cascade), real content fading into the room a skeleton held
   (.fade-in), a 2px lift on hover, a 160ms settle on a menu — all on one
   curve (--ease) and dropped under prefers-reduced-motion.
   DENSE, ON PURPOSE. The owner: "compress everything so they are smaller".
   This is a desk a clerk sits at all day, and the day has to fit above the
   fold of a 1366×768 laptop, so the whole system runs one notch small:
   38px controls (--ctl-h, section 1 — still a comfortable target for a
   mouse or a finger), 32px toolbar rows, 16–18px card padding, 24px page
   gutters, 13px table cells. The RAIL is the one exception: the owner
   asked for it to breathe, so it is a 260px column (--rail-w: a 236px
   card floating 12px in) with 42px items. Type is a six-step scale of
   tokens (--fs-*) and the control heights are three (--ctl-h*), so
   re-toning the density is a handful of edits in section 1. The avatar,
   the switch and the spinner stay literal circles — a radius token cannot
   express a circle.

   Sections
     1. Design tokens                 9. Toast
     2. Reset / base                 10. Modal · 10B. Overlay
     3. Auth page + grid bg          11. Data table + badges
     4. Buttons                      12. Empty / loading
     5. Fields / inputs              13. Stat tiles
     6. Status line                  14. Dropdown menu
     7. App shell · 7B. Header       15. Calendar
     8. Page header                  16. Funds ledger
                                     17. Charts
                                     18. Reports
                                     19. Print

   RULE: components never hardcode a color. Every rule below references a
   token — that is what makes a repalette a change to section 1 and nothing
   else. If a rule needs a colour it does not have, ADD A TOKEN; do not
   inline one. Hex appears only in section 1, in the .bg-grid data URI (CSS
   variables cannot be read inside url()) and in @media print, where the
   values are absolute paper and ink rather than palette.
   ============================================================ */

/* ============================================================
   1. DESIGN TOKENS
   ============================================================ */
:root {
  /* ---- NBC brand ink — sampled per-pixel from public/logo.png (modes over
         25,883 blue / 19,333 green / 47,645 gold badge pixels). Source of
         truth: every colour below is derived from or measured against these. */
  --nbc-blue:  #014F97;  /* the badge's lowercase n. Text-safe: 8.18:1 on white. Canonical brand value + favicon */
  --nbc-green: #017034;  /* the badge's c. 6.23:1 on white. Canonical success token. The calendar re-points it to --nbc-emerald inside .cal (section 15), and the location dot is --nbc-emerald-rich now */
  --nbc-gold:  #C3A245;  /* the ring. 2.45:1 on white — FILL / RULE / MARKER ONLY, NEVER on a glyph. Canonical value + favicon */

  /* ---- The rest of the badge, from the owner's brief — Dark Emerald
         #106639, Steel Azure, Pearl Beige #DDD0A7, Golden Bronze — "more HD,
         or a gradient inspired by the logo". What the file did not already
         carry is below; each is a FILL, a MARK or a SURFACE, and the text
         that may sit on each was measured with it. */
  --nbc-emerald:      #106639;  /* the logo's exact emerald. White on it 7.04:1 (and it on white 7.04:1, on the beige tint 6.13:1) — so it may carry a white glyph: the Done disc, a finished step. The calendar's arrival colour */
  --nbc-emerald-rich: #128A48;  /* emerald, lit — DOTS, BARS, STROKES ONLY. White on it is 4.42:1: NEVER white text on it. As a mark: 4.42:1 on white, 4.23:1 on --cal-bg-muted, 3.43:1 on --nbc-navy, 3.12:1 on the nav gradient 60% down */
  --nbc-gold-bright:  #D4A94A;  /* the ring gold, brighter — the highlight inside a gold GRADIENT (--gold-grad). 2.19:1 on white: never text, never a mark on its own */
  --gold-grad:        linear-gradient(90deg, var(--nbc-gold) 0%, var(--nbc-gold-bright) 50%, var(--nbc-gold) 100%);  /* the auth card's top accent line — a band of the ring, lit in the middle. Never behind type */
  --nbc-beige:        #DDD0A7;  /* Pearl Beige. The HAIRLINE of a warm surface: 1.34:1 on the tint, 1.39:1 on the canvas — decorative. 9.84:1 on --nbc-navy. Never on type */
  --nbc-beige-tint:   #F5EFDD;  /* THE WARM SURFACE — attention and summary blocks, so the app is not all cool grey. A background only. On it: --cal-text-emphasis 15.11 · --cal-text 9.32 · --cal-text-subtle 5.25 · --nbc-blue 7.12 · --nbc-green-ink 6.73 · --nbc-emerald 6.13 · --nbc-gold-ink 5.02 · --cal-error 7.93. --cal-text-muted is only 4.34 — it FAILS here, hence the next token */
  --on-beige-muted:   var(--cal-text-subtle);  /* the muted step ON the tint — 5.25:1 on #F5EFDD. A warm surface re-points --cal-text-muted to this in its own scope, so every label and hint inside clears 4.5:1 without a rule edit */

  /* ---- Gold and green pressed dark enough to be legible. Reach for these,
         never the raw ink, any time the colour lands on type or a thin mark. */
  --nbc-gold-strong: #947824;  /* retired, kept: it was the 3px active-nav marker on the old LIGHT rail (3.33:1 on --cal-nav-active, 3.82:1 on --cal-bg-subtle, 3.36:1 on --cal-bg-emphasis). The dark card's marker is --nav-marker. No rule reads it now; it stays so one that names it resolves */
  --nbc-gold-ink:    #7B631B;  /* the ONLY gold allowed on text. 5.76:1 on white, 5.20:1 on the warn-badge fill */
  --nbc-green-ink:   #016030;  /* green that carries type: .status.ok and .badge-ok text. 7.73:1 on white */

  /* ---- Backgrounds. Surfaces carry the faintest blue cast (h≈210, 3-13 pts
         of channel spread); --cal-bg stays PURE WHITE so no text ratio ever
         depends on the tint. */
  --cal-bg:          #FFFFFF;  /* page canvas, cards, inputs, buttons at rest. Every text floor is measured on this */
  --cal-bg-muted:    #F9FAFC;  /* table row hover, .inp:disabled, .icon-btn:hover fill — 1.06:1 off white */
  --cal-canvas:      #F1F4F8;  /* the page BEHIND the cards (L 0.9018). Soft enough that a white card with its shadow reads as floating, not as a hole. On it: --cal-text 9.71:1 · --cal-text-emphasis 15.74:1 · --cal-text-subtle 5.47:1 · --cal-text-muted 4.52:1 (passes, but prefer -subtle on the bare canvas) · --nbc-blue 7.42:1 */
  --cal-bg-subtle:   #F0F4F8;  /* table head, .badge-off, avatar-menu hover, a filled field, a skeleton's fill on a card — 1.11:1 off white. It is 1.00:1 on --cal-canvas, so a skeleton on the bare canvas swaps it for -emphasis (.skel-on-canvas, section 12). (The sidebar left it when it went dark: --nav-* below) */
  --cal-bg-emphasis: #E0E6ED;  /* a filled field's hover, skeleton peak — 1.14:1 off --cal-bg-subtle; also a skeleton's FILL on the bare canvas, 1.14:1 on --cal-canvas (the band there is --cal-bg-subtle) */
  --cal-bg-inverted: #0D1721;  /* toast. Blue-black, not neutral black, so it belongs to this palette */

  /* ---- Borders. --cal-border is the INTERACTIVE edge and clears 3:1;
         -subtle / -muted stay decorative hairlines. */
  --cal-border-muted:    #E9EEF3;  /* faintest rule: table row dividers */
  --cal-border-subtle:   #DCE2EA;  /* card edges, badge-off border — decorative, 1.30:1 (no bar) */
  --cal-border:          #7E8EA2;  /* inputs, .inp, .icon-btn, .seg, .chip, switch track — 3.34:1 on white, 3.03:1 on --cal-bg-subtle */
  --cal-border-emphasis: var(--nbc-blue);  /* FOCUS. Consumed only by the 4 :focus rules + .icon-btn:hover, so pinning it to blue turns every focus ring brand-blue with ZERO rule edits. 8.18:1 on white, 7.40:1 on --cal-bg-subtle. Not on the dark sidebar: that card focuses in gold (--nav-focus) */

  /* ---- Text */
  --cal-text-emphasis: #101B28;  /* headings, table values, input text — 17.36:1 */
  --cal-text:          #343F4C;  /* body copy, table cells — 10.71:1 (clears the 7:1 AAA floor) */
  --cal-text-subtle:   #566476;  /* secondary copy, .badge-off, disabled input — 6.03:1 (5.25:1 on the beige tint) */
  --cal-text-muted:    #637181;  /* field labels, hints, placeholders — 4.99:1. Only 4.34:1 on the beige tint: a warm surface re-points it to --on-beige-muted. (The dark sidebar's inks are --nav-*, never these) */
  --cal-text-inverted: #FFFFFF;  /* on --cal-bg-inverted — 18.08:1 */

  /* ---- Brand. NBC blue is the primary action, the focus base and every
         selected state. There is no light/dark inversion any more. */
  --cal-brand:          #014F97;  /* THE FLAT BRAND — text links, focus rings, borders, icons, .avatar, checked switch, the calendar's today marker, and the foot of --brand-grad. Gradients go on fills only; everything that is a line or a glyph reads this */
  --cal-brand-emphasis: #013A72;  /* :active PRESSES flat to this. White on it: 11.35:1 */
  --cal-brand-hover:    #0459A8;  /* the foot of --brand-grad-hover. White on it 7.00:1 */
  --cal-brand-text:     #FFFFFF;  /* sits on --cal-brand — 8.18:1; on --brand-grad's lightest point 5.69:1 */

  /* ---- THE ONE SOLID BUTTON, HD. A brighter azure at the top fading into
         NBC blue: the light falls on the side of the hand, so the thing to
         press reads as a lit, physical object — the logo's blue, not a new
         one. Read by .btn-brand, .qa-tile.is-primary and the wizard's
         .tf-btn-primary (--tf-brand-grad aliases it). Every white-on ratio
         is taken at the LIGHTEST stop, the worst point under the label. */
  --brand-grad-top:       #0A66C2;  /* white on it 5.69:1 */
  --brand-grad:           linear-gradient(180deg, var(--brand-grad-top) 0%, var(--cal-brand) 100%);  /* rest: #0A66C2 → #014F97 (white 5.69 → 8.18) */
  --brand-grad-hover-top: #1874D0;  /* hover LIFTS AND BRIGHTENS. White on it 4.72:1. The brief's #1A78D6 measured 4.47:1 — under the floor — so the stop sits at the nearest value that clears it */
  --brand-grad-hover:     linear-gradient(180deg, var(--brand-grad-hover-top) 0%, var(--cal-brand-hover) 100%);  /* hover: #1874D0 → #0459A8 (white 4.72 → 7.00) */
  --on-brand-muted:       #DDE9F6;  /* a caption ON --brand-grad (the primary tile's sub-line): 4.62:1 on the top stop. --on-navy-muted would be 3.11:1 there */

  /* ---- The deep navy. The wizard's sale card already fades into it, so it
         is not new ink. ACCENT, NOT ACTION: the sidebar and the accent card
         (both --nav-grad-top → here), a selected segment. Blue means "press
         this"; navy means "this is where you are". */
  --nbc-navy:      #02264D;  /* white on it 15.14:1 */
  --on-navy:       #FFFFFF;  /* type on --nbc-navy / --nav-grad-top / --cal-brand-emphasis: 15.14:1 / 11.79:1 / 11.35:1 */
  --on-navy-muted: #A9C2E0;  /* captions on navy: 6.45:1 on #02386B, 6.21:1 on #013A72, 8.28:1 on #02264D */
  --on-navy-figure: var(--nav-marker);  /* the accent card's big numeral: the ring gold, lifted. 6.53:1 on #02386B, 8.39:1 on #02264D — that clears the 4.5:1 floor for type of ANY size, so no large-text allowance is being spent and the smaller hero (2.1rem, 1.7rem on a phone) stays legal. DISPLAY ONLY by design all the same: gold type at caption size reads as a warning. A caption stays --on-navy-muted */
  --hero-grad: linear-gradient(135deg, var(--nav-grad-top) 0%, var(--nav-grad-bottom) 100%);  /* the accent card: the sidebar's own two stops, on the diagonal a wide card wants, so the two dark surfaces are one family */
  --on-navy-hover: rgba(255, 255, 255, .10);   /* a wash, not a colour: a hover on a navy surface */
  --on-navy-line:  rgba(255, 255, 255, .16);   /* a rule or an icon box on a navy surface */
  --on-navy-chip:  rgba(255, 255, 255, .22);   /* a count chip inside a navy selected segment (.rs-tab-n). Over --nbc-navy it composites to #3A5674: its white type 7.60:1 */
  /* A ROUND ICON BUTTON ON NAVY — the ticket wizard's back and close, on its
     pill (public/ticket-flow.css). A fill, so the button reads as a thing to
     press on a surface where an outline would be one more line. Measured
     composited over the LIGHTER stop, #02386B (the worst case), with the
     foot in brackets. The fill is only ~1.4:1 off the navy: a shape, not a
     boundary — the glyph is what says "press". The focus ring is --nav-focus
     drawn OUTSIDE the button, on the bare navy: 6.53:1 (8.39:1). */
  --on-navy-btn:       rgba(255, 255, 255, .12);  /* rest: #20507D, its white glyph 8.38:1 (#204062, 10.65:1) */
  --on-navy-btn-hover: rgba(255, 255, 255, .20);  /* under a hand: #356089, its white glyph 6.59:1 (#355171, 8.18:1) */
  --auth-field:    #013A72;  /* the login page's field — the ONE full-bleed brand surface left. = --cal-brand-emphasis; the .bg-grid hexes were computed against it */

  /* ---- Retired from the nav, kept. The active nav item is a frosted pill
         on the dark card now (--nav-active below), so the rail no longer
         reads this wash — but .tk-dep.is-selected (section 18E, the ticket
         window's chosen departure) still does, so it is NOT dead: do not
         delete it. NOT a general selected-row tint: --cal-text-muted is only
         3.94:1 on it. */
  --cal-nav-active: #DAE6F1;  /* off the rail; still the chosen departure's wash */

  /* ---- Semantic. Error stays a true red: none of the logo's colours are ever
         pressed into meaning "error". */
  --cal-error:        #8D1B1B;  /* .status.error, .btn-danger fill, .icon-btn.danger — 9.11:1 on white, white on it 9.11:1 */
  --cal-error-bg:     #F6EDED;  /* .icon-btn.danger:hover fill, .empty-state.error — error text on it 7.92:1 */
  --cal-error-border: #DDBBBB;  /* .empty-state.error ring — replaces an unauditable color-mix() */

  /* ---- The calendar's now-line. The ONE new red the calendar is allowed, and
         deliberately NOT --cal-error: "it is 10:42" is not an error condition,
         and the two must never be confused at a glance. A 2px rule and an 8px
         marker, so the 3:1 non-text bar is what applies — 4.77:1 on white and
         4.50:1 on the weekend tint clear it with room. Never carries type. */
  --cal-now: #D93025;

  /* ---- Backdrop scrim. Neutral black on purpose: it DIMS the page rather than
         tinting it, which is why it is not built out of a palette token. */
  --scrim: rgba(0, 0, 0, .45);
  /* The scrim for a browser that cannot blur, and for an operator who asked
     their OS for less transparency. Heavier because it is doing the whole job
     alone: .45 was chosen to sit UNDER an 8px blur, and without that blur it
     leaves the page behind the panel legible enough to compete with the form in
     front of it — worse than either a clean blur or a clean dim. Read by
     .overlay-backdrop (section 10B) only; .modal-backdrop blurs under the
     same three guards but keeps --scrim throughout. */
  --scrim-strong: rgba(0, 0, 0, .72);
  /* The overlay's blur radius. A token because two properties (prefixed and
     standard) must never drift apart, and because it is the one knob to turn
     if a terminal's hardware struggles. NEVER animate it — see 10B. */
  --overlay-blur: 8px;

  /* ---- Badge trios. Explicit rather than color-mix() chains, so text-on-fill
         is provable instead of emerging from an expression nobody can audit. */
  --badge-ok-bg:     #E6F1EB;  /* .badge-ok fill */
  --badge-ok-text:   #016030;  /* on its own fill — 6.68:1 */
  --badge-ok-border: #94CAAD;  /* --nbc-emerald-rich at 45% into white — the pill's edge in the emerald family. 1.60:1 on the fill — decorative. The text stays --nbc-green-ink */
  --badge-warn-bg:     #F7F3E7;  /* .badge-warn fill — a 13% gold tint */
  --badge-warn-text:   #7B631B;  /* on its own fill — 5.20:1 (the raw gold would be 1.6:1 here) */
  --badge-warn-border: #D2BE85;  /* 1.65:1 on the fill */
  --badge-role-bg:     #E8EFF6;  /* .badge-role fill */
  --badge-role-text:   #014F97;  /* on its own fill — 7.06:1 */
  --badge-role-border: #A6C0DA;  /* 1.62:1 on the fill */

  /* ---- Login grid squares, on the blue field. A whisper by design — 1.13:1
         and 1.28:1 against --auth-field — so they texture the page without
         ever competing with the white auth card sitting on it.
         CSS vars cannot be read inside url() — these RECORD the values; the
         hexes in the .bg-grid data URI must be edited to match. */
  --grid-fill:   #01427F;  /* rect fill   -> %2301427F in the data URI. 1.13:1 on --auth-field */
  --grid-stroke: #014A8C;  /* rect stroke -> %23014A8C in the data URI. 1.28:1 on --auth-field */

  /* ── The navigation is a DARK card ──
     THE ONE PLACE THE SIDEBAR'S COLOURS LIVE. The owner: "the nav bar is
     too white — it needs some colour for better contrast." The floating
     card keeps its inset, corner and shape and takes the logo's blue: a
     vertical gradient from deep azure at the top to the navy at the foot —
     the badge's own field, lit from above. The accent card (section 16)
     wears the same two stops, so the app's two dark surfaces are one
     family. The active item is a FROSTED pill — white at 14% with a
     hairline edge — and the gold marker sits in it; on the dark card the
     ring gold is at home. The avatar menu is NOT part of this: it drops
     over the page and stays a light card on the --cal-* tokens.
     HOW THESE WERE MEASURED: every ratio is taken on the LIGHTER top stop,
     #02386B — the worst case; the foot is darker and every figure there is
     higher. A translucent value is measured composited over #02386B. */
  --nav-grad-top:    #02386B;  /* deep azure. White on it 11.79:1 */
  --nav-grad-bottom: #02264D;  /* = --nbc-navy. White on it 15.14:1. THE FLAT STAND-IN: a rule that needs the card as one colour reads this stop */
  --nav-bg:          linear-gradient(180deg, var(--nav-grad-top) 0%, var(--nav-grad-bottom) 100%);  /* the sidebar card, and the phone drawer. An IMAGE: read it with `background`, never `background-color` */
  --nav-card-edge:   rgba(255, 255, 255, .06);  /* the card's rim — a glint on the gradient, not a line; --shadow-nav does the separating */
  --nav-text:        #D6E2F0;  /* item labels at rest — 8.98:1 (11.53:1 at the foot), 7.19:1 on --nav-hover; also the kbd hint's glyph, 5.19:1 on its keycap */
  --nav-text-strong: #FFFFFF;  /* the wordmark, the location name, the account name, the search text, a hovered label — 11.79:1; 9.44:1 on --nav-hover, 8.90:1 on --nav-field */
  --nav-text-muted:  var(--on-navy-muted);  /* #A9C2E0 — section headings, the LOCATION caption, a soon item, rest icons, the search glyph and placeholder — 6.45:1; 5.16:1 on --nav-hover; 4.87:1 on --nav-field */
  --nav-hover:       rgba(255, 255, 255, .08);  /* a white wash under a hand */
  --nav-active:      rgba(255, 255, 255, .14);  /* the active PILL, frosted. Its type is --on-navy, 7.91:1 on it; the gold marker inside 4.38:1 (a 3px bar needs 3:1) */
  --nav-active-edge: rgba(255, 255, 255, .18);  /* the pill's 1px edge, drawn inset so the label never shifts; also the soon tag's and the kbd keycap's edge, and the search field's edge under a hand */
  --nav-line:        rgba(255, 255, 255, .10);  /* the foot rule, the location block's edge, the search field's edge at rest */
  --nav-field:       rgba(255, 255, 255, .10);  /* the menu search and its kbd keycap. It never brightens: at .14 the placeholder falls to 4.33:1, so hover firms the edge and focus draws the gold ring instead */
  --nav-marker:      #E0BE4A;  /* THE RING GOLD, lifted — the 3px bar in the active pill, the avatar disc, the count badge, the focus ring. 6.53:1 on #02386B, 8.39:1 on #02264D. Never on white (1.81:1) */
  --nav-focus:       var(--nav-marker);  /* keyboard focus on the dark card: 6.53:1 on it, 5.23:1 on --nav-hover, 4.38:1 on the active pill, 4.93:1 against --nav-field. The search field's ring is it at 40% */
  --nav-dot:         var(--nbc-emerald-rich);  /* the location dot — #128A48, 3.12:1 on the gradient 60% down, 3.43:1 at the foot (an 8px mark needs 3:1). ON THE BARE CARD ONLY: a white wash under it sinks it (2.73:1 on a 6% one), which is why the location block has no fill */
  --nav-avatar-ink:  #101B28;  /* on the gold avatar and the gold count badge — 9.62:1 */

  /* ---- Corners. Five sizes, and the name says WHICH kind of thing a rule
         means, so re-toning the whole system is five edits. The avatar disc,
         the switch and the spinner carry 999px / 50% literally: they are
         circles, and a radius token could never express that. The corners
         came down with the density pass (file-top comment): a smaller card
         on the old radius reads as a pill. */
  --radius-sm:  8px;    /* calendar event chips, a sale row's hover, the day pips, the .skeleton bar, the wizard's --tf-r-sm */
  --radius-md:  10px;   /* inputs, .btn-sm, icon buttons, nav items, menu items, the KPI / quick-tile icon box, the wizard's --tf-r */
  --radius-lg:  12px;   /* menus, popovers, toasts, notify cards, small panels, a .skel-block */
  --radius-xl:  14px;   /* cards, tiles, tables, modals, overlays, the auth card, the sidebar card, the wizard's --tf-r-lg */
  --radius-btn: 10px;   /* buttons */
  --radius-pill: 999px; /* badges, pill tracks, segmented controls, pill IDs, a .skel-line */

  /* ---- Density. Three control heights, named by what they sit in, so
         the whole app's size is three numbers and the gutter/gap pair
         below — not forty literals. A segment inside a pill track is the
         track's height less its 3px padding and 1px border, top and
         bottom (calc(... - 8px) where it is read). */
  --ctl-h:       38px;  /* THE control: .btn, .inp, a field's input, the page search, the scope select, a full-size .seg track, the phone's hamburger */
  --ctl-h-sm:    30px;  /* .btn-sm — a row's or a card's own small action */
  --ctl-h-row:   32px;  /* a TOOLBAR ROW: the range pills' track and the date inputs and filter selects beside it, the calendar's nav + Today + Week|Month, a .seg-sm track */
  --gutter:      24px;  /* the page's side and foot padding (.page-card) and the header's bleed into it (.page-hd). 16px on a phone (the block after :root) */
  --gap-section: 16px;  /* between the blocks of a page — tiles, charts, tables, the desk's two columns */
  --app-header-h: calc(var(--ctl-h) + 12px);  /* the search's FALLBACK row (section 7B), drawn only on a screen with no .page-hd for the field to live in: 12px above + the --ctl-h field. The page under a visible row reserves 100vh less this, so a short page does not scroll by the row's height */

  /* ---- The rail. The dark card's width and how far it floats in from the
         screen's edges, and from those the column the page leaves for it —
         named once because the page's left margin (.app-main) and the
         search card's widest reach (7B) both have to agree with the card, or
         a re-sized rail leaves the page tucked under it. */
  --nav-w:     236px;  /* the sidebar card, and the phone drawer */
  --nav-inset: 12px;   /* the card's float off the top, left and bottom edges — and the same again between the card and the page */
  --rail-w:    calc(var(--nav-inset) + var(--nav-w) + var(--nav-inset));  /* 260px: where the page column starts */

  /* ---- Shadows — THE LADDER. Blue-black (rgba 13,23,33), never neutral, so
         a shadow belongs to this palette. Every surface sits on one rung:
         rest → raised → floating → modal. A resting card is ALWAYS the
         hairline (--cal-border-subtle) PLUS --shadow-card; the two together
         are the depth. Hover moves a thing one rung up and 2px north. */
  --shadow-sm:    0 1px 2px rgba(13, 23, 33, .05);   /* a control at rest: outline buttons, icon buttons, pill tracks */
  --shadow-bar:   0 1px 0 rgba(13, 23, 33, .04);     /* the hairline under a sticky bar that wears the canvas (.page-hd, the phone's .mobile-topbar): a shadow, not a border, so it never adds to the bar's height */
  --shadow-card:  0 1px 2px rgba(13, 23, 33, .04), 0 4px 14px rgba(13, 23, 33, .06);    /* REST — every resting card: panel, tile, table-wrap, desk panel, calendar (the dark sidebar and the accent card take --shadow-nav) */
  --shadow-lift:  0 2px 4px rgba(13, 23, 33, .06), 0 12px 28px rgba(13, 23, 33, .10);   /* RAISED — hover on any interactive card, tile or chip */
  --shadow-overlay: 0 2px 6px rgba(13, 23, 33, .08), 0 16px 40px rgba(13, 23, 33, .14); /* FLOATING — dropdown, avatar menu, calendar popover, toast, notify card, mobile drawer */
  --shadow-modal:   0 4px 10px rgba(13, 23, 33, .10), 0 28px 70px rgba(13, 23, 33, .22); /* MODAL — .modal, .overlay, .tk-done */
  /* The solid blue button and the primary quick tile carry the brand's own
     glow (the wizard's), so the one thing to press is also the one thing
     that shines. Hover deepens it. */
  --shadow-brand:       0 1px 2px rgba(1, 79, 151, .24), 0 6px 16px rgba(1, 79, 151, .20);
  --shadow-brand-hover: 0 2px 4px rgba(1, 79, 151, .24), 0 10px 24px rgba(1, 79, 151, .26);
  --shadow-auth:        0 2px 6px rgba(1, 15, 34, .18), 0 30px 80px rgba(1, 15, 34, .35);  /* the auth card floating on the navy field */
  /* The two DARK surfaces at rest — the sidebar and the accent card
     (.page-hero). A navy object on a light canvas needs a shadow that
     belongs to it — the grey --shadow-card reads as dirt under navy — so
     this is the card's own ink, one step deeper than the rest rung. */
  --shadow-nav:         0 2px 6px rgba(2, 38, 77, .16), 0 14px 34px rgba(2, 38, 77, .22);

  /* ---- Motion. One curve and two durations, so every hover, lift and
         settle in the app moves the same way the wizard's do (= --tf-ease). */
  --ease:     cubic-bezier(.2, .7, .2, 1);
  --dur:      200ms;
  --dur-fast: 150ms;

  /* ---- Type scale — six steps. Sizes are tokens so a step is one edit.
         One notch down with the density pass; body stays 14px because it
         is what a paragraph is read at, and the table steps to 13px on
         its own (section 11) where the figures are scanned, not read. */
  --fs-display: 1.75rem;  /* 28px, weight 500, letter-spacing -.02em, line-height 1.1 — KPI values (a tile sizes it down to the room it has, section 13). The accent card's hero figure is its own 2.1rem (section 16) */
  --fs-h1:      1.25rem;  /* 20px, 600, -.02em — page titles */
  --fs-h2:      1rem;     /* 16px, 600, -.005em — panel titles, modal titles, the calendar's range */
  --fs-body:    .875rem;  /* 14px, 400/500 — body copy, empty states, the default of anything unsized */
  --fs-small:   .78rem;   /* 12.5px — notes, sub-lines, hints */
  --fs-micro:   .68rem;   /* 10.9px, 600, uppercase, letter-spacing .06em — table heads, section labels, kickers. --cal-text-muted on white is 4.99:1, which holds at this size: no heading here leans on a large-text allowance */

  /* ---- Calendar geometry. Named because three separate grids (the day
         header, the all-day strip and the scrolling body) have to agree on
         the gutter width to the pixel or the columns visibly step. */
  --cal-gutter-w: 56px;  /* the time gutter, and the header's empty corner */
  --cal-hour-h:   48px;  /* one hour row. Every event position is derived from it */
  --cal-scrollbar: 0px;  /* classic scrollbars steal width from the scrolling body
                            only; the sticky header rows pad by this to stay in
                            step. 0 is correct for macOS/iOS overlay scrollbars —
                            set it from JS (offsetWidth - clientWidth) elsewhere. */

  /* ---- Calendar events. Aliases, not new ink. --cal-event-bg is THE event
         colour: a chip is a 14% tint of it with dark type
         (--cal-text-emphasis, 17:1 on white and barely less on the tint) and
         a solid bar of it down the left; the dots, badges and the popover's
         bar are it at full strength. They exist so a per-event colour can be
         injected as ONE custom property on the element instead of the
         component writing colours by hand. --cal-event-text is the type on a
         FULL-colour surface (the "Next" badge): white, 8.18:1 on the blue and
         7.04:1 on the arrival green — which inside .cal is --nbc-emerald
         (section 15 re-points --nbc-green for that subtree). */
  --cal-event-bg:   var(--cal-brand);
  --cal-event-text: var(--cal-brand-text);
  --cal-event-edge: var(--cal-brand-emphasis);

  /* ---- The funds ledger. Money IN is the badge green pressed dark enough to
         carry type (7.73:1 on white). Money OUT is deliberately NOT the error
         red: an expense is ordinary business — fuel, wages, a part — not a
         fault, and a column of red numbers cries wolf on every receipt until
         nobody reads it. Direction is carried by the type badge and a leading
         minus; the colour only has to make the two columns separable. Only a
         NEGATIVE NET, a day whose cash went backwards, earns the alarm colour,
         and that is the one place red means what red means everywhere else. */
  --ledger-in:      var(--nbc-green-ink);      /* income amounts — 7.73:1 on white */
  --ledger-out:     var(--cal-text-emphasis);  /* expense amounts — 17.36:1 on white */
  --ledger-net-neg: var(--cal-error);          /* a net below zero — 9.11:1 on white */

  /* ---- Chart series (section 17). THIS PALETTE WAS COMPUTED AND VALIDATED,
         NOT CHOSEN. Do not substitute a value on taste, and do not "brighten"
         one to make a chart pop: every number below was measured, and a hand
         picked replacement silently re-opens the failure this closes — two
         series a red-green reader cannot tell apart.

         Run through the house dataviz validator (scripts/validate_palette.js
         in the dataviz method) at mode=light, surface=#FFFFFF, pairs=all, it
         clears every check the tool computes: lightness band · chroma floor ·
         CVD separation across ALL pairs · normal-vision floor · contrast vs
         surface. To re-verify after any edit:
           node validate_palette.js "#014F97,#B45309" \
                --mode light --surface "#FFFFFF" --pairs all

         Two series — income vs expense, the pairing every chart here draws:
           worst all-pairs ΔE 22.4 protan · 27.5 tritan · 30.1 normal. PASS.
         Three series — if a third measure is ever plotted:
           worst all-pairs ΔE 10.6 deutan · 7.2 tritan · 17.1 normal. PASS,
           but that tritan figure sits in the 6–8 floor band, which is legal
           ONLY with a second identity channel — so a three-series chart owes
           a legend AND direct labels, never colour alone.

         WCAG against white: 8.18:1 / 5.02:1 / 7.10:1. Every mark clears the
         3:1 non-text floor with room, and the two-series pair separates by
         LIGHTNESS as well as hue — which is what keeps it readable when the
         report comes off a mono printer.

         --chart-1 IS --nbc-blue, so a chart reads as part of this app rather
         than as something a library drew. And a SINGLE-measure chart uses
         --chart-1 alone: net across nine locations is a magnitude encoding,
         so nine hues would encode nothing. Colour is identity, never size. */
  --chart-1: #014F97;  /* series 1 — income, and the lone hue of every single-measure chart */
  --chart-2: #B45309;  /* series 2 — expenses */
  --chart-3: #6D28D9;  /* series 3 */
  --chart-4: #0D9488;  /* series 4. The four together pass every check on EVERY pair (not just adjacent ones):
                          validate_palette.js "#014F97,#B45309,#6D28D9,#0D9488" --pairs all — which is what a donut
                          needs, since every slice touches every other. Five could not be made to pass: every
                          fifth hue tried fell under the 15 normal-vision floor against amber or the 8 CVD floor
                          against purple or teal. So a part-to-whole chart shows at most FOUR named slices and
                          folds the rest into Other, drawn in --chart-other below, which is deliberately not a
                          series colour. That is the method's own answer ("cut series") and it is why the
                          taxonomy's fifth and sixth categories never get a hue of their own. */
  --chart-other: #94A3B8;  /* the Other fold in a donut: a mid grey, chroma-free on purpose so it reads as "the rest", never as a sixth identity. 2.6:1 on white — a fill, never text */

  /* A chart's chrome — the gridlines, the axes, the zero rule, the crosshair,
     the 2px ring around a dot — introduces NO ink of its own. charts.js
     reaches straight for the tokens this app already has:
       --cal-border-subtle  hairline grid and axis, recessive, never black
       --cal-border         the zero rule and the crosshair (3.34:1 — the
                            reader has to see which day is being read)
       --cal-bg             the surface ring on a dot and the gap between
                            touching bars: it must be the colour of the CARD,
                            not a grey, or the gap reads as a stroke
       --cal-text / -emphasis / -muted   every word on the chart
     Aliases were written here first and then deleted: a second name for
     --cal-border-subtle is one more thing to keep in step, and it let a
     chart's grey drift away from the app's. */
  --chart-height: 240px;  /* the height an EMPTY or loading chart reserves. charts.js draws at 260 (line) / 240 (bar) unless the caller says otherwise, so a card is never taller empty than it is drawn, and the page below never jumps when the numbers arrive */

  /* ---- Type — unchanged */
  --font-sans: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  --switch-knob: var(--cal-bg);  /* white knob on the grey track; becomes --cal-brand-text on the blue checked track (8.18:1) */

  color-scheme: light;  /* one theme. Tells the UA to render form controls light even when the OS is dark */
}
/* The phone's gutter. A token re-pointed here, beside its definition,
   rather than a literal in each phone override: the page's padding and the
   header's bleed into it read the same token, so they can never step out
   of line with each other. */
@media (max-width: 768px) {
  :root { --gutter: 16px; }
}

/* ============================================================
   2. RESET / BASE
   ============================================================ */

/* The hidden attribute must WIN. The browser's own [hidden]{display:none} is a
   UA rule, and any author display — .form-row{display:grid}, .range-custom
   {display:inline-flex} — silently beats it, so el.hidden = true does nothing
   visible. That is exactly how the Expense form kept showing Route and Bus,
   and how the Custom date inputs never folded away on Funds or Reports. One
   !important here, in the reset where it belongs, and hidden means hidden. */
[hidden] { display: none !important; }

/* THE ENTRANCE. Things arrive one after another rather than all at once —
   the login card, and any screen that staggers its cards (the dashboard
   has its own, .cascade, below). --i is the order, set in the markup.
   Exactly the wizard's .tf-rise: `backwards`, not `both`,
   so the start state holds during the delay and the element is RELEASED the
   moment it lands — `both` would pin transform: none on it forever and kill
   the hover lift. */
.rise {
  animation: riseIn .46s var(--ease) backwards;
  animation-delay: calc(var(--i, 0) * 45ms);
}
@keyframes riseIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

/* THE DASHBOARD'S ENTRANCE — the page FILLS FROM THE TOP DOWN. The owner:
   "it fades from the top to the bottom". .rise lifts each card UP into
   place, which reads as the page assembling from below; .cascade drops
   each one 8px DOWN into place, and the view sets --i in reading order
   (top-left → bottom-right), so the fill travels down the screen the way
   the eye does. 60ms apart rather than .rise's 45: the step has to be long
   enough to read as a direction, and short enough that a dozen blocks are
   all down inside a second. `backwards`, never `both`, for .rise's reason:
   the hidden start holds through the delay and the card is RELEASED the
   moment it lands — `both` would pin transform: none on it and kill the
   hover lift. */
.cascade {
  animation: cascadeIn 420ms var(--ease) backwards;
  animation-delay: calc(var(--i, 0) * 60ms);
}
@keyframes cascadeIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }

/* CONTENT LANDING WHERE A SKELETON WAS. The placeholder already holds the
   room, so the real thing must not POP into it: it fades in over 260ms
   with a 2px settle in the cascade's direction (down), and a list staggers
   35ms a row (the view caps --i at 10, so the longest list is whole inside
   half a second). --i is reset HERE because custom properties inherit: a
   list inside a .cascade panel carrying --i: 6 would otherwise wait 210ms
   for a stagger it never asked for. An --i set inline on a row still wins.
   Never on the same element as .cascade — both set `animation`, and this
   later rule would silently take it. */
.fade-in {
  --i: 0;
  animation: fadeInSettle 260ms var(--ease) backwards;
  animation-delay: calc(var(--i, 0) * 35ms);
}
@keyframes fadeInSettle { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: none; } }

/* A menu settles into place rather than appearing: 160ms, a 4px drop and a
   2% scale, on the same curve. Read by .dropdown-menu, .avatar-menu and
   .cal-pop — defined once, here, so the three can never drift apart. */
@keyframes menuIn { from { opacity: 0; transform: translateY(-4px) scale(.98); } to { opacity: 1; transform: none; } }

* { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  font-family: var(--font-sans);
  background: var(--cal-bg);
  color: var(--cal-text);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a { color: inherit; }

.muted { color: var(--cal-text-subtle); }
.emphasis { color: var(--cal-text-emphasis); }

/* Motion off, on request: the one block for the entrances and the lifts
   sits at the END of section 18G, not here. A rule inside a media query
   carries no extra specificity, so `.qa-tile { transition: none }` only
   beats `.qa-tile { transition: … }` (18D) by coming AFTER it — from here
   the block quieted .rise alone and nothing else. The per-component blocks
   (the spinner, the pulse, the overlay's slide) stay beside the thing they
   quiet, each after its own rule. */

/* ============================================================
   3. AUTH PAGE — centered card over the square-box grid
   ============================================================ */
/* The arrival screen wears the company's colour — the ONE full-bleed brand
   surface (the sidebar is dark too, but it is a card) — so the first thing a
   clerk sees each morning is the badge on its own field. The white card
   floats on it under the deepest shadow in the file (--shadow-auth): this is
   the one page with no chrome to hide behind, so the badge, the field and
   one card have to do the branding on their own. */
.auth-page {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 32px 16px;
  position: relative;
  overflow: hidden;
  background: var(--auth-field);
}

/* Signature background: a field of squares that fades out from behind the
   card via a radial mask. The squares used to be rounded (rx='8'); they are
   rx='8' now, in step with every other corner in the app.
   NOTE: the square colors are baked into the inline SVG data URL —
   CSS variables cannot be read inside url(). To recolor, edit the
   hex INSIDE the url() string below (%23 is an encoded #) and keep
   --grid-fill / --grid-stroke in section 1 in step with it. */
.bg-grid {
  position: fixed;
  inset: -20px;
  z-index: 0;
  pointer-events: none;
  /* Two changes from the inherited tile. The squares are ROUNDED (rx 8), in
     step with the rest of the theme — a grid of hard corners behind a 14px
     card would be the last thing still square. And they are BLUE — a whisper
     above the field at 1.13:1 and 1.28:1 (both computed against the
     --auth-field value), which is the same relationship the grey tile had to
     the old white page. Any louder and the texture competes with the card.
     CSS variables cannot be read inside a url() data URI, so these hexes are
     baked in: change --auth-field and these two must be recomputed by hand. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='68' height='68'%3E%3Crect x='4' y='4' width='60' height='60' rx='8' fill='%2301427F' stroke='%23014A8C'/%3E%3C/svg%3E");
  background-size: 68px 68px;
  background-position: center;
  -webkit-mask-image: radial-gradient(ellipse 62% 58% at 50% 44%, #000 0%, transparent 74%);
  mask-image: radial-gradient(ellipse 62% 58% at 50% 44%, #000 0%, transparent 74%);
}

.auth-col {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 380px;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* The card FLOATS on the field. --shadow-auth is the deepest shadow in the
   file because the field is the darkest surface: a shadow that reads on the
   canvas would vanish on navy. It rises in (.rise, set in login.html) so the
   page opens rather than snaps.
   THE GOLD CAP. A 4px band of the ring (--gold-grad) across the card's top
   edge, so the arrival screen carries the badge's three colours — the blue
   field, the white card, the gold ring — before a word is read. It is a
   first background LAYER rather than a pseudo-element: clipped to the
   border-box it follows the card's own corner, needs no overflow: hidden
   that could clip a focus ring, and sits under no type. The top border
   goes transparent so the band starts at the very edge instead of under a
   grey hairline. */
.auth-card {
  width: 100%;
  background: var(--gold-grad) top / 100% 4px no-repeat border-box, var(--cal-bg);
  border: 1px solid var(--cal-border-subtle);
  border-top-color: transparent;
  border-radius: var(--radius-xl);
  padding: 32px;
  text-align: center;
  box-shadow: var(--shadow-auth);
}

/* The badge is the anchor of the card, not a favicon sat above the title:
   76px against the 380px card (the same fifth of its width the old 88 on
   400 was). 12px under it — a circle's bottom tangent donates a few px of
   optical air, so the tighter gap is what actually groups the mark with
   the title. */
.auth-logo { display: flex; justify-content: center; margin-bottom: 12px; }
.auth-logo svg { width: 76px; height: 76px; }
/* NBC ships a raster badge (public/logo.png) rather than an inline mark —
   contain, don't crop, or the gold ring loses its edge. No border-radius:
   the badge is a CIRCLE on a white field, so the rounded-square crop it
   inherited from Nucleus's square mark has nothing to round off. */
.auth-logo img { width: 76px; height: 76px; object-fit: contain; display: block; }

.auth-title { margin: 0 0 4px; font-size: var(--fs-h1); font-weight: 700; color: var(--cal-text-emphasis); letter-spacing: -.02em; }
.auth-subtitle { margin: 0 0 20px; font-size: .85rem; color: var(--cal-text-subtle); }
/* The one solid button on the page, a touch taller than a form control so
   it reads as the destination of the form rather than one more row of it. */
.auth-card .btn-brand.btn-block { height: calc(var(--ctl-h) + 4px); font-size: .9rem; }

/* Outside the card, so it is on the FIELD — a muted grey here would be
   unreadable. --on-navy-muted is 6.21:1 on the field. */
.auth-footer { margin-top: 18px; font-size: .76rem; color: var(--on-navy-muted); }
/* The release under it, same ink, a step smaller — a fact to quote, not read. */
.auth-version { margin-top: 4px; font-size: .7rem; color: var(--on-navy-muted); font-variant-numeric: tabular-nums; }

/* Divider with centered label */
.divider { display: flex; align-items: center; gap: 12px; margin: 18px 0; }
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--cal-border-subtle); }
.divider span { font-size: .76rem; color: var(--cal-text-muted); }

/* ============================================================
   4. BUTTONS
   ============================================================ */
/* --ctl-h (38px): dense, still a clean target; weight in the type; a 1px
   lift on hover and back down on press. The transition names its four
   properties rather than `all`, so a colour token change never animates on
   load. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  height: var(--ctl-h);
  padding: 0 14px;
  border: 1px solid transparent;
  border-radius: var(--radius-btn);
  background: var(--cal-bg);
  color: var(--cal-text);
  font-family: inherit;
  font-size: .85rem;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease),
              opacity var(--dur-fast) var(--ease);
  text-decoration: none;
  white-space: nowrap;
}
.btn:disabled { opacity: .45; cursor: default; }
.btn-block { width: 100%; }

/* THE ONE SOLID BUTTON, HD. The brand gradient — a brighter azure at the
   top fading into NBC blue (--brand-grad, white 5.69:1 at its lightest) —
   with the brand's own glow (the wizard's): hover LIFTS and BRIGHTENS the
   whole gradient (--brand-grad-hover, 4.72:1 at its lightest), press drops
   back and goes FLAT and dark (--cal-brand-emphasis, 11.35:1) — the light is
   on the side of the hand, so the button reads as a physical thing. A
   gradient cannot be transitioned, so the fill swaps at the hover while
   the lift and the glow still ease; nothing is lost that a hand would see. */
.btn-brand {
  background: var(--brand-grad);
  color: var(--cal-brand-text);
  border-color: transparent;
  box-shadow: var(--shadow-brand);
}
.btn-brand:not(:disabled):hover { background: var(--brand-grad-hover); transform: translateY(-1px); box-shadow: var(--shadow-brand-hover); }
.btn-brand:not(:disabled):active { background: var(--cal-brand-emphasis); transform: none; box-shadow: var(--shadow-brand); }
/* A dead button must not shine. .btn:disabled only fades it, and a faded
   gradient under a faded glow still reads as the lit thing to press — so a
   disabled one goes back to the FLAT brand with no glow, and the .45 fade
   on top says "not now". Disabled type is exempt from the contrast floor. */
.btn-brand:disabled { background: var(--cal-brand); box-shadow: none; }

/* White with a hairline and the control shadow; the edge darkens to the
   interactive border on hover so a pointer knows it landed. */
.btn-outline {
  background: var(--cal-bg);
  color: var(--cal-text-emphasis);
  border-color: var(--cal-border-subtle);
  box-shadow: var(--shadow-sm);
}
.btn-outline:not(:disabled):hover { border-color: var(--cal-border); transform: translateY(-1px); box-shadow: var(--shadow-lift); }
.btn-outline:not(:disabled):active { transform: none; box-shadow: var(--shadow-sm); }

.btn-ghost { background: transparent; border-color: transparent; color: var(--cal-text-subtle); box-shadow: none; }
.btn-ghost:hover { background: var(--cal-bg-subtle); color: var(--cal-text-emphasis); }

/* Compact variant for row / toolbar actions. */
.btn-sm { height: var(--ctl-h-sm); padding: 0 10px; font-size: .78rem; gap: 5px; border-radius: var(--radius-md); }
.btn-sm svg { width: 14px; height: 14px; }

/* Destructive confirm. The one place red is a fill: same geometry as the
   brand button, the control shadow rather than the glow. */
.btn-danger {
  background: var(--cal-error);
  color: var(--cal-text-inverted);
  border-color: var(--cal-error);
  box-shadow: var(--shadow-sm);
}
/* Hover resolves to #7A1B1D; white on it is 10.49:1. */
.btn-danger:not(:disabled):hover { background: color-mix(in srgb, var(--cal-error) 85%, var(--cal-text-emphasis)); transform: translateY(-1px); }
.btn-danger:not(:disabled):active { transform: none; }
.btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--cal-bg), 0 0 0 4px var(--cal-brand); }

.btn svg { width: 15px; height: 15px; flex: none; }

/* ============================================================
   5. FIELDS / INPUTS
   ============================================================ */
.field { display: block; text-align: left; margin-bottom: 12px; }
.field > span { display: block; font-size: .76rem; font-weight: 600; color: var(--cal-text-emphasis); margin-bottom: 5px; }
.field input {
  width: 100%;
  height: var(--ctl-h);
  padding: 0 12px;
  border: 1px solid var(--cal-border);   /* the 3.34:1 interactive edge, kept */
  border-radius: var(--radius-md);
  background: var(--cal-bg);
  color: var(--cal-text-emphasis);
  font-family: inherit;
  font-size: .85rem;
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.field input::placeholder { color: var(--cal-text-muted); }
.field input:hover { border-color: var(--cal-text-subtle); }
/* --cal-border-emphasis IS --nbc-blue, so focus is brand-blue everywhere
   without a single rule naming a colour. The compliant indicator is the
   BORDER (8.18:1); the 30% ring is a halo on top of it. */
.field input:focus {
  outline: none;
  border-color: var(--cal-border-emphasis);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--cal-border-emphasis) 30%, transparent);
}

/* Password field with show/hide toggle */
.field-pw { position: relative; }
.field-pw input { padding-right: 38px; }
.pw-toggle {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  width: 26px; height: 26px; display: grid; place-items: center;
  background: none; border: none; border-radius: var(--radius-md);
  color: var(--cal-text-muted); cursor: pointer;
}
.pw-toggle:hover { color: var(--cal-text-emphasis); }
.pw-toggle svg { width: 16px; height: 16px; }
/* Show one eye at a time via a class on the button (robust — not the hidden attr). */
.pw-toggle .eye-hide { display: none; }
.pw-toggle.revealed .eye-show { display: none; }
.pw-toggle.revealed .eye-hide { display: block; }

/* Label row with a trailing action (e.g. Password … Forgot?) */
.field-label-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px; }
.field-label-row label { font-size: .78rem; font-weight: 500; color: var(--cal-text-emphasis); }
.field-label-row .link-inline { font-size: .76rem; font-weight: 500; color: var(--cal-text-subtle); }
.field-label-row .link-inline:hover { color: var(--cal-text-emphasis); }

.link-inline {
  background: none; border: none; padding: 0; font: inherit; font-weight: 500;
  color: var(--cal-text-emphasis); cursor: pointer; text-decoration: none;
}
.link-inline:hover { text-decoration: underline; }

/* ── In-app form controls (modals, views) ── */
.form-row { display: grid; gap: 5px; margin-bottom: 12px; }
.form-row > label { font-size: .76rem; font-weight: 600; color: var(--cal-text-emphasis); }
.form-hint { font-size: var(--fs-small); color: var(--cal-text-muted); margin-top: 2px; }

.inp {
  width: 100%; height: var(--ctl-h); padding: 0 12px;
  border: 1px solid var(--cal-border); border-radius: var(--radius-md);   /* the 3.34:1 interactive edge, kept */
  background: var(--cal-bg); color: var(--cal-text-emphasis); font: inherit; font-size: .85rem;
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.inp:hover:not(:disabled):not(:focus):not([readonly]) { border-color: var(--cal-text-subtle); }
.inp:focus {
  outline: none; border-color: var(--cal-border-emphasis);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--cal-border-emphasis) 30%, transparent);
}
.inp::placeholder { color: var(--cal-text-muted); }
.inp:disabled { background: var(--cal-bg-muted); color: var(--cal-text-subtle); cursor: not-allowed; }
select.inp { cursor: pointer; }
textarea.inp { height: auto; min-height: 72px; padding: 8px 12px; resize: vertical; line-height: 1.5; }

/* A FIELD INSIDE A CARD is filled, not outlined — the wizard's rule. The
   card is white, the places you type are grey, and the one you are in turns
   white with the brand ring. The eye finds the inputs by tone before it
   reads a label. Scoped to the three card surfaces that hold forms, so a
   field standing on the canvas (a filter, a search) keeps its outline. */
.modal .inp, .overlay .inp, .wiz-card .inp { background: var(--cal-bg-subtle); border-color: transparent; }
.modal .inp:hover:not(:disabled):not(:focus):not([readonly]),
.overlay .inp:hover:not(:disabled):not(:focus):not([readonly]),
.wiz-card .inp:hover:not(:disabled):not(:focus):not([readonly]) { background: var(--cal-bg-emphasis); border-color: transparent; }
.modal .inp:focus, .overlay .inp:focus, .wiz-card .inp:focus {
  background: var(--cal-bg); border-color: var(--cal-border-emphasis);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--cal-border-emphasis) 30%, transparent);
}

/* Input with a trailing button (password + Generate). The button and the
   field are both --ctl-h, so the row lines up with every other field in the
   modal. */
.field-with-action { display: flex; gap: 8px; align-items: center; }
.field-with-action .inp { flex: 1; min-width: 0; }
.field-with-action .btn { flex: none; }

/* A generated password, shown exactly once. Monospace and letter-spaced
   because somebody has to read it off the screen and type it correctly —
   and select-all on click so it can be copied without the button. */
.password-reveal {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px;
  border: 1px solid var(--cal-border); border-radius: var(--radius-md);
  background: var(--cal-bg-muted);
}
.password-reveal code {
  flex: 1; min-width: 0;
  font-family: var(--font-mono); font-size: .95rem; font-weight: 600; letter-spacing: .04em;
  color: var(--cal-text-emphasis);
  word-break: break-all;
  user-select: all;
}
.password-reveal-actions { display: inline-flex; gap: 5px; flex: none; }
.password-reveal-actions .icon-btn { width: 28px; height: 28px; }
.password-reveal-actions .icon-btn svg { width: 14px; height: 14px; }

/* Segmented control (All / Active / Inactive, Week | Month, Cash | Card).
   A grey pill TRACK with the chosen segment in NAVY — the task board's
   Card | List. Navy, not blue: a selected segment is a state, and blue is
   reserved for the thing to press. The track has no shadow: it sits in the
   surface rather than on it. */
.seg {
  display: inline-flex; padding: 3px; gap: 2px;
  background: var(--cal-bg-subtle);
  border: 1px solid var(--cal-border-subtle);
  border-radius: var(--radius-pill);
  box-shadow: none;
  overflow: visible;
}
/* The track is --ctl-h overall, so it stands level with a button beside it;
   a segment is that less the track's 3px padding and 1px border. */
.seg button {
  height: calc(var(--ctl-h) - 8px); padding: 0 12px; border: 0; border-radius: var(--radius-pill);
  background: transparent; color: var(--cal-text-subtle);
  font: inherit; font-size: .78rem; font-weight: 600; line-height: 1; cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.seg button:hover { color: var(--cal-text-emphasis); background: var(--cal-bg); }
.seg button.on { background: var(--nbc-navy); color: var(--on-navy); }   /* 15.14:1 */
.seg button + button { border-left: 0; }
.seg button:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--cal-brand); }

/* Toggleable filter chip: a pill with a hairline; chosen, it takes the
   brand TINT trio (7.06:1) rather than solid blue, so a row of chosen chips
   never out-shouts the one solid button on the page. */
.chip {
  border: 1px solid var(--cal-border-subtle); background: var(--cal-bg); color: var(--cal-text-subtle);
  border-radius: var(--radius-pill); padding: 4px 10px; font: inherit; font-size: .72rem; font-weight: 500; cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.chip:hover { border-color: var(--cal-border); color: var(--cal-text-emphasis); }
.chip.on { background: var(--badge-role-bg); color: var(--badge-role-text); border-color: var(--badge-role-border); }

/* Two-column layout (collapses on mobile) */
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; }
.two-col .col-h { font-size: .92rem; font-weight: 600; margin: 0 0 2px; color: var(--cal-text-emphasis); }
@media (max-width: 720px) { .two-col { grid-template-columns: 1fr; gap: 4px; } }

/* 34px: an icon button mostly rides in a table row (the row menu, the
   users' actions), and at the full control height it would set the row's
   height instead of the 13px text doing so. */
.icon-btn {
  width: 34px; height: 34px; display: grid; place-items: center;
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-md);
  background: var(--cal-bg); color: var(--cal-text-subtle); cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
/* .icon-btn is not a .btn, so .btn:disabled never reaches it — a disabled icon
   button would otherwise sit at full strength while doing nothing. Opacity,
   not a colour, so this needs no new token. */
.icon-btn:disabled { opacity: .4; cursor: default; }
/* Reads --cal-border-emphasis, so the outline now snaps grey -> NBC blue on
   hover. Stronger than stock Nucleus, and deliberate: blue means "you are
   interacting with this" everywhere else in the app — which is exactly why the
   :not(:disabled) guard is here: a dead control must never claim that. */
.icon-btn:not(:disabled):hover { color: var(--cal-text-emphasis); border-color: var(--cal-border-emphasis); background: var(--cal-bg-muted); }
.icon-btn svg { width: 15px; height: 15px; }
.icon-btn.danger:not(:disabled):hover { color: var(--cal-error); border-color: var(--cal-error); background: var(--cal-error-bg); }

/* DELIBERATE ROUND EXCEPTION (2 of 3). A square track with a square knob does
   not read as a switch — it reads as a broken segmented control. Google keeps
   its switch round for the same reason. Do not square these two radii. */
/* 36×20 with a 14px knob 3px in from each end, so the knob travels
   36 − 3 − 14 − 3 = 16px. */
.switch { position: relative; display: inline-block; width: 36px; height: 20px; }
.switch input { opacity: 0; width: 0; height: 0; }
.switch span { position: absolute; inset: 0; background: var(--cal-border); border-radius: 999px; transition: .15s; cursor: pointer; }
.switch span::before { content: ''; position: absolute; width: 14px; height: 14px; left: 3px; top: 3px; background: var(--switch-knob); border-radius: 50%; transition: .15s; }
.switch input:checked + span { background: var(--cal-brand); }
/* Knob switches to --cal-brand-text on the filled track — white on NBC blue,
   8.18:1 — so the knob never dissolves into the bar it sits on. */
.switch input:checked + span::before { transform: translateX(16px); background: var(--cal-brand-text); }

/* ============================================================
   6. STATUS LINE
   ============================================================ */
.status { margin-top: 12px; min-height: 1.2em; font-size: .8rem; color: var(--cal-text-subtle); }
.status.error { color: var(--cal-error); }
/* The badge green pressed dark enough to carry type: 7.73:1 on white. */
.status.ok { color: var(--nbc-green-ink); }

/* ============================================================
   7. APP SHELL + NAVIGATION
   The nav's CONTENT comes from config/navigation.mjs via /config.js —
   this section only dresses whatever app.js renders.
   ============================================================ */
/* THE SIDEBAR IS A DARK CARD. It floats --nav-inset (12px) in from the
   screen's edge on the canvas like every card does — fixed, so it never
   scrolls with a long ledger, and the inset is what lets it read as an
   object on the page
   rather than a wall the page is bolted to — but where every other card is
   white it wears the logo's blue (--nav-bg: deep azure at the top into the
   navy at the foot). The owner found the white rail too white; the one
   thing that is on every screen now carries the company's colour, and the
   white page beside it reads against it. EVERY colour here is a --nav-*
   token from section 1, measured on the dark card — no --cal-* ink below
   is allowed to touch it, except inside the light avatar menu. The border
   is a glint, not a line; the navy-tinted --shadow-nav separates it.
   ROOMY, ON PURPOSE. The density pass took the rail down with everything
   else and the owner found it cramped: "expand the padding and increase the
   size". The page stays dense; the rail — read at a glance from across the
   counter, and now the one place the terminal is named — does not. Sizes
   only: every ink below is the section-1 pair it was measured as. */
.sidebar {
  position: fixed; left: var(--nav-inset); top: var(--nav-inset); bottom: var(--nav-inset); width: var(--nav-w); z-index: 40;
  display: flex; flex-direction: column; gap: 6px;
  background: var(--nav-bg);
  /* The card's own ink, so anything inside that names no colour of its own
     inherits one that reads on the navy rather than the body's dark text.
     (The light avatar menu resets it to --cal-text on itself.) */
  color: var(--nav-text);
  border: 1px solid var(--nav-card-edge);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-nav);
  padding: 16px 16px 14px;
}
/* The wordmark wraps rather than truncates: "NBC Terminal Management" is
   the product's name, and two lines of it beside the badge read better
   than an ellipsis. */
.sidebar-brand { display: flex; align-items: center; gap: 10px; font-size: 1rem; font-weight: 600; line-height: 1.2; letter-spacing: -.01em; color: var(--nav-text-strong); padding: 2px 6px 16px; }
.sidebar-brand > span { min-width: 0; }
/* 34px, and cut to a CIRCLE. public/logo.png is an RGB file with no alpha:
   the badge is drawn on a white square, and on the navy card those white
   corners would frame it as a white tile. The ring runs out to ~250 of the
   image's 256px radius, so a 50% crop loses nothing of the badge and takes
   only the corners — then the gold ring sits straight on the navy it was
   drawn against. The phone topbar's copy (.mobile-brand) is on the light
   canvas and keeps no crop. */
.sidebar-brand svg { width: 34px; height: 34px; flex: none; }
.sidebar-brand .brand-mark { width: 34px; height: 34px; object-fit: contain; flex: none; display: block; border-radius: 50%; }

/* The rail scrolls on a short screen; a light UA scrollbar would be a white
   stripe down the dark card, so the thumb takes the card's own edge. */
.sidebar-nav { display: flex; flex-direction: column; gap: 3px; flex: 1; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--nav-active-edge) transparent; }
/* The group headings — Counter · Manage · System — a hair over the micro
   step, with a clear gap above each so the three groups read as three.
   Drawn by App.mountShell only when more than one group has an item. */
.nav-section { padding: 16px 12px 6px; font-size: .7rem; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; color: var(--nav-text-muted); }
.nav-section:first-child { padding-top: 4px; }

/* 42px tall by min-height, not by padding: the label's line box never
   decides the rail's rhythm, so every item is the same height whatever
   the font does. */
.nav-item {
  position: relative;
  display: flex; align-items: center; gap: 12px; width: 100%;
  min-height: 42px; padding: 0 12px; border: none; border-radius: var(--radius-md); background: none;
  color: var(--nav-text); font: inherit; font-size: .92rem; font-weight: 500; cursor: pointer; text-align: left;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.nav-item:hover { background: var(--nav-hover); color: var(--nav-text-strong); }
.nav-item:hover .nav-ic { color: var(--nav-text-strong); }
/* THE ACTIVE PILL, FROSTED. White at 14% over the gradient with a 1px edge
   of its own and white type (7.91:1) — glass on the navy, so "where am I"
   is answered from across the room without a second dark colour. The edge
   is an INSET shadow, not a border, so the label does not shift a pixel
   when an item becomes active. */
.nav-item.active {
  background: var(--nav-active); color: var(--on-navy); font-weight: 600;
  box-shadow: inset 0 0 0 1px var(--nav-active-edge);
}
.nav-item.active .nav-ic { color: var(--on-navy); }
/* NBC gold marker — the badge's ring colour, so the chrome reads as NBC
   without competing with blue for "action". Inside the frosted pill it is
   4.38:1 at the lightest point of the card (a 3px bar needs 3:1). Inset and
   rounded: flush against the left edge a square bar would poke out past
   the pill's corner radius. Centred by top/transform, so it follows the
   item's height; 18px is the 42px item's own proportion of the old 14/36. */
.nav-item.active::before {
  content: ''; position: absolute; left: 5px; top: 50%; transform: translateY(-50%);
  width: 3px; height: 18px; border-radius: 999px; background: var(--nav-marker);
}
/* Room for the marker (12px + its 3px), so it never sits under the icon. */
.nav-item.active { padding-left: 15px; }
/* KEYBOARD FOCUS ON THE DARK CARD. The browser's own ring is dark blue or
   black and sinks into the navy, so focus is the gold (6.53:1 on the card,
   4.38:1 on the active pill). Inset, because the rail is a scroll box
   (overflow-y: auto) and an outline drawn outside an item is clipped at
   its left and right edges. */
.nav-item:focus-visible,
.avatar-btn:focus-visible { outline: 2px solid var(--nav-focus); outline-offset: -2px; }
/* Quiet pale line icons at rest — the Tesla dashboard's — that go white
   with the label on hover and in the active pill. */
.nav-ic { display: inline-flex; flex: none; color: var(--nav-text-muted); transition: color var(--dur-fast) var(--ease); }
.nav-ic svg { width: 20px; height: 20px; }

/* Phase 2+ modules: visible so the operator can see what is coming, but
   plainly not clickable yet. */
.nav-item.soon { color: var(--nav-text-muted); }
.nav-item.soon:hover { background: var(--nav-hover); color: var(--nav-text); }
.nav-item.soon .nav-ic { opacity: .7; }
/* An OUTLINED tag, no fill: a wash of its own on top of a hovered item's
   would take its muted type to 3.93:1. Bare, it is 6.45:1 at rest and
   5.16:1 on a hovered item. */
.nav-soon {
  margin-left: auto; font-size: .62rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  padding: 1px 6px; border-radius: var(--radius-pill); line-height: 1.5;
  color: var(--nav-text-muted); background: transparent; border: 1px solid var(--nav-active-edge);
}

/* A count on a nav item — how many cash outs are waiting on To verify
   (App.setNavBadge, for the collector and an admin). The ring gold with the
   avatar's dark ink (9.62:1), so on the dark card it is the brightest thing
   there, the one thing asking for a hand (the gold against the card itself:
   6.53:1).
   Round like the .nav-soon pill and the avatar disc: a square 20px count
   reads as a broken checkbox. Hidden at zero by the JS; the [hidden] rule in
   section 2 wins over this display. */
.nav-badge {
  display: inline-grid; place-items: center;
  margin-left: auto; min-width: 20px; height: 20px; padding: 0 6px;
  border-radius: 999px;
  font-size: .7rem; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums;
  color: var(--nav-avatar-ink); background: var(--nav-marker);
}

/* Which location (bus terminal) this account is on — the operator should never
   have to guess which one their day's figures belong to. THIS IS THE ONE
   PLACE the terminal is named: the global header's chip is gone and the
   page subtitles no longer repeat it (the owner: "once is fine"), so the
   block is drawn to be found — a caption over the name at a readable
   size, and the name WRAPS rather than truncating, because a clipped
   "Belize City Termin…" would be the only copy on screen. */
/* An OUTLINED block — the card's hairline, no fill — so it reads as a
   quiet fact beside the pressable items. It used to share the search
   field's fill; on the dark card it cannot, because the emerald dot is
   measured on the BARE gradient (3.12:1 60% down, and the block always sits
   lower than that, between the rail and the foot) and any white wash under
   it sinks it below the 3:1 a small mark needs.
   A two-row grid: the caption sits over the name, and the dot hangs in its
   own column beside the NAME, so both lines start at one edge. The text
   wrapper steps aside (display: contents) to let its two lines be cells
   of this grid — the markup is unchanged. */
.sidebar-location {
  display: grid; grid-template-columns: 8px minmax(0, 1fr);
  grid-template-areas: ". label" "dot name";
  column-gap: 10px; row-gap: 2px; align-items: start;
  margin: 10px 0 6px; padding: 10px 12px 11px;
  border: 1px solid var(--nav-line); border-radius: var(--radius-md);
  background: transparent;
}
/* Emerald, lit (--nav-dot = --nbc-emerald-rich): "open, trading" in the
   badge's own green, on the navy. Centred on the name's FIRST line — its
   line box is .9rem × 1.3 — so a name that wraps keeps the dot beside
   its start. */
.sidebar-location .dot { grid-area: dot; width: 8px; height: 8px; margin-top: calc((.9rem * 1.3 - 8px) / 2); border-radius: 999px; background: var(--nav-dot); }
.sidebar-location-text { display: contents; min-width: 0; }
.sidebar-location-label { grid-area: label; font-size: var(--fs-micro); font-weight: 600; letter-spacing: .07em; line-height: 1.4; text-transform: uppercase; color: var(--nav-text-muted); }
.sidebar-location-name { grid-area: name; min-width: 0; font-size: .9rem; font-weight: 600; line-height: 1.3; color: var(--nav-text-strong); overflow-wrap: anywhere; }

.sidebar-foot { position: relative; border-top: 1px solid var(--nav-line); padding-top: 10px; }
.avatar-btn { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px; border: none; border-radius: var(--radius-md); background: none; cursor: pointer; transition: background var(--dur-fast) var(--ease); }
.avatar-btn:hover { background: var(--nav-hover); }
/* DELIBERATE ROUND EXCEPTION (1 of 3). An avatar is a disc everywhere — a
   square one reads as a missing image. */
.avatar { width: 26px; height: 26px; border-radius: 999px; display: grid; place-items: center; background: var(--cal-brand); color: var(--cal-brand-text); font-size: .72rem; font-weight: 600; flex: none; }
.avatar-name { flex: 1; text-align: left; font-size: .88rem; font-weight: 500; color: var(--nav-text-strong); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.avatar-btn svg { width: 16px; height: 16px; color: var(--nav-text-muted); flex: none; }
/* The third logo colour: the account disc is the ring gold, so the card
   carries blue (the card itself), emerald (the location dot) and gold (the
   person) — the badge, in three marks. Dark ink on it reads 9.62:1; the
   disc against the card, 8.39:1 at the foot. The avatar in the MENU below
   keeps the blue fill.
   THE MENU STAYS LIGHT. It drops over the page, not over the card, so it is
   a white floating card on the --cal-* tokens like every other menu — the
   --nav-* inks above never reach it. */
.sidebar-foot .avatar { width: 30px; height: 30px; font-size: .76rem; background: var(--nav-marker); color: var(--nav-avatar-ink); }
/* The release this server runs (package.json via /config.js), under the
   account button — the answer to "which version is this terminal on?". */
.sidebar-version { padding: 6px 8px 0; font-size: var(--fs-micro); line-height: 1.4; color: var(--nav-text-muted); font-variant-numeric: tabular-nums; }

/* A floating layer: the overlay rung of the ladder, and it settles in. */
.avatar-menu {
  position: absolute; left: 8px; right: 8px; bottom: calc(100% + 6px);
  background: var(--cal-bg); border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-lg);
  color: var(--cal-text);   /* its own ink — it sits in the dark card's DOM and must not inherit --nav-text */
  box-shadow: var(--shadow-overlay); padding: 5px; z-index: 50;
  animation: menuIn 160ms var(--ease);
}
.avatar-menu-head { padding: 6px 8px 8px; border-bottom: 1px solid var(--cal-border-subtle); margin-bottom: 4px; }
.avatar-menu-name { font-size: .82rem; font-weight: 600; color: var(--cal-text-emphasis); }
.avatar-menu-mail { font-size: .74rem; color: var(--cal-text-subtle); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.avatar-menu-meta { font-size: .7rem; color: var(--cal-text-muted); margin-top: 2px; }
.avatar-menu-location { margin-left: 6px; }
.avatar-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 7px 8px; border: none; border-radius: var(--radius-md); background: none; color: var(--cal-text); font: inherit; font-size: .82rem; font-weight: 500; cursor: pointer; text-align: left; }
.avatar-menu-item:hover { background: var(--cal-bg-subtle); color: var(--cal-text-emphasis); }
.avatar-menu-item svg { width: 15px; height: 15px; flex: none; }

.app-body { background: var(--cal-canvas); }
/* 12px inset + 236px card + 12px gap = 260px (--rail-w): the page starts
   one inset past the sidebar card's right edge. */
.app-main { margin-left: var(--rail-w); min-height: 100vh; padding: 0; }
/* NOT a surface. The house layout used to put every screen on one white sheet
   and then place white tiles and tables INSIDE it — card-on-card, where a
   hairline was the only thing separating them and nothing read as a distinct
   object. So this keeps its padding and is a plain layout box: the canvas
   shows through as the page, and the tiles, tables and panels below are the
   cards floating on it. The gutter is a token (--gutter, 24px; 16px on a
   phone) because the header bleeds into it by the same amount. */
.page-card { background: transparent; border: none; min-height: 100vh; padding: 0 var(--gutter) var(--gutter); }
/* A view arrives the way a card does: an 8px rise on the house curve. */
#view.view-in { animation: viewIn 260ms var(--ease); }
@keyframes viewIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
/* EXCEPT the dashboard, which fades only. Its blocks drop into place from
   above (.cascade, section 2), and a whole view rising 8px underneath them
   would pull the other way — the first card would hang still while the
   two motions cancelled. The fade stays, so anything the view leaves
   un-cascaded still arrives rather than popping. Keyed on body[data-view]
   (set by App.renderRoute before the view draws, and stable for its whole
   life) — not on :has(.cascade), which would flip the animation name back
   the moment a re-render dropped the class and replay the view's entrance. */
body[data-view="dashboard"] #view.view-in { animation-name: viewFade; }
@keyframes viewFade { from { opacity: 0; } to { opacity: 1; } }

.mobile-topbar { display: none; }
.sidebar-backdrop { display: none; }

@media (max-width: 768px) {
  /* On a phone the card becomes a DRAWER: flush to the left edge, rounded on
     the side that faces the page, and on the floating rung of the ladder
     because it is now a layer over the page rather than a card beside it.
     It keeps the same gradient (--nav-bg, from the rule above), so the rail
     is the same dark object on every screen size. */
  .sidebar {
    left: 0; top: 0; bottom: 0;
    /* The card's own width, never more of a small phone than 86% of it:
       the scrim beside it is what a tap closes the drawer on. */
    width: var(--nav-w); max-width: 86vw;
    border-radius: 0 var(--radius-xl) var(--radius-xl) 0;
    box-shadow: var(--shadow-overlay);
    transform: translateX(-100%); transition: transform var(--dur) var(--ease);
  }
  .sidebar.open { transform: none; }
  /* The fixed 56px topbar owns the top of the phone screen. The header bar's
     own phone override lives at the end of section 8, AFTER the desktop rule
     it has to beat — the same-specificity rule here would lose on source
     order; the hero's is at the end of its own block in section 16. */
  .app-main { margin-left: 0; padding: 56px 0 0; }
  /* The padding follows --gutter, which section 1 re-points to 16px here. */
  .page-card { min-height: calc(100vh - 56px); }
  /* The frosted canvas the page header wears, so the two bars a phone stacks
     read as one surface. Solid canvas is the base; the blur is the upgrade
     (guarded in section 8 alongside .page-hd's). */
  .mobile-topbar {
    display: flex; align-items: center; gap: 10px; position: fixed; top: 0; left: 0; right: 0; height: 56px; z-index: 30;
    padding: 0 12px; background: var(--cal-canvas); box-shadow: var(--shadow-bar);
  }
  .mobile-brand { display: flex; align-items: center; gap: 8px; font-weight: 700; color: var(--cal-text-emphasis); }
  /* 24px — sized to the 56px bar, not to the rail's 34px badge: the drawer
     that opens from here carries the big one. No circle crop here: the
     topbar is the light canvas, and the PNG's white corners sit on it at
     1.1:1 — nothing there to cut. */
  .mobile-brand svg { width: 24px; height: 24px; flex: none; }
  .mobile-brand .brand-mark { width: 24px; height: 24px; object-fit: contain; flex: none; display: block; }
  /* The one control that opens the navigation on a phone: a --ctl-h icon
     button on the white-card recipe (hairline + control shadow), the edge
     going brand-blue under a hand like every other icon button. It stays
     the full control height, unlike a table's icon buttons: it is the
     phone's only way into the app. */
  .hamburger { display: grid; place-items: center; width: var(--ctl-h); height: var(--ctl-h); border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-md); background: var(--cal-bg); color: var(--cal-text); cursor: pointer; box-shadow: var(--shadow-sm); }
  .hamburger:hover { border-color: var(--cal-border-emphasis); color: var(--cal-text-emphasis); }
  .hamburger svg { width: 18px; height: 18px; }
  /* A scrim, not a shadow: it stays neutral black so it dims rather than tints. */
  .sidebar-backdrop.show { display: block; position: fixed; inset: 0; background: var(--scrim); z-index: 35; }
}

/* ============================================================
   7B. APP HEADER — the global search (App.search, public/app.js)
   ONE field for every staff page, drawn ONCE by App.mountShell and then
   MOVED, never re-drawn: App.search._home puts it inside the view's own
   .page-hd (section 8) — level with the page title, pushed right, before
   the page's actions — so the search and the title share one row instead
   of the search stacking a row of its own above the title (the owner:
   "the search can be at the same height as the header"). The .app-header
   row above #view is only its FALLBACK, for a screen that draws no
   .page-hd; the JS hides the row, so it takes no height, whenever the
   field is homed. The row holds nothing else — the terminal is named once,
   in the rail's location block, and a page's subtitle carries the date
   where the page needs it.
   HIDDEN. A screen whose own header takes the page-header's place — the
   ticket wizard's navy pill — takes the field away with App.search.hide().
   That state needs no rule of its own here: the JS sets the `hidden`
   ATTRIBUTE on the field (it travels with it wherever it is homed) and on
   the row, and section 2's [hidden] rule beats both their displays. The
   row's hidden attribute is also what the .page-card reservation at the
   end of this section keys on, so a hidden row reserves nothing.
   The row is the CANVAS, not a bar — no fill, no rule, no shadow of its
   own — and it scrolls away with the page. The conductor's shell draws
   none of this.
   ============================================================ */
/* z 21 at rest: over the page's sticky .page-hd (20) — the two never
   overlap at rest, the number only matters for the card below. Height is
   the token, so the page under a VISIBLE row can reserve exactly the rest
   of the screen (the .page-card rule at the end of this section). */
.app-header {
  position: relative; z-index: 21;
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  height: var(--app-header-h);
  padding: 12px var(--gutter) 0;
}
/* Open, the card must clear every sticky bar a page draws under the
   header — a page's own bar may sit at 30 (the ticket wizard's pill does,
   though it hides the field for as long as it is up) — so the header
   rises to 31 only for as long as the list is up. On a phone that is also
   over the fixed topbar (30); the list closes on blur, so it never lingers
   there. */
.app-header.is-open { z-index: 31; }

/* HOMED IN THE PAGE'S OWN HEADER. Its own flex item in .page-hd's row:
   after the title block, before .page-hd-actions, and margin-left: auto
   takes the free space so the field (and the actions after it) sit at the
   right while the title keeps the left. It GROWS from 260px up to 380px:
   the small basis is what lets it share a laptop's row with a page's
   range pills and solid button before anything wraps, the cap is a
   ticket number and a destination with room to spare. .page-hd centres
   its items, so the field sits on the title row's middle. */
.page-hd > .app-search { flex: 1 1 260px; max-width: 380px; margin-left: auto; }
/* The page header is a sticky bar with a stacking context of its own
   (z 20), and the card of matches lives INSIDE it — so the card can only
   ever be as high as the bar. While the list is up the bar rises to the
   fallback row's open level (31, over the ticket window's 30 and the
   calendar's own popover at 20, which comes later in the page and would
   win a tie); App.search._paintOpen sets the class and takes it off. */
.page-hd.has-search-open { z-index: 31; }

/* The field: white on the canvas at the control height, a hairline and the
   control shadow at rest — the white card's recipe, so it reads as the
   thing to type in without the 3:1 edge of a form field; the glyph and the
   placeholder name it. Under a hand the edge firms to --cal-border; in
   focus it is the brand border and ring every field in the app wears. */
.app-search { position: relative; flex: 0 1 480px; min-width: 0; }
.app-search-ic {
  position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
  display: flex; color: var(--cal-text-muted); pointer-events: none;
}
.app-search-ic svg { width: 15px; height: 15px; }
.app-search input {
  -webkit-appearance: none; appearance: none;   /* Safari draws type=search as a rounded well of its own otherwise */
  display: block; width: 100%; height: var(--ctl-h); margin: 0;
  padding: 0 38px 0 33px;                       /* 38 on the right: the keycap sits there until the field is used */
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-md);
  background: var(--cal-bg); color: var(--cal-text-emphasis);
  font: inherit; font-size: .85rem;
  box-shadow: var(--shadow-sm);
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.app-search input::placeholder { color: var(--cal-text-muted); }
.app-search input::-webkit-search-decoration { -webkit-appearance: none; }
.app-search input:hover:not(:focus) { border-color: var(--cal-border); }
.app-search input:focus {
  outline: none; border-color: var(--cal-border-emphasis);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--cal-border-emphasis) 30%, transparent);
}
/* In use, the keycap steps aside and the native × takes the right edge. */
.app-search input:focus,
.app-search input:not(:placeholder-shown) { padding-right: 10px; }

/* The "/" keycap: the hint that the key exists, where the eye already is.
   The same keycap the ticket window's hint row draws (.tk-hint kbd,
   section 18E) — a 6px corner, because the control radius on a 20px box
   reads as a pill. Gone once the field has focus or words. */
.app-search-kbd {
  position: absolute; right: 9px; top: 50%; transform: translateY(-50%);
  min-width: 20px; height: 20px; padding: 0 5px;
  display: inline-grid; place-items: center;
  border: 1px solid var(--cal-border-subtle); border-bottom-width: 2px; border-radius: 6px;
  background: var(--cal-bg-subtle); color: var(--cal-text-subtle);   /* ~5.4:1 */
  font-family: inherit; font-size: .7rem; font-weight: 600; line-height: 1;
  pointer-events: none;
}
.app-search input:focus + .app-search-kbd,
.app-search input:not(:placeholder-shown) + .app-search-kbd { display: none; }

/* THE CARD OF MATCHES — the floating rung of the ladder, settling in on
   menuIn like every menu. Anchored under the field and at least 480px wide
   (never wider than the page column — the screen less the rail and the two
   gutters), so a ticket's full route line has room even when the field has
   been squeezed. Eight rows at most, so the max-height is a guard for a
   short window, not a scroll the list needs. */
.app-search-pop {
  position: absolute; left: 0; right: 0; top: calc(100% + 6px);
  min-width: min(480px, calc(100vw - var(--rail-w) - 2 * var(--gutter)));
  max-height: min(420px, calc(100vh - 140px));
  overflow-y: auto; overscroll-behavior: contain;
  padding: 5px;
  background: var(--cal-bg); color: var(--cal-text);
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-overlay);
  transform-origin: top left;
  animation: menuIn 160ms var(--ease);
}

/* A row: the kind badge in a fixed column (so every handle starts at the
   same x), the handle over one line of what it is, and the money on the
   right. Two lines in 42px — dense, the tape's own rhythm. The highlight is
   ONE state for the arrows and the mouse (the JS moves .is-active on
   hover), so two rows are never lit at once. */
.app-search-item {
  display: grid; grid-template-columns: 80px minmax(0, 1fr) auto; align-items: center; gap: 10px;
  min-height: 42px; padding: 5px 8px;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
}
.app-search-item.is-active { background: var(--cal-bg-subtle); }
.app-search-item.is-active .app-search-title { color: var(--cal-brand); }   /* 7.40:1 on the highlight */

/* The kind, in the badge trios section 1 measured: a ticket in the brand
   tint, a cash out (drawer or bag) in the gold, a deposit in the green, a
   plain ledger entry in the neutral grey. Micro type in a fixed column. */
.app-search-kind {
  justify-self: start; max-width: 100%;
  display: inline-flex; align-items: center; height: 20px; padding: 0 7px;
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-pill);
  background: var(--cal-bg-subtle); color: var(--cal-text-subtle);
  font-size: .62rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; line-height: 1;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.app-search-kind.is-ticket { background: var(--badge-role-bg); color: var(--badge-role-text); border-color: var(--badge-role-border); }
.app-search-kind.is-drawer,
.app-search-kind.is-bag { background: var(--badge-warn-bg); color: var(--badge-warn-text); border-color: var(--badge-warn-border); }
.app-search-kind.is-deposit { background: var(--badge-ok-bg); color: var(--badge-ok-text); border-color: var(--badge-ok-border); }

.app-search-main { display: flex; flex-direction: column; min-width: 0; }
.app-search-title {
  font-size: .82rem; font-weight: 600; line-height: 1.25rem; color: var(--cal-text-emphasis);
  font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.app-search-sub {
  font-size: .72rem; line-height: 1.05rem; color: var(--cal-text-subtle);   /* ~5.4:1 on the highlight */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.app-search-side { display: flex; flex-direction: column; align-items: flex-end; text-align: right; white-space: nowrap; }
.app-search-amount { font-size: .8rem; font-weight: 600; line-height: 1.25rem; color: var(--cal-text-emphasis); font-variant-numeric: tabular-nums; }
.app-search-status { font-size: .68rem; font-weight: 600; line-height: 1.05rem; color: var(--cal-text-subtle); }
/* A refunded ticket or a voided entry is no longer money held: the amount
   is struck, as My sales strikes it. Colour, not opacity, so the figure
   keeps its 4.5:1 on the highlight. */
.app-search-item.is-void .app-search-amount { text-decoration: line-through; color: var(--cal-text-subtle); }

.app-search-empty { padding: 14px 10px; text-align: center; font-size: .8rem; color: var(--cal-text-subtle); }

/* FLIPPED. Homed at the right end of a page's title row — often with
   nothing after it — a card dropping from the field's LEFT edge at 480px
   would run off the screen. App.search._placePop measures that on every
   open and hangs the card from the field's RIGHT edge instead, opening
   leftward over the page, as wide as the field or 480px, whichever is
   more. Measured rather than assumed, because with a short title and a
   squeezed field a fixed right anchor would push the card under the rail. */
.app-search-pop.is-flipped { left: auto; right: 0; width: 100%; transform-origin: top right; }

/* NARROW. Up to 900px the column is too tight to share, so the field takes
   a whole line of its own. In the fallback row it is simply the row. In a
   page header it breaks onto its own full-width line straight under the
   title block (the flex basis does it; the markup already puts it after
   the title and before the actions), and its card, as wide as the field,
   never needs to flip. On a phone the fixed topbar is above all of this,
   and the keycap goes wherever there is no keyboard to press it on. */
@media (max-width: 900px) {
  .app-search { flex: 1 1 auto; }
  .page-hd > .app-search { flex: 1 1 100%; max-width: none; margin-left: 0; }
}
@media (max-width: 768px) {
  .app-search-pop { min-width: 0; }
  .app-search-kbd { display: none; }
}
@media (hover: none) {
  .app-search-kbd { display: none; }
}

/* The page under a VISIBLE fallback row reserves the REST of the screen,
   not all of it — otherwise every short page would scroll by exactly the
   row's height. A hidden row (the field homed in the page) takes no
   height, so the page's own min-height stands. Screen only: on paper
   .page-card's min-height is 0 (section 19), and this selector would
   outrank that rule. */
@media screen {
  .app-header:not([hidden]) + #view .page-card { min-height: calc(100vh - var(--app-header-h)); }
}
@media screen and (max-width: 768px) {
  .app-header:not([hidden]) + #view .page-card { min-height: calc(100vh - 56px - var(--app-header-h)); }
}

/* ============================================================
   8. PAGE HEADER
   ============================================================ */
/* THE HEADER BREATHES. Every view renders a .page-hd at the top of its
   card, so styling it here frames every screen at once with no view
   changing. It is not a white bar any more — the cards are the white things
   — but the CANVAS ITSELF, frosted, so the page scrolls up under it through
   a blur and the title, the location selector and the one solid button stay
   reachable however far down a month of ledger rows you are. It bleeds to
   the card's edges with negative margins of the card's own side padding
   (--gutter, so the two are one number).
   It is also where the global search lives: App.search moves its ONE
   field into this row, after the title block and before the actions
   (section 7B styles it here, as .page-hd > .app-search).
   z-index above the sheet's sticky table header (2) and below the sidebar
   (40) and every overlay — and 31 while the search's card is open
   (.has-search-open, 7B), so the card clears every bar under it.

   THREE GUARDS, the same three 10B uses for the overlay's scrim:
     1. the SOLID canvas is the base; the translucent canvas and the blur are
        an upgrade applied only inside @supports, so a browser that cannot
        blur never lands on a see-through bar with rows showing through it;
     2. the blur is NEVER ANIMATED — a sticky backdrop-filter re-rasterizes
        the page under it every frame it moves;
     3. an operator who asked their OS for less transparency gets the solid
        canvas back (the block must come AFTER @supports to win on order). */
.page-hd {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; justify-content: space-between; gap: 12px 16px; flex-wrap: wrap;
  margin: 0 calc(-1 * var(--gutter)) var(--gap-section);
  padding: 12px var(--gutter) 10px;
  background: var(--cal-canvas);
  border-bottom: 0;
  box-shadow: var(--shadow-bar);
}
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .page-hd, .mobile-topbar {
    background: color-mix(in srgb, var(--cal-canvas) 84%, transparent);
    -webkit-backdrop-filter: saturate(160%) blur(14px);
    backdrop-filter: saturate(160%) blur(14px);
  }
}
@media (prefers-reduced-transparency: reduce) {
  .page-hd, .mobile-topbar {
    background: var(--cal-canvas);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}
.page-hd h1 { font-size: var(--fs-h1); font-weight: 600; letter-spacing: -.02em; color: var(--cal-text-emphasis); margin: 0; }
.page-hd p { font-size: var(--fs-small); color: var(--cal-text-subtle); margin: 2px 0 0; }
.page-hd-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.page-search { position: relative; }
.page-search svg { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); width: 15px; height: 15px; color: var(--cal-text-muted); pointer-events: none; }
.page-search input {
  height: var(--ctl-h); padding: 0 12px 0 32px; width: 220px; max-width: 44vw;
  border: 1px solid var(--cal-border); border-radius: var(--radius-md);
  background: var(--cal-bg); color: var(--cal-text-emphasis); font: inherit; font-size: .85rem;
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.page-search input:hover { border-color: var(--cal-text-subtle); }
.page-search input:focus { outline: none; border-color: var(--cal-border-emphasis); box-shadow: 0 0 0 3px color-mix(in srgb, var(--cal-border-emphasis) 25%, transparent); }

/* Which location a screen is showing. An ADMIN gets a <select> here; everyone
   else gets the same block with static text, because they cannot change it and
   a disabled dropdown only invites the question. */
.dash-scope { display: inline-flex; align-items: center; gap: 8px; }
.dash-scope-label { font-size: var(--fs-micro); font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--cal-text-muted); }
.dash-scope-value { font-size: .85rem; font-weight: 600; color: var(--cal-text-emphasis); }
.dash-scope-select { height: var(--ctl-h); width: auto; min-width: 170px; max-width: 60vw; font-size: .85rem; }

/* THE CARD. White on the canvas, hairline + resting shadow, 14px corners:
   the rest rung of the ladder, and the shape every panel on every screen
   shares with the wizard's. */
.panel {
  background: var(--cal-bg);
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-xl);
  box-shadow: var(--shadow-card);
  padding: 16px 18px;
}
.panel h2 { font-size: var(--fs-h2); font-weight: 600; letter-spacing: -.005em; color: var(--cal-text-emphasis); margin: 0 0 10px; }
.panel-row { display: flex; justify-content: space-between; gap: 16px; font-size: .82rem; padding: 3px 0; }
.panel-row span:first-child { color: var(--cal-text-subtle); }
.panel-row span:last-child { color: var(--cal-text-emphasis); text-align: right; }

/* The phone override, HERE and not in section 7's phone block: same
   specificity as the desktop rule above, so it has to come after it to win.
   The header sticks just under the 56px topbar; its bleed and side padding
   follow --gutter, already 16px here, so only the vertical padding moves. */
@media (max-width: 768px) {
  .page-hd { top: 56px; padding-top: 10px; padding-bottom: 10px; }
}

/* ============================================================
   9. TOAST
   ============================================================ */
/* Floats over whatever the operator was reading, so it sits on the overlay
   rung and rises in on the house curve. */
.toast {
  position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%) translateY(20px);
  background: var(--cal-bg-inverted); color: var(--cal-text-inverted);
  padding: 10px 14px; border-radius: var(--radius-lg); font-size: .82rem;
  box-shadow: var(--shadow-overlay);
  opacity: 0; transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease); pointer-events: none; z-index: 50;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

/* ============================================================
   10. MODAL
   ============================================================ */
/* The backdrop is a scrim — neutral black, so it dims the page rather than
   tinting it — and the page behind it blurs under the SAME three guards
   10B applies to the overlay's (read them there). --scrim throughout: the
   modal's dim was tuned with the blur in mind and holds without it. */
.modal-backdrop { position: fixed; inset: 0; background: var(--scrim); z-index: 100; display: grid; place-items: center; padding: 20px; opacity: 0; transition: opacity var(--dur-fast) var(--ease); }
.modal-backdrop.show { opacity: 1; }
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .modal-backdrop {
    -webkit-backdrop-filter: blur(var(--overlay-blur));
    backdrop-filter: blur(var(--overlay-blur));
  }
}
@media (prefers-reduced-transparency: reduce) {
  .modal-backdrop {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}
/* The furthest thing above the page, so it takes the deepest rung — a modal
   that sat flat on its own scrim would read as part of the page behind it.
   It arrives with a 12px rise and a 2% settle on the house curve. */
/* 420px: the fields inside came down a size, so the old 440 was only air. */
.modal {
  width: 100%; max-width: 420px;
  background: var(--cal-bg); border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-xl);
  box-shadow: var(--shadow-modal);
  transform: translateY(12px) scale(.98); transition: transform var(--dur) var(--ease);
}
.modal-backdrop.show .modal { transform: none; }
.modal-head { padding: 18px 20px 4px; }
.modal-head h2 { font-size: var(--fs-h2); font-weight: 600; letter-spacing: -.005em; color: var(--cal-text-emphasis); margin: 0; }
.modal-head p { font-size: .78rem; color: var(--cal-text-subtle); margin: 3px 0 0; }
.modal-body { padding: 12px 20px; max-height: 68vh; overflow-y: auto; }
.modal-foot { padding: 12px 20px 18px; display: flex; justify-content: flex-end; gap: 8px; }
.modal-foot .btn { min-width: 76px; }

/* Confirmations and other short prompts. */
.modal.modal-sm { max-width: 360px; }
/* Destructive prompts get a hairline of error color at the top so the stakes
   read before the text does. */
.modal.modal-danger { border-top: 3px solid var(--cal-error); }
.modal.modal-danger .modal-head h2 { color: var(--cal-error); }

/* ============================================================
   10B. OVERLAY — the blurred component layer
   Dressed for public/components/funds-overlay.js, but named for no component
   in particular: any component that has to own its own layer from any page
   mounts these classes.

   WHY IT IS NOT .modal. A .modal is a body the shell fills in; an overlay is a
   component that brought its own head and foot, its own close button and its
   own status line, and can be opened from a page that knows nothing about it.
   It reads as the SAME FAMILY on purpose — white surface, hairline border,
   the card corner, the modal's shadow and the same 20px gutter — so the two
   never look like two design systems. What it adds is the ruled head/foot;
   what it must never become is a second .modal. Section 10 is untouched by
   everything below (it borrows the three blur guards, that is all).

   THE BLUR, and its three guards
     1. NOT UNIVERSAL. The unprefixed rule is the ENHANCEMENT, not the base:
        the plain .overlay-backdrop paints --scrim-strong, and only inside
        @supports does it lighten to --scrim and pick up the blur. A browser
        that cannot blur (or cannot parse @supports at all) therefore lands on
        a heavy dim rather than on a barely-tinted, fully-legible page sitting
        behind a form, which is unreadable mush. -webkit- is there because
        Safari still wants it and these are whatever machines the terminal has.
     2. NEVER ANIMATED. The blur radius is a fixed token and there is no
        will-change on it: a full-screen backdrop-filter re-rasterizes the page
        behind it every frame, and on terminal hardware an animated one drops
        frames visibly. Only OPACITY transitions — it is cheap, it is composited
        and at 150ms it looks identical.
     3. REDUCED TRANSPARENCY. When the operator has asked their OS for less of
        it, the blur is dropped for the same solid dim as guard 1.
   ============================================================ */

/* z-index 100 — the modal's layer. Only one of the two is ever up, and if a
   confirm is ever raised over an overlay, DOM order settles it. */
.overlay-backdrop {
  position: fixed; inset: 0; z-index: 100;
  display: grid; place-items: center;
  padding: 20px;
  background: var(--scrim-strong);
  opacity: 0;
  transition: opacity .15s ease;
}
.overlay-backdrop.show { opacity: 1; }

/* Guard 1: the blur is an upgrade applied only where it exists, and it brings
   the lighter scrim with it — the two belong together and are set together. */
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .overlay-backdrop {
    background: var(--scrim);
    -webkit-backdrop-filter: blur(var(--overlay-blur));
    backdrop-filter: blur(var(--overlay-blur));
  }
}

/* Guard 3: must come AFTER the @supports block — same specificity, so source
   order is what lets it win back the solid scrim. */
@media (prefers-reduced-transparency: reduce) {
  .overlay-backdrop {
    background: var(--scrim-strong);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* The panel. A column so the head and foot stay put and the body is the only
   part that scrolls — the buttons must never scroll off a laptop screen with
   the keyboard up. max-height keeps it inside the backdrop's own 20px padding. */
.overlay {
  display: flex; flex-direction: column;
  width: 100%; max-width: 440px;
  /* Two columns of fields need the room; the detail view and any future
     single-column overlay keep the narrower default. */
  /* Two declarations on purpose: a browser that does not know dvh drops the
     second and keeps the vh, and one that does gets a panel that stays inside
     the visible area when a phone's toolbars are showing. */
  max-height: calc(100vh - 40px);
  max-height: calc(100dvh - 40px);
  background: var(--cal-bg);
  border: 1px solid var(--cal-border-subtle);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-modal);
  transform: translateY(12px) scale(.98);
  transition: transform var(--dur) var(--ease);
}
.overlay-backdrop.show .overlay { transform: none; }

/* Two columns: the text stack takes the first, the close button the second.
   Written so it lays out whether the JS wraps the title and subtitle in a
   div or hands them over as direct children. */
.overlay-head {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: start;
  column-gap: 12px;
  padding: 14px 20px 12px;
  border-bottom: 1px solid var(--cal-border-subtle);
}
.overlay-title { grid-column: 1; font-size: var(--fs-h2); font-weight: 600; letter-spacing: -.005em; color: var(--cal-text-emphasis); margin: 0; }
/* Where the entry will actually be filed. It is here so the page and the
   overlay can never disagree about the destination, which makes it a line of
   record, not decoration — hence -subtle rather than -muted. */
.overlay-sub { grid-column: 1; font-size: .78rem; color: var(--cal-text-subtle); margin: 3px 0 0; overflow-wrap: anywhere; }

/* A quiet square target. The border is transparent at rest rather than absent,
   so the focus ring lands on it without the button changing size. */
.overlay-close {
  grid-column: 2; grid-row: 1; align-self: start;
  width: 28px; height: 28px; flex: none;
  display: grid; place-items: center;
  padding: 0;
  border: 1px solid transparent; border-radius: var(--radius-md);
  background: none; color: var(--cal-text-muted);
  cursor: pointer;
}
.overlay-close:hover { background: var(--cal-bg-subtle); color: var(--cal-text-emphasis); }
.overlay-close:focus-visible {
  outline: none;
  border-color: var(--cal-border-emphasis);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--cal-border-emphasis) 30%, transparent);
}
.overlay-close svg { width: 15px; height: 15px; display: block; }

/* The only scrolling part. overscroll-behavior stops a flick at the end of the
   fields from chaining into the page behind — the component locks body scroll
   as well, and these two solve different halves of the same problem. */
.overlay-body {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 14px 20px 4px;
}

/* Ruled, like the head. The status shares the row with the buttons and gets
   the leftover width; below ~220px of it the row wraps and the message takes a
   line of its own rather than crushing Save into two characters. */
.overlay-foot {
  display: flex; align-items: center; justify-content: flex-end;
  flex-wrap: wrap; gap: 8px 10px;
  padding: 12px 20px;
  border-top: 1px solid var(--cal-border-subtle);
}
.overlay-foot .btn { min-width: 76px; }

/* Where a rejected save says why, INSIDE the overlay, next to the button that
   was pressed — the one place the operator is already looking. :empty rather
   than a reserved line, so a footer with nothing to say costs no height and
   forces no wrap. */
.overlay-status {
  flex: 1 1 220px; min-width: 0;
  margin: 0;
  font-size: .78rem; line-height: 1.4;
  color: var(--cal-text-muted);
  overflow-wrap: anywhere;
}
.overlay-status:empty { display: none; }
/* Wider when it is carrying two columns — the record form's .form-grid, and
   now the detail's .detail-cols. A class rather than :has(), which a browser
   older than the terminal machines might not support — and it would fail
   SILENTLY, leaving two columns crammed into 440px with no error anywhere to
   explain it. */
.overlay.overlay-wide { max-width: 600px; }
.overlay-status.error { color: var(--cal-error); font-weight: 500; }
/* It reads as a foot line, but it still works if a component puts it at the
   end of the body — then it needs its own air, which flex-gap was giving it. */
.overlay-body > .overlay-status { margin-top: 4px; }

/* Guard 2 lives here too: the blur is deliberately NOT in this block. Killing
   the fade and the panel's slide is the whole of the reduced-motion story. */
@media (prefers-reduced-motion: reduce) {
  .overlay-backdrop, .overlay { transition: none; }
  .overlay { transform: none; }
}

/* On a phone the panel is nearly the screen, so the gutters come in and the
   two buttons split the width instead of huddling at one end of an empty row. */
@media (max-width: 560px) {
  .overlay-backdrop { padding: 12px; }
  .overlay { max-height: calc(100vh - 24px); max-height: calc(100dvh - 24px); }
  .overlay-head { padding: 12px 14px 10px; }
  .overlay-body { padding: 12px 14px 4px; }
  .overlay-foot { padding: 10px 14px; }
  .overlay-foot .btn { flex: 1 1 auto; }
}

/* ============================================================
   11. DATA TABLE + BADGES
   ============================================================ */
.table-wrap {
  background: var(--cal-bg);
  border: 1px solid var(--cal-border-subtle);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-card);
  /* Bounded, which is what makes the sticky header below real: a month range is
     150+ rows, and scrolling the column headings off the top of a table of
     figures is how an amount gets read against the wrong column. The table
     scrolls inside its own card; the page itself stays put. overflow (not
     overflow-x) so it scrolls in both axes and clips to the rounded corners. */
  max-height: clamp(320px, calc(100vh - 400px), 760px);
  overflow: auto;
}

table.data-table { width: 100%; border-collapse: collapse; }
/* The head is sticky on EVERY table now, not only the sheet: .table-wrap is
   bounded, so any long list scrolls inside its card and the column names
   must stay put. Harmless where nothing scrolls. */
.data-table thead th {
  text-align: left;
  font-size: var(--fs-micro); font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--cal-text-muted);
  background: var(--cal-bg-muted);
  padding: 9px 14px;
  white-space: nowrap;
  border-bottom: 1px solid var(--cal-border-subtle);
  position: sticky; top: 0; z-index: 2;
}
/* 13px, a step under body: a table is scanned down a column, not read
   across, and the step is what lets a day of rows fit the screen. Every
   ink a cell uses clears 4.5:1 on white at any size, so nothing here was
   leaning on large type. */
.data-table tbody td {
  font-size: .8125rem;
  padding: 10px 14px;
  color: var(--cal-text);
  border-bottom: 1px solid var(--cal-border-muted);
  vertical-align: middle;
}
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover { background: var(--cal-bg-subtle); }
.data-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.data-table .col-actions { width: 1%; text-align: right; white-space: nowrap; }

/* SORTABLE COLUMNS. A th that carries aria-sort is one you can click; the
   arrow after the label says which way the column is running, and a column
   not in force shows a faint both-ways arrow so the affordance is visible
   before the first click. The JS only toggles aria-sort — no CSS class. */
.data-table th[aria-sort] { cursor: pointer; user-select: none; }
.data-table th[aria-sort]:hover { color: var(--cal-text-emphasis); }
.data-table th[aria-sort="ascending"] .th-sort::after { content: " \2191"; }
.data-table th[aria-sort="descending"] .th-sort::after { content: " \2193"; }
.data-table th[aria-sort="none"] .th-sort::after { content: " \2195"; opacity: .5; }
/* The button inside a sortable th: unstyled so it IS the heading, with the
   heading's own type, and a ring so the keyboard can find it. */
.th-sort { all: unset; cursor: pointer; font: inherit; color: inherit; text-transform: inherit; letter-spacing: inherit; }
.th-sort:focus-visible { outline: 2px solid var(--cal-border-emphasis); outline-offset: 2px; border-radius: 4px; }

/* A table INSIDE a card is not a second card: no edge, no shadow, a control
   corner. (.dash-metrics .table-wrap further down does the same for the
   admin's log and stays as it is.) */
.panel .table-wrap { border: 0; box-shadow: none; border-radius: var(--radius-md); }

/* A handle as a pill — TXN-0042, a ticket number: monospaced and tabular so
   a column of them lines up, in the quiet fill so it reads as a tag on the
   row rather than one more cell of type. */
.ref-pill {
  display: inline-flex; align-items: center;
  height: 20px; padding: 0 8px;
  border-radius: var(--radius-pill);
  background: var(--cal-bg-subtle); border: 1px solid var(--cal-border-subtle);
  font-family: var(--font-mono); font-size: .68rem; font-weight: 600; letter-spacing: .01em;
  color: var(--cal-text-subtle); white-space: nowrap; font-variant-numeric: tabular-nums;
}

/* ── Sheet variant: a spreadsheet, not a list ──
   Opt-in via .data-table.sheet, because it changes how the table reads: banded
   rows and vertical rules turn it into a grid you scan down a COLUMN of, which
   is right for a ledger of figures and wrong for the Users list, where each row
   is a person rather than a cell in a table of numbers. Add the class to adopt
   it elsewhere; nothing else changes. */
.data-table.sheet tbody td {
  padding: 7px 12px;              /* denser: more of the day on one screen */
  border-right: 1px solid var(--cal-border-muted);
}
.data-table.sheet thead th {
  padding: 7px 12px;
  border-right: 1px solid var(--cal-border-subtle);
}
.data-table.sheet thead th:last-child,
.data-table.sheet tbody td:last-child { border-right: none; }
/* With banded rows inside a rounded, elevated card the vertical grid is one
   line too many — the banding already does the work of keeping the eye on a
   row. Kept as a single override rather than deleting the borders above, so
   the plain (unbanded) sheet variant still has them if it is ever wanted. */
.data-table.sheet tbody td,
.data-table.sheet thead th { border-right: none; }

/* The banding. Only a background, so the voided row's strikethrough and muted
   colour sit on top of it untouched rather than fighting it. */
.data-table.sheet tbody tr:nth-child(even) { background: var(--cal-bg-muted); }
.data-table.sheet tbody tr:hover { background: var(--cal-bg-subtle); }

/* The row itself opens the detail overlay, so it has to look pressable —
   without this the only affordance is the hover band, which reads as decoration
   rather than an invitation. The action buttons stop the click before it
   reaches the row, so Edit and Void still do their own job. */
.data-table.sheet tbody tr[data-id] { cursor: pointer; }

/* Figures line up on the decimal point or the column is unscannable. */
.data-table.sheet .num,
.data-table.sheet .amount-in,
.data-table.sheet .amount-out { font-variant-numeric: tabular-nums; }

/* ── Range pills ──
   Today / This week / This month / Last month / Custom. A segmented row rather
   than a select: five options, and the one in force should be readable without
   opening anything. */
.range-bar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-bottom: 14px;
}
/* The same pill track as .seg (section 5): grey, the chosen range in navy.
   The whole control is --ctl-h-row (32px), the segments that less the 3px
   + 1px track — the height of the Custom date inputs and the Reports
   filter selects that sit on the same row. */
.range-pills {
  display: inline-flex; padding: 3px; gap: 2px;
  border: 1px solid var(--cal-border-subtle);
  border-radius: var(--radius-pill);
  background: var(--cal-bg-subtle);
  box-shadow: none;
  overflow: visible;
}
.range-pill {
  border: 0; background: transparent; color: var(--cal-text-subtle);
  font: inherit; font-size: .78rem; font-weight: 600; line-height: 1;
  padding: 0 12px; height: calc(var(--ctl-h-row) - 8px); cursor: pointer;
  border-radius: var(--radius-pill);
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.range-pill:hover { background: var(--cal-bg); color: var(--cal-text-emphasis); }
.range-pill.on { background: var(--nbc-navy); color: var(--on-navy); }   /* 15.14:1 */
.range-pill:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--cal-brand); }
.range-custom { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.range-custom label { font-size: .74rem; font-weight: 500; color: var(--cal-text-muted); }
.range-custom .inp { height: var(--ctl-h-row); width: auto; padding: 0 8px; }

@media (max-width: 640px) {
  .range-pills { width: 100%; }
  .range-pill { flex: 1; padding: 0 8px; font-size: .72rem; }
}

/* Avatar + name/email stack inside a cell */
.cell-user { display: flex; align-items: center; gap: 8px; min-width: 0; }
.cell-user .avatar { width: 28px; height: 28px; font-size: .72rem; }
.cell-user-text { min-width: 0; }
.cell-user-name { font-size: .8125rem; font-weight: 600; color: var(--cal-text-emphasis); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cell-user-mail { font-size: .74rem; color: var(--cal-text-subtle); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Row actions: quiet until the row is hovered, always visible on touch. */
.row-actions { display: inline-flex; align-items: center; gap: 4px; justify-content: flex-end; opacity: 0; transition: opacity .12s ease; }
.data-table tbody tr:hover .row-actions,
.row-actions:focus-within { opacity: 1; }
.row-actions .icon-btn { width: 28px; height: 28px; }
.row-actions .icon-btn svg { width: 14px; height: 14px; }
@media (hover: none) { .row-actions { opacity: 1; } }

/* Badges. Each trio is a FLAT token set, not a color-mix() chain: the fill and
   the text on it were solved together, so the ratio is provable rather than
   emergent. The inherited mixes resolved to 3.97:1 on .badge-warn and were
   failing invisibly. Text on its own fill: ok 6.68 · warn 5.20 · role 7.06 ·
   off 5.46:1. The borders are decorative — meaning is carried by the text.
   A PILL, like every status in the reference screens (Signed · Waiting ·
   Done · In progress): a status is a tag on the row, not a cell of it. */
.badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px; border-radius: var(--radius-pill);
  font-size: .68rem; font-weight: 600; line-height: 1.5; white-space: nowrap;
  border: 1px solid transparent;
  color: var(--cal-text-subtle);
  background: var(--cal-bg-subtle);
}
.badge-ok {
  color: var(--badge-ok-text);
  background: var(--badge-ok-bg);
  border-color: var(--badge-ok-border);
}
.badge-off {
  color: var(--cal-text-subtle);
  background: var(--cal-bg-subtle);
  border-color: var(--cal-border-subtle);
}
.badge-warn {
  color: var(--badge-warn-text);
  background: var(--badge-warn-bg);
  border-color: var(--badge-warn-border);
}
.badge-role {
  color: var(--badge-role-text);
  background: var(--badge-role-bg);
  border-color: var(--badge-role-border);
}
/* Danger: something sent back or failing a check, which somebody has to act
   on — a queried deposit, a ledger row whose cash-chain pointer its frozen
   list does not confirm. The error trio the error surfaces already wear
   (error text 7.92:1 on its fill), so no new ink. */
.badge-danger {
  color: var(--cal-error);
  background: var(--cal-error-bg);
  border-color: var(--cal-error-border);
}

/* ============================================================
   12. EMPTY / LOADING
   ============================================================ */
/* 28px, not 40: the density pass took the cards to 16–18px, and an empty
   table under 40px of air read as a hole in the page. The same room, and
   a 36px disc, as its siblings (.tk-sale-empty; .desk-empty a step less). */
.empty-state {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 5px; padding: 28px 20px; text-align: center;
}
/* A tonal disc, no edge — the wizard's empty state. */
.empty-state .empty-ic {
  width: 36px; height: 36px; display: grid; place-items: center; margin-bottom: 4px;
  border: 0; border-radius: 999px;
  background: var(--cal-bg-subtle); color: var(--cal-text-muted);
}
.empty-state .empty-ic svg { width: 18px; height: 18px; }
.empty-state h3 { margin: 0; font-size: .95rem; font-weight: 600; color: var(--cal-text-emphasis); }
.empty-state p { margin: 0; font-size: .82rem; color: var(--cal-text-subtle); max-width: 42ch; }
.empty-state .btn { margin-top: 10px; }
/* The error variant now actually lands on the error surface. --cal-error-bg
   was defined for it and never wired up, so a failed load rendered on the plain
   white card and only the icon changed colour — the one state where the
   operator most needs to see at a glance that something went wrong. The text
   ratios in section 1 were certified on THIS fill (error text 7.92:1 on it),
   not on white, so the pairing is the point. */
.empty-state.error {
  background: var(--cal-error-bg);
  border: 1px solid var(--cal-error-border);
}
.empty-state.error { border-radius: var(--radius-lg); }
.empty-state.error .empty-ic { color: var(--cal-error); border: 1px solid var(--cal-error-border); background: var(--cal-bg); }

/* A button that is waiting. The label and the spinner share one line; the
   spinner is hidden until .is-busy and takes the label's colour, so it is
   white on the brand button and blue on an outline one with no extra rule.
   min-width holds the button's resting width, so swapping "Sign in" for
   "Signing in…" does not make the card twitch under a clerk's hand. */
.btn .spinner { width: 16px; height: 16px; border-width: 2px; }
.btn:not(.is-busy) .spinner { display: none; }
.btn.is-busy { cursor: progress; }

/* DELIBERATE ROUND EXCEPTION (3 of 3). A square spinner does not spin, it
   wobbles. This one is a circle by definition, not by decoration. */
.spinner {
  display: inline-block; width: 18px; height: 18px; flex: none;
  border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
  border-top-color: currentColor;
  border-radius: 50%;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 2s; } }

/* THE SKELETON — a grey shape with a soft band of light crossing it ONCE,
   left to right, then a rest before the next pass. The owner: the skeleton
   should not "just pop", it should look like it LOADS. The old shimmer was
   a 200% gradient left to tile and shunted two whole periods a beat, so a
   highlight crossed every bar twice a second and read as flicker rather
   than progress. Now the band is a single 60%-wide sweep, no-repeat, from
   fully off the left edge (-150%) to fully off the right (250%) in the
   first 65% of the beat; the rest of the beat it waits off-stage — which is
   what makes it a pulse and not a strobe. The band is the fill's own step
   up (--cal-bg-subtle → --cal-bg-emphasis → back), so its edges melt into
   the base with no seam and nothing new is inked.
   A LIST LOADS DOWNWARD. --skel-at (set by the ladder below from a row's
   place in its list) holds each row's sweep back 90ms more than the row
   above, so the light travels down the list the way the dashboard fills —
   top to bottom. Nothing outside a list sets it; everything else sweeps
   together, as before.
   Every shape below is the SAME rule (one selector list), so a .skel-line
   and the older .skeleton bar can never drift apart. The fill and the band
   are read through --skel-base / --skel-band (internal, like --skel-at:
   markup never sets them) so that .skel-on-canvas, below, can re-point
   the pair for a shape that has no card under it. */
.skeleton,
.skel-line, .skel-block, .skel-circle,
.skel-row > *, .skel-row:empty::before, .skel-row:empty::after {
  background-color: var(--skel-base, var(--cal-bg-subtle));
  background-image: linear-gradient(90deg, var(--skel-base, var(--cal-bg-subtle)) 0%, var(--skel-band, var(--cal-bg-emphasis)) 50%, var(--skel-base, var(--cal-bg-subtle)) 100%);
  background-repeat: no-repeat;
  background-size: 60% 100%;
  background-position: -150% 0;
  animation: shimmer 1.6s ease-in-out infinite;
  animation-delay: calc(var(--skel-at, 0) * 90ms);
}
.skeleton { height: 12px; border-radius: var(--radius-sm); }
@keyframes shimmer {
  0%        { background-position: -150% 0; }
  65%, 100% { background-position: 250% 0; }
}

/* ── The shapes (the dashboard's placeholders) ──
   Drawn in the SHAPE of what is coming, so the page is already laid out
   while it loads and the real thing lands in a space that was waiting for
   it. All aria-hidden in the markup; the region holding them carries
   aria-busy until the content arrives. */
/* A line of text: --w wide (100% unless the markup says), a pill so it
   reads as a line and not as a box. .is-lg stands for a big numeral,
   .is-sm for a caption. */
.skel-line { display: block; width: var(--w, 100%); max-width: 100%; height: 10px; border-radius: var(--radius-pill); }
.skel-line.is-lg { height: 16px; }
.skel-line.is-sm { height: 8px; }
/* A block ignores text-align, so in a right-aligned figure column the bar
   would sit at the LEFT and the amount would land on the other side of the
   cell. Pushed right, it holds the place the figure will take. */
.num > .skel-line { margin-inline-start: auto; }
/* A chart or any block of content: --h tall (160px unless told), the small
   panel corner — it sits inside a card, so the card's own corner would be
   too round for it. */
.skel-block { display: block; width: 100%; height: var(--h, 160px); border-radius: var(--radius-lg); }
/* An icon disc or an avatar. A circle by definition, like the three round
   exceptions, so 50% rather than a radius token. */
.skel-circle { display: block; flex: none; width: var(--s, 32px); height: var(--s, 32px); border-radius: 50%; }
/* Lines stacked like a paragraph or a tile's label / figure / note. The
   stack itself paints nothing — only its children shimmer. */
.skel-stack { display: flex; flex-direction: column; gap: 8px; min-width: 0; }

/* A SKELETON WITH NO CARD UNDER IT. The fill above is --cal-bg-subtle,
   which is 1.11:1 on a white card but 1.00:1 on --cal-canvas — the same
   colour to the eye — so a shape standing on the bare page (the desk's calendar,
   whose toolbar and legend sit on the canvas and whose week card has not
   been drawn yet) was invisible at rest, and all that showed was a band
   drifting over nothing. On the canvas the pair turns over: the fill steps
   DOWN to --cal-bg-emphasis (1.14:1 on the canvas, the same presence a
   shape has on a card) and the band is the lighter --cal-bg-subtle, a
   sheen crossing a slab. Set on the container, inherited by its shapes;
   no new ink. */
.skel-on-canvas { --skel-base: var(--cal-bg-emphasis); --skel-band: var(--cal-bg-subtle); }

/* A LIST ROW, shaped like the desk's sale row (.desk-sale, section 18D):
   the ticket number (60%) and the amount (44px) on line one, the trip
   (80%, caption height) on line two, nothing under the amount. The same
   padding, the same bleed over the panel body's 4px, the same hairline,
   and line boxes of .desk-sale's own line-heights (1.2rem, 1.1rem, a 2px
   gap), so the row is the height the sale will be and the list does not
   jump when the sales land. KEEP IN STEP WITH .desk-sale.
   It takes three children of any kind (a .skel-line each is natural); the
   nth-child rules place and size them, and a --w set on one inline still
   wins. An EMPTY .skel-row draws its two text lines itself (::before,
   ::after), so a bare <div class="skel-row"></div> is a valid row too. */
.skel-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: 1.2rem 1.1rem;
  grid-template-areas: "a b" "c d";
  column-gap: 10px; row-gap: 2px;
  align-items: center;
  margin: 0 -4px;
  padding: 6px;
  border-top: 1px solid var(--cal-border-muted);
}
.skel-row:first-child { border-top: 0; }
.skel-row > *, .skel-row:empty::before, .skel-row:empty::after {
  display: block; width: var(--w, 100%); max-width: 100%; height: 10px;
  border-radius: var(--radius-pill);
}
.skel-row:empty::before, .skel-row:empty::after { content: ''; }
.skel-row > :nth-child(1), .skel-row:empty::before { grid-area: a; --w: 60%; }
.skel-row > :nth-child(2)                          { grid-area: b; --w: 44px; }
.skel-row > :nth-child(3), .skel-row:empty::after  { grid-area: c; --w: 80%; height: 8px; }

/* The downward ladder: each row of a skeleton list (and each line of a
   stack, and each row of a table skeleton) sweeps one step after the one
   above it. Five steps, then every later row shares the fifth — a sixth
   row 90ms later than the fifth would be read as lag, not as a wave. */
.skel-row:nth-child(2), .skel-stack > :nth-child(2), .data-table tbody tr:nth-child(2) { --skel-at: 1; }
.skel-row:nth-child(3), .skel-stack > :nth-child(3), .data-table tbody tr:nth-child(3) { --skel-at: 2; }
.skel-row:nth-child(4), .skel-stack > :nth-child(4), .data-table tbody tr:nth-child(4) { --skel-at: 3; }
.skel-row:nth-child(5), .skel-stack > :nth-child(5), .data-table tbody tr:nth-child(5) { --skel-at: 4; }
.skel-row:nth-child(n+6), .skel-stack > :nth-child(n+6), .data-table tbody tr:nth-child(n+6) { --skel-at: 5; }

/* Motion off: the shapes simply sit there, grey, until the content lands.
   After every rule above that sets `animation`, so it wins on source order. */
@media (prefers-reduced-motion: reduce) {
  .skeleton,
  .skel-line, .skel-block, .skel-circle,
  .skel-row > *, .skel-row:empty::before, .skel-row:empty::after { animation: none; }
}

/* ============================================================
   13. STAT TILES
   ============================================================ */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
/* The admin's four KPIs as a 2×2 block beside the month chart (the proposals
   console's grid); one column on a phone. A modifier on .stat-grid, so
   Funds' and Reports' auto-fit rows are untouched. */
.stat-grid.kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 560px) { .stat-grid.kpi-grid { grid-template-columns: minmax(0, 1fr); } }

/* THE KPI TILE — the Tesla figure: a tonal icon, a tiny grey label, a large
   LIGHT numeral, a note. A resting card (hairline + --shadow-card); a column
   with a 4px rhythm, so the four parts stack the same whatever each holds. */
.stat-tile {
  display: flex; flex-direction: column; gap: 4px; min-width: 0;
  /* A query container, so the figure can be sized to the room the tile
     actually has (.stat-tile .stat-value below). */
  container-type: inline-size;
  background: var(--cal-bg);
  border: 1px solid var(--cal-border-subtle);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-card);
  padding: 16px 18px;
}
/* The tonal icon box, 30px, tinted by data-tile (section 16) so the tile's
   meaning is carried by the icon rather than by a coloured edge. */
.stat-ic {
  width: 30px; height: 30px; display: grid; place-items: center;
  border-radius: var(--radius-md);
  background: var(--badge-role-bg); color: var(--badge-role-text);
  margin-bottom: 2px;
}
.stat-ic svg { width: 16px; height: 16px; }
/* NOT uppercase: the tiny grey label under a Tesla figure is set in
   sentence case, and so is this. */
.stat-label { font-size: .76rem; font-weight: 500; color: var(--cal-text-subtle); }
.stat-value { font-size: var(--fs-display); font-weight: 500; letter-spacing: -.02em; color: var(--cal-text-emphasis); line-height: 1.1; font-variant-numeric: tabular-nums; }
/* THE FIGURE FITS THE TILE. The display step is what the figure wants, and
   the tile does not always have it. "$12,345.00" in Inter's tabular digits
   is ~5.3× its font size wide; at the old 36px it was ~190px in a 158px
   tile and painted straight through the card's edge into its neighbour,
   and nothing wraps a sum of money. So the size gives: 16% of the tile's
   content width (cqi) — the figure then fills ~85% of it — capped at the
   display step and floored where even that is too big. MEASURED FOR THE
   DENSE LAYOUT (then a 240px sidebar — the roomier 260px rail takes ~5px
   more off each tile, and the figure is sized by the tile, so it still
   clears — 24px gutters, 16px section gap, 12px tile
   gap, 16/18px tile padding), the admin's 2×2 block beside the month
   chart: at 1366px a tile is ~215px wide, 177px inside, and the figure
   takes the full 28px display step (~148px); at 1280px it is ~197px,
   159px inside, the figure 25.5px (~135px; an eleven-glyph "$123,456.00"
   ~150px) — both clear. Just above the 1100px stack, the tightest it
   gets, a tile is 122px inside and the figure 19.5px (~103px). The vw
   line first is the
   fallback for a browser without container units, tuned so the same three
   widths clear (22–25px). The hero's figure is not a tile and keeps its
   own size: its track is min-content and never squeezes it. */
.stat-tile .stat-value {
  font-size: clamp(1.125rem, 1.1vw + .6rem, var(--fs-display));
  font-size: clamp(1.125rem, 16cqi, var(--fs-display));
}
/* The note holds text, or the vs-yesterday chip and text, on one line that
   may wrap. */
.stat-note { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: var(--fs-small); color: var(--cal-text-muted); }
/* The delta chip: ▲ 12% in the green trio, ▼ 8% in the error trio (7.92:1).
   The arrow is text, so it survives forced colours and a mono printer. */
.stat-delta {
  display: inline-flex; align-items: center;
  height: 20px; padding: 0 7px;
  border-radius: var(--radius-pill);
  font-size: .68rem; font-weight: 700; font-variant-numeric: tabular-nums;
  border: 1px solid transparent;
}
.stat-delta.is-up   { color: var(--badge-ok-text); background: var(--badge-ok-bg); border-color: var(--badge-ok-border); }
.stat-delta.is-down { color: var(--cal-error); background: var(--cal-error-bg); border-color: var(--cal-error-border); }

/* ============================================================
   14. DROPDOWN MENU
   The shell's one menu primitive — App.dropdown(anchorEl, items, opts) in
   public/app.js. Every future data-entry flow hangs off one of these, so
   the classes below are a CONTRACT with that function; do not rename them
   and do not re-implement a menu inside a view.

   DOM the JS builds:
     <div class="dropdown-menu" role="menu">
       <button class="dropdown-item" role="menuitem">
         <span class="dropdown-item-icon">svg</span>
         Funds
         <span class="dropdown-item-hint">Phase 2</span>   (optional)
       </button>
       <div class="dropdown-sep" role="separator"></div>
       ...
     </div>

   POSITION: absolute, in page coordinates set inline by the JS (top/left),
   the same way .avatar-menu is placed. A floating layer — the overlay rung
   of the ladder — and it settles in over 160ms like the avatar menu does.
   ============================================================ */
.dropdown-menu {
  position: absolute;
  z-index: 60;
  min-width: 220px;
  max-width: 320px;
  padding: 5px;
  background: var(--cal-bg);
  border: 1px solid var(--cal-border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-overlay);
  animation: menuIn 160ms var(--ease);
}

.dropdown-item {
  display: flex; align-items: center; gap: 8px;
  width: 100%;
  padding: 8px 10px;
  border: none; border-radius: var(--radius-md);
  background: none;
  color: var(--cal-text);
  font: inherit; font-size: .82rem; font-weight: 500;
  text-align: left;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
/* :focus, not only :focus-visible — arrow-key navigation moves focus
   programmatically and the highlight has to follow it every time, not only
   when the UA decides the interaction looked keyboard-ish. */
.dropdown-item:hover,
.dropdown-item:focus {
  outline: none;
  background: var(--cal-bg-subtle);
  color: var(--cal-text-emphasis);
}
.dropdown-item-icon { display: inline-flex; flex: none; color: var(--cal-text-muted); }
.dropdown-item-icon svg { width: 15px; height: 15px; }
.dropdown-item:hover .dropdown-item-icon,
.dropdown-item:focus .dropdown-item-icon { color: var(--cal-brand); }

/* The hint is why an item is NOT actionable ("Phase 2"), so it sits at the far
   right of the row where the eye lands after reading the label. */
.dropdown-item-hint { margin-left: auto; padding-left: 10px; font-size: .7rem; font-weight: 500; color: var(--cal-text-muted); white-space: nowrap; }

/* A hint that is a whole sentence — the day-close lock's, the wrong-terminal
   one's — takes a line of its own under the label, indented past the icon
   (15px + the 8px gap, and a pixel so it sits on the label's stem),
   instead of squeezing the label to make room at the right. The JS adds
   both classes together (App.dropdown) when the hint is longer than a tag. */
.dropdown-item.has-sentence { flex-wrap: wrap; }
.dropdown-item-hint.is-sentence {
  flex-basis: 100%; margin-left: 0; padding-left: 24px; margin-top: -2px;
  white-space: normal; line-height: 1.4;
}

.dropdown-sep { height: 1px; margin: 5px 0; background: var(--cal-border-subtle); }

/* Dimmed, and it stays dimmed on hover: nothing about pointing at a disabled
   item should suggest it is about to do something. */
.dropdown-item[aria-disabled="true"] { color: var(--cal-text-muted); cursor: default; }
.dropdown-item[aria-disabled="true"]:hover,
.dropdown-item[aria-disabled="true"]:focus { background: none; color: var(--cal-text-muted); }
.dropdown-item[aria-disabled="true"] .dropdown-item-icon,
.dropdown-item[aria-disabled="true"]:hover .dropdown-item-icon { color: var(--cal-text-muted); opacity: .7; }

/* ============================================================
   15. CALENDAR
   App.Calendar(mountEl, opts) — public/components/calendar.js. A COMPONENT,
   not a view: the dashboard mounts it today and the bus-schedule page will
   mount the same one, so nothing here may assume the dashboard.

   Modelled on Google Calendar: a toolbar, a sticky day header, an all-day
   strip, and a scrolling grid of 24 hour rows with a red now-line. A card
   like the rest of the app — the week and the month grids sit on the rest
   rung of the ladder — and today's date marker is a filled brand CIRCLE,
   as Google draws it.

   DOM CONTRACT — the JS must build exactly this shape, because the three
   grids (head, all-day, body) align on --cal-gutter-w and on identical
   auto-column tracks, and nothing else keeps their columns in step:

     .cal
       .cal-toolbar   [ .cal-title  .cal-nav>(.cal-nav-btn .cal-today-btn
                        .cal-nav-btn)  .cal-note  .cal-views>.seg ]
       .cal-legend    [ .cal-legend-item[.is-live] > .cal-legend-dot + text ]
                      (only when the caller passed a legend)
       .cal-week                         (or .cal-month)
         .cal-week-head  [ .cal-corner, then N x .cal-daycol-head[.today]
                           > (.cal-dow + .cal-daynum[.today]) ]
         .cal-allday     [ .cal-allday-label, then N day cells ]
                         (only when an all-day event is in range)
         .cal-body       [ .cal-gutter > 24 x .cal-hourlabel
                           .cal-cols   > N x .cal-col[.weekend][.today]
                                           > 24 x .cal-hour > .cal-halfhour
                                           + .cal-now > .cal-now-dot
                                           + .cal-event[.is-arrival|.is-departure]
                                                       [.is-past|.is-live|.is-next]
                                                       [.is-short]
                                               > .cal-event-time
                                               + .cal-event-title
                                               + .cal-event-badge > .cal-event-dot?
                                               (tall: the time and badge sit in a
                                                .cal-event-head row, and a
                                                .cal-event-sub line follows) ]
       .cal-month
         7 x .cal-month-dow, then 42 x .cal-month-cell[.other-month][.today]
                                    > .cal-month-date
                                    + .cal-month-events > .cal-chip*
                                        > (.cal-chip-dot .cal-chip-time .cal-chip-title)
                                                        + .cal-chip-more
       .cal-pop  (appended to .cal while open)
         .cal-pop-bar  .cal-pop-close  .cal-pop-kind  .cal-pop-title
         .cal-pop-sub  .cal-pop-rows (dl)  .cal-pop-live > .cal-event-dot

   N is 7 in the week view and 1 in the phone fallback; the column tracks are
   implicit (grid-auto-columns), so both work with no extra rule.

   .cal-halfhour is a CHILD of its .cal-hour — it is drawn at 50% of the row's
   height, so it follows --cal-hour-h without any second number to keep in sync.

   Events are positioned by the JS with inline top/height/left/width in
   percentages of the day column; the CSS only says what an event looks like.
   A CHIP IS A MOMENT, not a journey: the dashboard puts an arrival at the
   minute the bus pulls in and a departure at the minute it leaves, so most
   chips are the 30-minute visual floor (24px at the 48px hour) and read as
   one line. The full journey is in the popover.
   ============================================================ */
/* position: relative — the popover (.cal-pop) is placed against, and kept
   inside, the calendar box.
   THE ARRIVAL GREEN IS THE LOGO'S EMERALD. The desk hands the component
   `color: 'var(--nbc-green)'` for an arrival, and the component writes it
   onto the chip as an inline --cal-event-bg — inline, so no class rule can
   outrank it. But var() in an inline style resolves on the chip, against
   whatever --nbc-green the chip inherits, so re-pointing it HERE, for this
   subtree only, turns every arrival chip, legend dot, popover bar and live
   pulse emerald with no JS change. --nbc-emerald carries what the green
   did: white on it 7.04:1 (the Next badge's type), it on white 7.04:1 (the
   popover's ARRIVAL kicker). Nothing in the calendar uses --nbc-green for
   anything but that injected event colour. */
.cal { position: relative; display: flex; flex-direction: column; min-width: 0; --nbc-green: var(--nbc-emerald); }

/* ── Toolbar ── */
.cal-toolbar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding-bottom: 10px;
}
.cal-title {
  margin: 0;
  font-size: var(--fs-h2); font-weight: 600; letter-spacing: -.005em;
  color: var(--cal-text-emphasis);
  white-space: nowrap;
}
.cal-nav { display: inline-flex; align-items: center; gap: 5px; }
/* The nav and Today buttons on the white-control recipe: --ctl-h-row
   (32px), hairline + control shadow, the edge darkening under a hand. */
.cal-nav-btn {
  width: var(--ctl-h-row); height: var(--ctl-h-row);
  display: grid; place-items: center;
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-md);
  background: var(--cal-bg); color: var(--cal-text-emphasis);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.cal-nav-btn:hover { border-color: var(--cal-border); }
.cal-nav-btn svg { width: 15px; height: 15px; }
.cal-today-btn {
  height: var(--ctl-h-row); padding: 0 12px;
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-md);
  background: var(--cal-bg); color: var(--cal-text-emphasis);
  box-shadow: var(--shadow-sm);
  font: inherit; font-size: .78rem; font-weight: 600;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.cal-today-btn:hover { border-color: var(--cal-border); }
.cal-nav-btn:focus-visible,
.cal-today-btn:focus-visible {
  outline: none;
  border-color: var(--cal-border-emphasis);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--cal-border-emphasis) 25%, transparent);
}
/* Pinned right whatever order the toolbar's children arrive in. The pill
   track (section 5) is --ctl-h-row overall, its segments that less the 3px
   + 1px — the same height as the nav and Today buttons, so the whole
   toolbar sits on one line. */
.cal-views { margin-left: auto; flex: none; }
.cal-views .seg button { height: calc(var(--ctl-h-row) - 8px); padding: 0 12px; font-size: .78rem; line-height: 1; }

/* The quiet line that says the grid is empty on purpose ("Bus schedule appears
   here once it is connected"). It reads as a caption, never as an error —
   nothing is wrong, the data simply has not been built yet. Styled to sit
   either inside the toolbar or directly under it. */
.cal-note { font-size: .74rem; color: var(--cal-text-muted); min-width: 0; }
.cal > .cal-note { padding: 0 0 8px; }

/* ── Legend ──
   The caller's sentence about its colours, under the toolbar: "● Arriving at
   Belize City Terminal  ● Departing  ● En route now". Each item carries its
   colour on --cal-event-bg like a chip does, so the dot is the same ink as the
   chips it explains. .is-live pulses the dot exactly as a live chip's badge
   does — the legend shows the motion it is naming. */
.cal-legend {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px;
  padding: 0 2px 8px;
  font-size: .72rem; font-weight: 500;
  color: var(--cal-text-subtle);
}
.cal-legend-item { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
.cal-legend-dot { flex: none; width: 9px; height: 9px; border-radius: 999px; background: var(--cal-event-bg); }
.cal-legend-item.is-live .cal-legend-dot { animation: cal-pulse 1.6s ease-in-out infinite; }

/* ── Week view ── */
.cal-week {
  display: flex; flex-direction: column;
  min-width: 0;
  background: var(--cal-bg);
  border: 1px solid var(--cal-border-subtle);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-card);
  /* Clips the sticky head and the hour grid to the card's rounded corners. */
  overflow: hidden;
}

/* Head, all-day strip and body all start with one fixed gutter track and then
   as many equal columns as there are days. grid-auto-columns is what makes the
   seven-day week and the one-day phone fallback the same stylesheet. */
.cal-week-head,
.cal-allday {
  display: grid;
  grid-template-columns: var(--cal-gutter-w);
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  padding-right: var(--cal-scrollbar);
  background: var(--cal-bg);
}
/* Sticky, so the day columns stay labelled while the hour grid is read. Its
   containing block is .cal-week, which confines it to the calendar — it can
   never float free over the rest of the page. */
.cal-week-head {
  position: sticky; top: 0; z-index: 3;
  border-bottom: 1px solid var(--cal-border-subtle);
}
/* The corner is deliberately empty — it is the head of the time gutter, and a
   label there would collide with the 12 AM row directly beneath it. */
.cal-corner { min-width: 0; }
.cal-daycol-head {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  min-width: 0;
  padding: 6px 4px 5px;
  /* Left borders only: the first day's left edge IS the gutter divider, so a
     right border on .cal-corner would double it. */
  border-left: 1px solid var(--cal-border-subtle);
}
.cal-daycol-head.weekend { background: var(--cal-bg-muted); }
.cal-dow {
  font-size: .64rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--cal-text-muted);
}
/* Today is a filled brand CIRCLE, as Google draws it. White on NBC blue is
   8.18:1. min-width rather than width so a two-digit date never clips. */
.cal-daynum {
  display: grid; place-items: center;
  min-width: 26px; height: 26px; padding: 0 5px;
  font-size: .92rem; font-weight: 600; font-variant-numeric: tabular-nums;
  color: var(--cal-text-emphasis);
}
.cal-daynum.today { background: var(--cal-brand); color: var(--cal-brand-text); border-radius: 999px; }

/* Events with no time. The component draws the strip only when an all-day
   event is in range: a bus timetable has none, and a permanently empty row
   between the day names and 4 AM was dead space on every desk. */
.cal-allday {
  min-height: 30px;
  border-bottom: 1px solid var(--cal-border-subtle);
}
.cal-allday-label {
  display: flex; align-items: center; justify-content: flex-end;
  padding: 5px 8px;
  font-size: .62rem; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  color: var(--cal-text-muted);
}
/* Whatever element the component uses for a day cell here — the rule keys off
   position, not off a class, so the strip cannot fall out of step with the
   columns above and below it. */
.cal-allday > :not(.cal-allday-label) {
  display: flex; flex-direction: column; gap: 2px;
  min-width: 0;
  padding: 4px;
  border-left: 1px solid var(--cal-border-subtle);
}
.cal-allday > .weekend { background: var(--cal-bg-muted); }

/* The scrolling half. Height is bounded so the hour grid scrolls inside the
   page rather than pushing the page taller than the screen; the clamp keeps it
   usable on a 13" terminal machine and on a wall-mounted display alike. */
.cal-body {
  position: relative;
  display: flex;
  height: clamp(320px, calc(100vh - 420px), 720px);
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-gutter: stable;
}
.cal-gutter { flex: none; width: var(--cal-gutter-w); }
/* The label sits at the TOP of its row, which is the hour line itself. Aligning
   it to the line by translating it upwards instead would clip 12 AM against the
   top of the scroll box on first paint. */
.cal-hourlabel {
  height: var(--cal-hour-h);
  padding: 2px 8px 0 0;
  text-align: right;
  font-size: .68rem; line-height: 1;
  color: var(--cal-text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.cal-cols {
  flex: 1 1 auto; min-width: 0;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
}
.cal-col {
  position: relative;
  min-width: 0;
  border-left: 1px solid var(--cal-border-subtle);
}
/* Google's faint weekend wash — 1.06:1 off white, enough to group Saturday and
   Sunday and far too little to compete with an event. */
.cal-col.weekend { background: var(--cal-bg-muted); }
/* Today's column: the subtle surface with a breath of the brand in it, so
   the eye lands on today before it finds the now-line. After .weekend so a
   Saturday that is today reads as today. The head cell matches. */
.cal-col.today,
.cal-daycol-head.today { background: color-mix(in srgb, var(--cal-brand) 4%, var(--cal-bg-subtle)); }

/* The rule is on the TOP of each row, not the bottom: the hour line for 9 AM
   should be the line the 9 AM label sits against, and drawing it this way means
   the first row's line is the header's own border (skipped here) and the last
   row ends on the container's border — neither is ever doubled. Doing it with
   border-bottom would need a :last-child that the now-line and event blocks
   sitting after the 24 rows in the same column would quietly break. */
.cal-hour {
  position: relative;
  height: var(--cal-hour-h);
  border-top: 1px solid var(--cal-border-subtle);
}
.cal-hour:first-child { border-top: none; }
/* Lighter than the hour rule on purpose: it is an aid for reading a 30-minute
   departure off the grid, not a second hour line. */
.cal-halfhour {
  position: absolute; left: 0; right: 0; top: 50%;
  border-top: 1px solid var(--cal-border-muted);
  pointer-events: none;
}

/* The now-line. The single most recognisable thing about a week view: a 2px red
   rule across today's column, positioned by the minute and redrawn every 60s.
   z-index above events — "it is 10:42 right now" must never be hidden behind a
   block that happens to cover 10:42. */
.cal-now {
  position: absolute; left: 0; right: 0;
  height: 0;
  border-top: 2px solid var(--cal-now);
  z-index: 6;
  pointer-events: none;
}
/* Square, like every other marker here. Centred on the 2px rule: the marker is
   8px tall, so -3px puts its middle on the line's middle. */
.cal-now-dot {
  position: absolute; left: 0; top: -3px;
  width: 8px; height: 8px;
  background: var(--cal-now);
}

/* ── An event chip ──
   A light TINT of the event colour with DARK type and a solid 3px bar of the
   colour down the left — the way Google draws a calendar since 2018, and the
   reason a week of buses no longer reads as a wall of solid slabs. The
   hairline is the same tint a shade deeper, so two chips sharing an hour still
   read as two. --cal-event-* are custom properties, so a per-event colour is
   one inline property on the element rather than the component writing
   colours by hand; the mixes below all derive from that one property.
   A chip is a BUTTON (role=button, tabindex=0 — it opens the popover), so it
   has a pointer, a hover lift and the app's focus ring. */
.cal-event {
  position: absolute;
  z-index: 5;
  overflow: hidden;
  box-sizing: border-box;
  min-height: 15px;
  padding: 2px 6px 2px 7px;
  background: color-mix(in srgb, var(--cal-event-bg) 14%, var(--cal-bg));
  color: var(--cal-text-emphasis);
  border: 1px solid color-mix(in srgb, var(--cal-event-bg) 26%, var(--cal-bg));
  border-left: 3px solid var(--cal-event-bg);
  border-radius: var(--radius-sm);
  font-size: .74rem; line-height: 1.25;
  text-align: left;
  cursor: pointer;
  transition: box-shadow var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
/* Above its neighbours (5) while lifted — and above the now-line (6), which
   is fine for the moment a hand rests on it. The raised rung, and 1px north. */
.cal-event:hover { z-index: 7; box-shadow: var(--shadow-lift); transform: translateY(-1px); }
.cal-event:focus-visible {
  z-index: 7; outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--cal-border-emphasis) 30%, transparent), var(--shadow-lift);
}
.cal-event-head { display: flex; align-items: center; justify-content: space-between; gap: 6px; min-width: 0; }
.cal-event-time { flex: none; font-size: .68rem; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.cal-event-title { display: block; min-width: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The journey in one line — "arrives 8:15 · left Corozal 5:30" — shown only
   on a chip tall enough (the component decides, 40px); the title attribute
   always carries it. --cal-text-subtle (6.03:1 on white) rather than -muted so
   it still clears AA on the tint. */
.cal-event-sub { display: block; font-size: .68rem; color: var(--cal-text-subtle); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The 30-minute floor is one 24px line: time, title, then the compact badge
   pinned right. The title gives way first. */
.cal-event.is-short { display: flex; align-items: center; gap: 6px; padding-top: 0; padding-bottom: 0; }
.cal-event.is-short .cal-event-title { flex: 1 1 auto; }
.cal-event.is-short .cal-event-badge { margin-left: auto; }

/* The badge pill: "Next" · "En route · arrives in 25 min" · "Boarding · leaves
   in 12 min". A deeper tint of the chip's colour with the same dark type; on a
   NEXT chip it goes to the full colour with white type (--cal-event-text). */
.cal-event-badge {
  display: inline-flex; align-items: center; gap: 4px;
  flex: none; max-width: 100%;
  height: 15px; padding: 0 6px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--cal-event-bg) 22%, var(--cal-bg));
  color: var(--cal-text-emphasis);
  font-size: .62rem; font-weight: 700; line-height: 1; letter-spacing: .01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The pulsing dot on a live badge: the bus is moving right now. The ring is
   the event colour fading out; the dot itself never moves, so it stays a
   marker and not a distraction. */
.cal-event-dot {
  flex: none; width: 6px; height: 6px; border-radius: 999px;
  background: var(--cal-event-bg);
  animation: cal-pulse 1.6s ease-in-out infinite;
}
@keyframes cal-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--cal-event-bg) 45%, transparent); }
  70%  { box-shadow: 0 0 0 5px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .cal-event-dot, .cal-legend-item.is-live .cal-legend-dot { animation: none; }
}

/* ── States ──
   live: the bus is on the road (an arrival) or boarding (a departure) — the
         tint deepens to 24% so the chip stands forward of its neighbours.
   next: the first upcoming one of its kind today — a 4px bar and the badge in
         the full colour, so "which one is next" is answered from across the
         room.
   past: gone — at .55 it recedes without disappearing; the day's history is
         still readable. No pulse on anything past. */
.cal-event.is-live { background: color-mix(in srgb, var(--cal-event-bg) 24%, var(--cal-bg)); border-color: color-mix(in srgb, var(--cal-event-bg) 36%, var(--cal-bg)); }
.cal-event.is-next { border-left-width: 4px; }
.cal-event.is-next .cal-event-badge { background: var(--cal-event-bg); color: var(--cal-event-text); }
.cal-event.is-past { opacity: .55; }
.cal-event.is-past .cal-event-dot { animation: none; }

/* ── The popover ──
   One at a time, appended to .cal and positioned by the component beside the
   chip that opened it: the colour bar, the kind, the title large, the journey
   line, then Departs · Arrives · Journey · Fare · Runs, and the live sentence.
   A floating layer, so it carries the overlay shadow and settles in like a
   menu does. */
.cal-pop {
  position: absolute; z-index: 20;
  width: min(280px, calc(100% - 8px));
  padding: 12px 14px 12px 18px;
  background: var(--cal-bg);
  border: 1px solid var(--cal-border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-overlay);
  color: var(--cal-text);
  overflow: hidden;
  outline: none;
  animation: menuIn 160ms var(--ease);
}
.cal-pop-bar { position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--cal-event-bg); }
.cal-pop-close {
  position: absolute; top: 7px; right: 7px;
  width: 26px; height: 26px;
  display: grid; place-items: center;
  border: none; border-radius: var(--radius-md);
  background: none; color: var(--cal-text-muted);
  cursor: pointer;
}
.cal-pop-close:hover { background: var(--cal-bg-subtle); color: var(--cal-text-emphasis); }
.cal-pop-close:focus-visible { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--cal-border-emphasis) 30%, transparent); }
.cal-pop-close svg { width: 15px; height: 15px; }
/* "ARRIVAL" / "DEPARTURE" in the event colour: 8.18:1 (blue) and 7.04:1
   (emerald — see .cal above) on white, both fine at 700 weight. */
.cal-pop-kind {
  display: block;
  font-size: .62rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--cal-event-bg);
  margin-bottom: 2px;
}
.cal-pop-title { padding-right: 26px; font-size: .95rem; font-weight: 700; line-height: 1.25; color: var(--cal-text-emphasis); }
.cal-pop-sub { margin-top: 2px; font-size: .76rem; color: var(--cal-text-subtle); }
.cal-pop-rows {
  display: grid; grid-template-columns: auto 1fr; gap: 5px 12px;
  margin: 10px 0 0;
  font-size: .78rem;
}
.cal-pop-rows dt { margin: 0; color: var(--cal-text-muted); font-weight: 500; }
.cal-pop-rows dd { margin: 0; color: var(--cal-text-emphasis); font-weight: 600; font-variant-numeric: tabular-nums; }
.cal-pop-live {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 10px; padding: 3px 9px 3px 8px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--cal-event-bg) 14%, var(--cal-bg));
  font-size: .74rem; font-weight: 600; color: var(--cal-text-emphasis);
}

/* ── Month view ── */
/* ALWAYS six week rows. A month that starts on a Sunday needs six and a 31-day
   month starting on a Saturday needs six, so every month gets six and the page
   height never jumps as the operator pages through the year. The 7 day-name
   cells are the `auto` first row; the 42 date cells are the six below it. */
.cal-month {
  display: grid;
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  grid-template-rows: auto repeat(6, minmax(92px, 1fr));
  min-width: 0;
  background: var(--cal-bg);
  border: 1px solid var(--cal-border-subtle);
}
.cal-month-dow {
  padding: 8px 6px;
  text-align: center;
  font-size: .68rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--cal-text-muted);
}
/* Top and left borders only, and none on the first of each — the container's
   own border closes the grid, so no edge is ever drawn twice. The day-name row
   needs no bottom rule of its own: the first week's top borders are it. */
.cal-month-cell {
  display: flex; flex-direction: column; gap: 3px;
  min-width: 0; overflow: hidden;
  padding: 5px 6px 6px;
  border-top: 1px solid var(--cal-border-subtle);
  border-left: 1px solid var(--cal-border-subtle);
}
.cal-month-cell:nth-child(7n + 1) { border-left: none; }
.cal-month-cell.other-month { background: var(--cal-bg-muted); }
.cal-month-cell.other-month .cal-month-date { color: var(--cal-text-muted); font-weight: 500; }

.cal-month-date {
  align-self: flex-start;
  display: grid; place-items: center;
  min-width: 24px; height: 24px; padding: 0 5px;
  font-size: .8rem; font-weight: 600; font-variant-numeric: tabular-nums;
  color: var(--cal-text-emphasis);
}
/* The same filled circle as the week header, whether the component marks the
   cell or the date itself. */
.cal-month-cell.today .cal-month-date,
.cal-month-date.today { background: var(--cal-brand); color: var(--cal-brand-text); border-radius: 999px; }

.cal-month-events { display: flex; flex-direction: column; gap: 2px; min-width: 0; overflow: hidden; }
/* A month chip (and an all-day chip): a colour dot, the time in bold, the
   title — type on the cell, not a filled block, because thirty filled blocks
   in a month is a wall of colour and the dot says arrival-or-departure just
   as well. A button like the week chip, with the tint on hover and the ring
   on focus; past ones dim exactly as they do in the week. */
.cal-chip {
  display: flex; align-items: center; gap: 5px;
  min-width: 0;
  padding: 1px 5px;
  border-radius: var(--radius-sm);
  color: var(--cal-text-emphasis);
  font-size: .72rem; font-weight: 500; line-height: 1.5;
  text-align: left; white-space: nowrap;
  cursor: pointer;
}
.cal-chip:hover { background: color-mix(in srgb, var(--cal-event-bg) 14%, var(--cal-bg)); }
.cal-chip:focus-visible { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--cal-border-emphasis) 30%, transparent); }
.cal-chip-dot { flex: none; width: 7px; height: 7px; border-radius: 999px; background: var(--cal-event-bg); }
.cal-chip-time { flex: none; font-weight: 700; font-variant-numeric: tabular-nums; }
.cal-chip-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.cal-chip.is-past { opacity: .55; }
/* "+3 more" — a count, not an event, so it is type on the cell rather than
   another filled block competing with the three above it. */
.cal-chip-more { padding: 0 5px; font-size: .72rem; font-weight: 500; color: var(--cal-text-muted); white-space: nowrap; }

/* ── Empty ── */
.cal-empty {
  display: flex; align-items: center; justify-content: center;
  padding: 20px 16px;
  text-align: center;
  font-size: .8rem;
  color: var(--cal-text-muted);
}

/* ── Responsive ──
   900px: the week compresses — a narrower gutter, smaller day labels.
   640px: the component re-renders a SINGLE day column for today, because seven
   columns on a phone is unusable. That is a JS decision; the grids here need no
   change to draw one column, since the day tracks are implicit. If it ever does
   hand seven columns to a phone, they still lay out — cramped, not broken. */
@media (max-width: 900px) {
  .cal { --cal-gutter-w: 46px; }
  .cal-toolbar { gap: 8px; }
  .cal-dow { font-size: .62rem; letter-spacing: .04em; }
  .cal-daynum { min-width: 24px; height: 24px; font-size: .9rem; }
  .cal-hourlabel { font-size: .62rem; padding-right: 6px; }
  .cal-month { grid-template-rows: auto repeat(6, minmax(78px, 1fr)); }
}
@media (max-width: 640px) {
  .cal { --cal-gutter-w: 42px; }
  .cal-title { font-size: .88rem; white-space: normal; }
  /* The toggle stops being a right-hand affordance and just joins the row. */
  .cal-views { margin-left: 0; }
  .cal-note { flex-basis: 100%; }
  .cal-body { height: clamp(300px, calc(100vh - 340px), 560px); }
  .cal-month { grid-template-rows: auto repeat(6, minmax(62px, 1fr)); }
  .cal-month-cell { padding: 3px 4px 4px; }
  .cal-month-date { min-width: 20px; height: 20px; font-size: .74rem; }
  .cal-chip, .cal-chip-more { font-size: .66rem; }
}

/* ============================================================
   16. FUNDS LEDGER
   The day's transactions — public/views/funds.js. Everything here sits on
   top of the data table (section 11) and the stat tiles (section 13); only
   what a ledger genuinely needs is new: an amount cell and the voided row.
   The range pills and the sheet table variant live with section 11.
   ============================================================ */

/* The three figures sit above the table; .stat-grid carries no bottom margin
   of its own, so the gap to the ledger is set here rather than inline. */
.ledger-figs { margin-bottom: var(--gap-section); }

/* The category charts between the figures and the ledger, laid out on the
   Reports grid unchanged. .report-grid carries no bottom margin either, so
   the same section gap the figures keep to the table is set here — when
   the section is hidden it takes its margin with it and the page reads as
   it did before the charts existed. */
.ledger-charts { margin: 0 0 var(--gap-section); }

/* The Income / Expense toggle in the shared transaction form. A .form-row is a
   grid, so the control already stretches to the row — but its buttons keep
   their content width and end up huddled at one end of a wide box. Give each
   option an equal share. Scoped to a seg INSIDE a form row, so the calendar
   toolbar's week/month toggle, which is sized by its labels, is untouched. */
.form-row > .seg { width: 100%; }
.form-row > .seg button { flex: 1; }

/* Net. Positive is money kept, negative is a day that went backwards — the one
   figure on this screen allowed to raise its voice. Zero stays neutral: a
   terminal that took nothing and spent nothing is not a green day. */
.stat-value.net-pos { color: var(--ledger-in); }
.stat-value.net-neg { color: var(--ledger-net-neg); }

/* The amount column. .num (section 11) already right-aligns and sets
   tabular-nums; this adds the weight and stops a figure wrapping mid-number. */
.data-table tbody td.amount-cell { font-weight: 600; white-space: nowrap; }
.data-table tbody td.ledger-time { white-space: nowrap; color: var(--cal-text-subtle); }
/* The readable handle — TXN-0042, HND-0007, a ticket number. Monospaced and
   tabular so a column of them lines up and one can be read out without
   losing your place; the document id rides in the cell's title. */
.data-table tbody td.ledger-ref { white-space: nowrap; font-family: var(--font-mono); font-size: .72rem; font-variant-numeric: tabular-nums; color: var(--cal-text-subtle); }

/* The same two classes dress a summary figure and a table cell, so each needs
   the reach to beat what it lands on: .stat-value sets its own colour, and so
   does `.data-table tbody td`. A bare .amount-in would lose to the cell. */
.stat-value.amount-in,
.data-table tbody td.amount-in { color: var(--ledger-in); }
.stat-value.amount-out,
.data-table tbody td.amount-out { color: var(--ledger-out); }

/* A VOIDED ROW STAYS. It is never deleted, so it has to read at a glance as
   struck from the totals without being struck from the record: muted, ruled
   through, and carrying a badge whose title holds the reason. */
.data-table tbody tr.row-voided td { color: var(--cal-text-muted); text-decoration: line-through; }
/* The "Voided" badge and the row's buttons are NOT struck, and no rule below
   has to un-strike them: a decoration on an ancestor cannot be cancelled by a
   descendant, but it also never crosses into an atomic inline box, and both
   .badge and .row-actions are display: inline-flex. That is why the badge
   stays legible on a ruled-through row — do not change either to a plain
   inline display without re-checking this. */
/* A struck-through green figure still reads as money in the day's takings.
   It is not in the total, so it must not keep the total's colour. */
.data-table tbody tr.row-voided td.amount-in,
.data-table tbody tr.row-voided td.amount-out { color: var(--cal-text-muted); }

/* ── Two-column form ──
   The record/edit form has seven fields and was taller than a laptop viewport,
   so it scrolled inside its own overlay — and a form you have to scroll to
   reach the Record button on is one where the button gets missed. Pairing the
   short fields halves the height; only Location and Description, which are
   genuinely wide, span both tracks.

   The overlay widens to suit (see .overlay below). It stays two columns down to
   a phone, because collapsing early would put the scrolling straight back. */
.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px 16px;
  align-items: start;
}
.form-grid > .span-2 { grid-column: 1 / -1; }
/* Location is a read line, not a control — it needs no room under it. */
.form-grid > .form-row-tight { margin-bottom: 2px; }
/* The hint under a field is what varies in height between the two columns, so
   the row heights are set by content and the columns simply run independently. */
.form-grid .form-hint { margin-bottom: 2px; }

@media (max-width: 460px) {
  /* Below this a two-up amount field is genuinely too narrow to read a figure
     in, which matters more than the scroll it costs back. */
  .form-grid { grid-template-columns: 1fr; }
}

/* A value the form shows but does not let you change — the location a
   transaction files against. Styled as text, not as a disabled input: a greyed
   box invites a click and then refuses it, where a line of text simply reads
   as a fact. */
.form-fixed {
  margin: 0;
  font-size: .85rem; font-weight: 500;
  color: var(--cal-text-emphasis);
}
/* The Income/Expense toggle when only an administrator may restate the money.
   Kept visible rather than hidden: which way this transaction went is the
   thing a clerk most needs to READ while fixing its description. */
.seg.is-locked { opacity: .55; pointer-events: none; }
/* The small variant: a two-way switch that sits inside a panel, above a chart,
   sized like the calendar's view toggle so it reads as a control of the panel
   rather than of the page. */
.seg.seg-sm { margin: 0 0 10px; }
.seg.seg-sm button { height: calc(var(--ctl-h-row) - 8px); padding: 0 12px; font-size: .78rem; line-height: 1; }   /* --ctl-h-row (32px) overall, the toolbar-row height */

/* ── The accent card (Funds, Reports, Day Close) ──
   One deliberate brand moment per screen: the task board's dark navy card.
   The SIDEBAR'S OWN GRADIENT (--hero-grad: #02386B into #02264D, on the
   diagonal a wide card wants), so the two dark surfaces on the screen are
   one family rather than two blues arguing. It carries today's headline
   figure in the ring gold — large display only, 6.53:1 at the lightest
   corner — with the stat tiles straddling its foot: half on navy, half on
   canvas. Everything below it stays white-on-canvas; the colour is spent
   HERE, once, on purpose, rather than sprinkled over the charts, which
   already carry a validated palette of their own.

   A CARD now, not a band: it sits inside the page's gutters with the card
   corner and the resting shadow (the sidebar's navy one, --shadow-nav), so
   it reads as an object on the canvas
   like the tiles under it. Its bottom padding is what the tiles overlap
   into — the two numbers (48px here, -34px on the grid) set how far the
   cards sit over the edge, and leave 14px of navy under the marker;
   change them together. */
.page-hero {
  margin: 0;
  padding: 18px 22px 48px;
  border-radius: var(--radius-xl);
  background: var(--hero-grad);
  color: var(--on-navy);
  /* The sidebar's shadow, not the grey rest rung: a navy object on the
     light canvas takes a shadow in its own ink (--shadow-nav, section 1). */
  box-shadow: var(--shadow-nav);
  /* Two columns, no wrapper markup: the figure (label, value, note, marker)
     stacks on the left and whatever controls the screen has — Funds' and
     Reports' range pills, Day Close's action buttons — sit on the right,
     bottom-aligned with the marker. Every hero is written as the same four
     flat children, so the areas are assigned by class here rather than by a
     container in each view; a screen with no controls (the dashboard) simply
     leaves the second column empty. Moving the pills beside the figure
     instead of under it is what takes ~60px off the band's height. */
  display: grid;
  /* min-content, not 0, for the figure's track: the headline number must
     never be squeezed by a wide filter row — the controls wrap first. */
  grid-template-columns: minmax(min-content, 1fr) auto;
  grid-template-areas:
    "label controls"
    "value controls"
    "note  controls"
    "mark  controls";
  column-gap: 20px;
  align-items: end;
}
.page-hero .hero-label { grid-area: label; }
.page-hero .stat-value { grid-area: value; }
.page-hero .stat-note { grid-area: note; }
.page-hero::after { grid-area: mark; }
.page-hero .hero-controls { grid-area: controls; justify-self: end; align-self: end; align-items: flex-end; }
.page-hero .range-bar { justify-content: flex-end; }
/* Narrower than this the pills have nowhere to go beside the figure (the
   group never wraps internally), so the hero stacks the old way: figure,
   marker, then controls across the full width. */
@media (max-width: 960px) {
  .page-hero {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "label" "value" "note" "mark" "controls";
  }
  .page-hero .hero-controls { justify-self: stretch; align-items: flex-start; margin-top: 12px; }
  .page-hero .range-bar { justify-content: flex-start; }
}
.page-hero .hero-label {
  font-size: var(--fs-micro); font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--on-navy-muted);
}
/* Inherits .stat-value so the dashboard's setTile() fills it unchanged — the
   hero IS the income tile, just worn differently: the Tesla figure, large
   and light, in the RING GOLD on the navy (--on-navy-figure: 6.53:1 at the
   card's lightest corner, 8.39:1 at its darkest). 2.1rem, 1.7rem on a
   phone — the display step's own proportion to the old 2.6 / 2rem. Its
   legality does NOT rest on that size: 6.53:1 clears the 4.5:1 floor for
   type of any size, so shrinking it spent no large-text allowance. It
   stays display-only by design (see --on-navy-figure); the label and the
   note around it stay --on-navy-muted (6.45:1). */
.page-hero .stat-value {
  font-size: 2.1rem; font-weight: 500; letter-spacing: -.02em; line-height: 1.1;
  color: var(--on-navy-figure);
  margin-top: 4px;
}
.page-hero .stat-note { color: var(--on-navy-muted); margin-top: 4px; }
/* The gold marker, once, under the figure — the ring colour on the navy it
   was made for (6.53:1 on the lighter stop, 8.39:1 on the darker). */
.page-hero::after {
  content: ''; display: block; width: 34px; height: 3px; margin-top: 10px;
  background: var(--nav-marker); border-radius: 999px;
}
.page-hero + .stat-grid { position: relative; z-index: 1; margin-top: -34px; }

/* Filters inside the hero. The pill track is the light track it is
   everywhere, sitting on the navy card exactly as a Stripe date picker sits
   on its hero — its navy selected segment reads on the light track, and the
   track reads on the card; only the TEXT that touches the navy changes
   token. The range bar loses its bottom margin here — the hero's own padding
   sets the gap. */
.page-hero .hero-controls { display: flex; flex-direction: column; gap: 8px; }
.page-hero .range-bar { margin-bottom: 0; }
.page-hero .range-custom label,
.page-hero .hero-controls label,
/* .dash-scope-label sets its own (dark) colour, so inheritance from the
   <label> above it never reaches the span — it has to be named here or the
   Reports filter captions disappear into the field. */
.page-hero .dash-scope-label { color: var(--on-navy-muted); }
.page-hero .range-pills { box-shadow: none; border-color: transparent; }
.page-hero .btn-ghost { color: var(--on-navy); }
.page-hero .btn-ghost:hover { color: var(--on-navy); background: var(--on-navy-hover); }

/* The phone override, here AFTER the desktop rules it has to beat on source
   order (section 7's phone block runs before them). The card keeps its
   corners on a phone — it is a card — and the figure steps down a size. */
@media (max-width: 768px) {
  .page-hero { padding: 16px 16px 42px; }
  .page-hero .stat-value { font-size: 1.7rem; }
}

/* The money tiles' meaning moved from a coloured top edge to the ICON: the
   tonal box on the admin's four KPIs is tinted by key, and a tile without
   an icon (Funds', Reports') is simply a card. Hex-free: the tints are the
   badge trios and a 12% mix of the chart's own expense colour. */
.stat-tile[data-tile] { border-top: 1px solid var(--cal-border-subtle); }
.stat-tile[data-tile="card"] .stat-ic    { background: var(--badge-role-bg); color: var(--nbc-blue); }
.stat-tile[data-tile="expense"] .stat-ic { background: color-mix(in srgb, var(--chart-2) 12%, var(--cal-bg)); color: var(--chart-2); }
.stat-tile[data-tile="count"] .stat-ic   { background: var(--cal-bg-subtle); color: var(--cal-text-subtle); }
/* Cash is the GOLD tile: the ring's tint with the gold that may carry a
   mark (--nbc-gold-ink, 5.20:1 on the tint) — the drawer is the gold of the
   badge, the card and the expense keep their own inks. */
.stat-tile[data-tile="cash"] .stat-ic    { background: var(--badge-warn-bg); color: var(--nbc-gold-ink); }

/* ── Reports: two columns ──
   A report is read by scanning, and a page that stacks five full-width panels
   makes the ministry scroll past a chart to reach the table that explains it.
   Two tracks fit both on one screen. The daily line chart alone spans the row:
   thirty-odd points in a half-width track collapse the shape of the month,
   which is the one thing that chart exists to show. Charts re-fit to their
   track on their own (ResizeObserver in charts.js), so no chart code changes.
   minmax(0, …) rather than 1fr: a wide table inside a track must scroll in its
   own .table-wrap, not force the track — and the page — wider. */
.report-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--gap-section);
  align-items: start;
}
.report-grid > .span-2 { grid-column: 1 / -1; }
.report-grid > .panel { min-width: 0; }
@media (max-width: 1100px) {
  .report-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ── Admin dashboard: the top row ──
   The proposals console's layout: the four KPIs as a 2×2 block (5 tracks)
   beside the month's cash chart (7 tracks), stretched so the two columns
   share a floor, and stacked on anything narrower. The row carries the gap
   to the metrics under it, so the view writes no inline margins. */
.dash-top {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--gap-section);
  align-items: stretch;
  margin-bottom: var(--gap-section);
}
.dash-top > .panel { min-width: 0; }
@media (max-width: 1100px) {
  .dash-top { grid-template-columns: minmax(0, 1fr); }
}

/* ── Admin dashboard: today's metrics ──
   Two panels side by side on a wide screen — where the cash came in, and the
   month's top routes — stacking on anything narrower. The distribution
   chart needs the width more than the second panel does, so it takes the
   larger share rather than splitting evenly. */
.dash-metrics {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 4fr);
  gap: var(--gap-section);
  align-items: start;
  margin-bottom: var(--gap-section);
}
.dash-metrics > .panel { min-width: 0; }
/* The log is a table in a panel that is already a card, so it drops the second
   border and shadow — a card inside a card is the thing this theme exists to
   avoid. */
.dash-metrics .table-wrap {
  border: none;
  box-shadow: none;
  border-radius: 0;
  max-height: none;
}
@media (max-width: 1100px) {
  .dash-metrics { grid-template-columns: minmax(0, 1fr); }
}

/* ── Transaction detail (App.FundsOverlay.view) ──
   The read-only face of a row, in the same overlay chrome as the form. The
   amount leads, because that is what anyone opening a ledger row came to
   check; everything else is a definition list under it. */
.detail-loading {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 22px 0;
  font-size: .82rem; color: var(--cal-text-subtle);
}
.detail-amount {
  font-size: 1.7rem; font-weight: 700; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.detail-type { display: flex; align-items: center; gap: 6px; margin-top: 6px; }
.detail-void {
  margin: 12px 0 0;
  padding: 8px 10px;
  background: var(--badge-warn-bg);
  border: 1px solid var(--badge-warn-border);
  color: var(--badge-warn-text);
  font-size: .78rem; line-height: 1.45;
}
.detail-void strong { font-weight: 600; }

.detail-list { margin: 14px 0 0; padding: 0; }
/* Label and value on one line, the value taking the slack — a ledger detail is
   read down the left edge, so the labels have to stay in a column. */
.detail-row {
  display: grid; grid-template-columns: 116px 1fr; gap: 10px;
  padding: 7px 0;
  border-top: 1px solid var(--cal-border-muted);
}
.detail-row:first-child { border-top: none; }
.detail-row dt { margin: 0; font-size: .76rem; color: var(--cal-text-muted); }
.detail-row dd {
  margin: 0; min-width: 0;
  font-size: .82rem; color: var(--cal-text-emphasis);
  overflow-wrap: anywhere;   /* a pasted reference must not widen the overlay */
}
@media (max-width: 480px) {
  .detail-row { grid-template-columns: 1fr; gap: 2px; }
}

/* THE DETAIL IN TWO COLUMNS — what the money WAS on the left, what the RECORD
   is on the right. It halves the scroll on the sheet a clerk opens most, and
   it is the same split End of Day makes, so a reader learns one shape. The
   label column narrows because each half has half the width to give it. */
.detail-cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0 20px; align-items: start; }
.detail-cols .detail-list { margin-top: 0; min-width: 0; }
.detail-cols .detail-row { grid-template-columns: 96px minmax(0, 1fr); }
@media (max-width: 640px) {
  /* One column, and the second list gets its rule back — stacked, the two
     are one list and the break between them must not read as a gap. */
  .detail-cols { grid-template-columns: minmax(0, 1fr); }
  .detail-cols .detail-list + .detail-list .detail-row:first-child {
    border-top: 1px solid var(--cal-border-muted);
  }
}

/* ── End of Day (public/views/dayclose.js) ──
   One list of days and a wizard that adds a report to one. Almost everything
   on it is REUSED: .page-hd (8) with the range pills in its action row,
   .table-wrap + .data-table.sheet (11), .badge (11), .detail-list /
   .detail-loading (above), .modal (10), .empty-state and .skeleton (12).
   What is new is only what a close genuinely needs: four status badges named
   for what they mean, the over/short figure, a wide modal for the frozen
   lines and the small blocks inside it, the two report cards a day opens
   into, and the wizard's stepper, pick-list and choice cards. Every rule is
   scoped to a dc-, wiz-, pick- or choice- class, so Funds, Reports and the
   dashboard are untouched. */

/* The range pills sit in the header's action row here, beside the one solid
   button, rather than in a hero: they filter the list, which IS the page. */
.page-hd-actions .range-bar { margin-bottom: 0; }

/* Status badges, named for what they MEAN so the Funds lock badge and this
   screen agree on a class. Each is an existing trio — no new ink. Open is
   green (trading), Closed is brand blue (locked, official), Reopened is gold
   (attention: open for corrections), Superseded is grey (history). */
.badge-open       { color: var(--badge-ok-text);   background: var(--badge-ok-bg);   border-color: var(--badge-ok-border); }
.badge-closed     { color: var(--badge-role-text); background: var(--badge-role-bg); border-color: var(--badge-role-border); }
.badge-reopened   { color: var(--badge-warn-text); background: var(--badge-warn-bg); border-color: var(--badge-warn-border); }
.badge-superseded { color: var(--cal-text-subtle); background: var(--cal-bg-subtle); border-color: var(--cal-border-subtle); }

/* Over / short — the Z-report's one verdict. Balanced is the only good
   answer. Over is gold ink: a drawer holding more than the ledger is a sale
   nobody recorded, not a windfall. Short is the error red. Two classes
   (0,2,0) outrank `.data-table tbody td` (0,1,2) and `.stat-value` (0,1,0),
   so one rule dresses the tile, the table cell and the modal figure alike. */
.dc-variance.is-even  { color: var(--nbc-green-ink); }
.dc-variance.is-over  { color: var(--nbc-gold-ink); }
.dc-variance.is-short { color: var(--cal-error); }

/* The list: the day is the row's key and reads as one. A shift cell is the
   counted total with the over/short beside it in a smaller size, never
   wrapping — "$412.00 Short $2.00" split across two lines is misread. */
.data-table tbody td.dc-day { white-space: nowrap; font-weight: 600; color: var(--cal-text-emphasis); }
.data-table tbody td.dc-shift-cell { white-space: nowrap; font-variant-numeric: tabular-nums; }
.dc-shift-cell small { margin-left: 5px; font-size: .7rem; font-weight: 500; }

/* The detail holds a day's worth of frozen lines, and 420px is a form, not a
   sheet. The frozen table scrolls inside its own card so the modal body
   (68vh) never holds two scrollbars' worth of page. 880px: the ledger and
   the record beside it came down a size, so the old 920 was margin. */
.modal.modal-wide { max-width: 880px; }
.modal-wide .table-wrap { max-height: 38vh; }
.modal-wide .detail-list { margin-top: 12px; }
.modal-wide .modal-body > .badge { margin-bottom: 10px; }

/* Compact figures inside a modal — the preview before a close, the frozen
   totals in the detail. Stat tiles would be cards inside a card. */
/* THE DAY, IN TWO COLUMNS. The money on the left — what was taken, counted
   and banked — and the workflow on the right: what to do next, who has had
   it, and the record's own facts. It halves the scroll on the sheet that is
   read most, and the two halves answer different questions, so nothing has
   to be read across the fold. One column under 900px, where two would just
   be two narrow ones. */
/* An even split, and each half FILLS its half. The ledger is a table and a
   table is only as wide as its content unless it is told otherwise, so the
   sheet is stretched (.dc-col > .sc-sheet below) rather than the modal — a
   wider overlay only bought dead air between the two columns. */
.dc-two-col { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; align-items: start; }
.dc-col { min-width: 0; }
/* The ledger takes the whole column it was given. Without this the table
   shrinks to its longest label and the rest of the track reads as a margin. */
.dc-col > .sc-sheet { width: 100%; }
@media (max-width: 900px) { .dc-two-col { grid-template-columns: 1fr; gap: 10px; } }

/* ── SOME SURFACE UNDER THE SUMMARY ──
   A close is four blocks of figures on one sheet of white, and white on
   white gave a reader no edges to navigate by. Each half becomes a CARD —
   a faint fill and a hairline, no shadow, square, the way every other
   surface in this app is built — and the receipt inside it gets its own
   structure: a band per section, and the brand's tint on the one figure the
   whole sheet is for. Colour stays where this app puts colour: on the money
   and on the status, never as decoration. */
.dc-two-col > .dc-col {
  background: var(--cal-bg-muted);
  border: 1px solid var(--cal-border-subtle);
  padding: 12px 14px;
}
.dc-two-col > .dc-col > *:first-child { margin-top: 0; }
.dc-two-col > .dc-col > *:last-child { margin-bottom: 0; }
/* The faintest rule disappears on a tint — every divider inside a card goes
   up one step so the lines a receipt is read by are still there. */
.dc-col .sc-sheet td,
.dc-col .detail-row { border-top-color: var(--cal-border-subtle); }

/* THE FOUR BANDS of the receipt — Income, Paid out, The drawer, The bank —
   read as sections rather than as one more row of small caps. */
.dc-col .sc-sheet tr.sc-group td {
  background: var(--cal-bg-subtle);
  padding: 6px 8px; border-top: none;
  color: var(--cal-text-subtle);
}
.dc-col .sc-sheet tr.sc-group + tr td { border-top: none; }

/* THE ANSWER THE SHEET IS FOR. Expected cash is the figure the count is
   judged against, so it is a band in the brand's own audited tint
   (--badge-role-*, 7.06:1) rather than one more bold row. */
.dc-col .sc-sheet tr.sc-total td {
  background: var(--badge-role-bg);
  color: var(--badge-role-text);
  padding: 7px 8px; border-top: 1px solid var(--badge-role-border);
}
/* Full strength (7.06:1): faded to .75 the entry count read 4.08:1 on the band. */
.dc-col .sc-sheet tr.sc-total td small { color: var(--badge-role-text); }
.dc-col .sc-sheet tr.sc-total td.num { padding-left: 8px; }

/* THE DAY SHEET IS AN ORDINARY WIDE MODAL (880px). It carries a ledger beside
   a record and both fit, now that neither is squeezed into a fraction of its
   own column. .dc-modal is still its own class — the header below is its. */

/* THE TOGGLE LIVES IN THE HEADER, beside the title: it chooses which of the
   two panes the body is showing, so it belongs to the overlay and not to the
   pane it would otherwise scroll away with. */
.dc-modal .modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px 16px; flex-wrap: wrap; padding-bottom: 12px; }
.dc-head-main { min-width: 0; }
/* The stage tag, then the toggle: where the day has got to, and what of it
   you are looking at. */
.dc-head-tools { flex: none; display: flex; align-items: center; gap: 10px; }
.dc-head-tools:empty { display: none; }

.dc-figs { display: grid; grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); gap: 8px; margin: 10px 0 12px; }
/* Inside half the width, three across at most — five would set the figures in
   a column of their own width and the labels would wrap to three lines. */
.dc-col .dc-figs { grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); }
/* A WARM SURFACE. The figures a close (and a cash out, and a verify) is
   summed up by are the thing a reader came for, so each box takes the
   pearl beige tint with a beige hairline rather than one more cool grey —
   the app stops being all cool grey exactly where the answer is. Inside,
   --cal-text-muted is re-pointed to --on-beige-muted: the muted step is
   4.34:1 on the tint and fails, this is 5.25:1. Every other ink the box
   holds was measured on the tint in section 1 (ledger in 6.73 · gold ink
   5.02 · error 7.93 · emphasis 15.11). */
.dc-fig {
  padding: 8px 10px; min-width: 0;
  border: 1px solid var(--nbc-beige); border-radius: var(--radius-md);
  background: var(--nbc-beige-tint);
  --cal-text-muted: var(--on-beige-muted);
}
.dc-fig span { display: block; font-size: .66rem; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--cal-text-muted); }
.dc-fig strong { display: block; margin-top: 2px; font-size: .95rem; font-weight: 700; color: var(--cal-text-emphasis); font-variant-numeric: tabular-nums; white-space: nowrap; }
.dc-fig strong.amount-in  { color: var(--ledger-in); }
.dc-fig strong.amount-out { color: var(--ledger-out); }
.dc-fig small { display: block; font-size: .7rem; color: var(--cal-text-muted); }

/* Over / short, live, as the manager types the count. */
.dc-variance-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin: -4px 0 12px; padding: 8px 10px;
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-md);
  font-size: .82rem;
}
.dc-variance-row span { color: var(--cal-text-subtle); }
.dc-variance-row strong { font-weight: 600; font-variant-numeric: tabular-nums; color: var(--cal-text-emphasis); }

/* The one paragraph nobody may skim past — what confirming locks — and the
   reopened / superseded notices in the detail. The same warn surface as a
   voided row's detail: attention, not error. The error variant carries a
   preview that failed to load. */
.dc-notice {
  margin: 0 0 12px; padding: 8px 10px;
  background: var(--badge-warn-bg); border: 1px solid var(--badge-warn-border);
  color: var(--badge-warn-text); font-size: .78rem; line-height: 1.45;
}
.dc-notice strong { font-weight: 600; }
.dc-notice.is-error { background: var(--cal-error-bg); border-color: var(--cal-error-border); color: var(--cal-error); }
/* The workflow's own three notices: sent back by head office, waiting on the
   bank, and done. Warn is the default above, which is what "waiting" wants. */
.dc-notice.is-queried { background: var(--cal-error-bg); border-color: var(--cal-error-border); color: var(--cal-error); }
.dc-notice.is-ok { background: var(--badge-ok-bg); border-color: var(--badge-ok-border); color: var(--badge-ok-text); }

/* The "still to bank" warning over the list, the slip-photo placeholder and
   the "coming soon" badge it wore went with the deposit End of Day no
   longer records; the Deposits page (18G) is where the bank is now. */

/* What to do next, offered where the previous step ended. */
.dc-next-step {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin-top: 10px; padding: 10px 12px;
  background: var(--badge-warn-bg); border: 1px solid var(--badge-warn-border);
  border-radius: var(--radius-md); color: var(--badge-warn-text);
}
.dc-next-step > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.dc-next-step strong { font-size: .82rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.dc-next-step span { font-size: .74rem; line-height: 1.4; }
.dc-next-step .btn { flex: none; }
/* A pointer rather than a warning: where the cash goes after a close is
   somebody else's next step, not something this close is waiting on, so it
   takes the neutral surface instead of the attention gold. */
.dc-next-step.is-quiet { background: var(--cal-bg-subtle); border-color: var(--cal-border-subtle); color: var(--cal-text-subtle); }
.dc-next-step.is-quiet strong { color: var(--cal-text-emphasis); }
@media (max-width: 640px) {
  .dc-next-step { flex-wrap: wrap; }
}

/* WHERE THE DAY'S CASH IS NOW — the custody summary at the head of a
   close's record: one line of states in the order the money moves, each a
   label and a figure, the deposit refs at the end, and the way to the
   Deposits page under it. Ruled off from the record's facts below it, like
   the head-office check on a deposit. */
.dc-custody { margin: 0 0 12px; padding: 0 0 12px; border-bottom: 1px solid var(--cal-border-subtle); }
.dc-custody h3 { margin: 0 0 5px; font-size: .8rem; font-weight: 600; color: var(--cal-text-emphasis); }
.dc-custody-line { display: flex; flex-wrap: wrap; row-gap: 4px; margin: 0; font-size: .8rem; line-height: 1.5; color: var(--cal-text-subtle); }
.dc-custody-part { white-space: nowrap; }
.dc-custody-part + .dc-custody-part::before { content: '·'; margin: 0 7px; color: var(--cal-text-muted); }
.dc-custody-part strong { margin-left: 4px; font-weight: 600; color: var(--cal-text-emphasis); font-variant-numeric: tabular-nums; }
.dc-custody-refs { font-family: var(--font-mono); font-size: .74rem; color: var(--cal-text); }
/* A row the chain could not confirm is tamper evidence, so it is said in
   the error ink rather than folded into the line. */
.dc-custody-flag { display: flex; align-items: flex-start; gap: 6px; margin: 6px 0 0; font-size: .76rem; line-height: 1.45; color: var(--cal-error); }
.dc-custody-flag svg { width: 14px; height: 14px; flex: none; margin-top: 2px; }
.dc-custody .form-hint { margin: 0; }
.dc-custody .btn { margin-top: 8px; }

/* The close's two views. The toggle sits under the stage notice, above both
   panes, so it reads as "which of these am I looking at" rather than as a
   filter on one of them. */
/* The toggle is a header control now, not a thing standing over the sheet, so
   it loses the bottom margin .seg.seg-sm gives a control that stands over
   something. Scoped through the parent because .dc-panes alone is one class
   and .seg.seg-sm is two — the same shape as .page-hd-actions .seg.seg-sm. */
.dc-head-tools .seg.seg-sm { margin: 0; }
.dc-pane[hidden] { display: none; }
/* A frozen line is a button onto the ledger row it copies — the same
   affordance .rs-row gives a run on Route Settings. */
.dc-tx-row { cursor: pointer; }
.dc-tx-row:focus-visible { outline: 2px solid var(--cal-brand); outline-offset: -2px; }

/* THE AUDIT TRAIL on a close: started → counted → banked → confirmed, each
   with who and when. A rail with a dot per event, so it reads as a sequence
   rather than as a table of rows. */
/* The activity log ends the summary: it is the tail of the story, not a
   column beside the money, so it runs full width under both columns — its
   own card, like the two it sits under. */
.dc-pane .dc-timeline {
  margin-top: 16px; padding: 12px 14px;
  background: var(--cal-bg-muted); border: 1px solid var(--cal-border-subtle);
}
.dc-timeline { margin-top: 14px; }
.dc-timeline h3 { margin: 0 0 6px; font-size: .8rem; font-weight: 600; color: var(--cal-text-emphasis); }
.dc-timeline ol { margin: 0; padding: 0 0 0 4px; list-style: none; }
.dc-timeline li { position: relative; display: flex; align-items: flex-start; gap: 8px; padding: 0 0 10px 14px; }
/* The rail, drawn by the item rather than behind the list, so the last event
   ends the line instead of leaving it hanging. */
.dc-timeline li::before {
  content: ''; position: absolute; left: 3px; top: 12px; bottom: 0; width: 1px; background: var(--cal-border);
}
.dc-timeline li:last-child::before { display: none; }
.dc-timeline .dc-tl-dot { position: absolute; left: 0; top: 5px; width: 7px; height: 7px; border-radius: 999px; background: var(--cal-border-emphasis); }
/* Confirmed is the emerald, lit — a 7px dot, 4.23:1 on the muted card. */
.dc-timeline li.dc-tl-confirmed .dc-tl-dot { background: var(--nbc-emerald-rich); }
.dc-timeline li.dc-tl-queried .dc-tl-dot { background: var(--cal-error); }
.dc-timeline li > div { flex: 1; display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.dc-timeline strong { font-size: .8rem; font-weight: 600; color: var(--cal-text-emphasis); }
.dc-tl-who { font-size: .74rem; color: var(--cal-text-subtle); }
.dc-tl-note { font-size: .76rem; color: var(--cal-text); }
.dc-tl-amount { flex: none; font-size: .8rem; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--cal-text); }
/* A link-shaped button inside a notice: it opens another close in place. */
.btn-link { background: none; border: none; padding: 0; font: inherit; color: inherit; text-decoration: underline; cursor: pointer; }
.btn-link:focus-visible { outline: 2px solid var(--cal-border-emphasis); outline-offset: 2px; }

/* Drift: does the ledger still say what this close says? Quiet when
   unchanged, a warn box when not — and never hidden, because a close the
   ledger has moved away from is exactly the one a reader needs warning about. */
.dc-drift { display: flex; align-items: center; gap: 6px; margin: 12px 0 8px; font-size: .78rem; color: var(--cal-text-subtle); }
.dc-drift svg { width: 14px; height: 14px; flex: none; }
.dc-drift.is-changed { padding: 6px 10px; background: var(--badge-warn-bg); border: 1px solid var(--badge-warn-border); color: var(--badge-warn-text); }

/* Earlier versions of the same day, listed under the frozen lines. */
.dc-versions { margin-top: 14px; }
.dc-versions h3 { margin: 0 0 4px; font-size: .8rem; font-weight: 600; color: var(--cal-text-emphasis); }
.dc-versions ul { list-style: none; margin: 0; padding: 0; }
.dc-versions li { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 6px 0; border-top: 1px solid var(--cal-border-muted); font-size: .78rem; color: var(--cal-text-subtle); }
.dc-versions li:first-child { border-top: none; }
.dc-versions li > span { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.dc-versions li strong { color: var(--cal-text-emphasis); font-weight: 600; }

/* ── Shift closes — the drawer confirmed, twice a day (same view) ──
   Twice a day the drawer is confirmed against the ledger for the shift's
   window: what the system has on the left, what the clerk has on the right.
   A day opened from the list shows its two reports as cards; the wizard's
   Confirm step and the Correct count modal show the CONFIRM FORM
   (.shift-form, .sc-*). Everything else is reused — .badge-open /
   .badge-closed / .badge-superseded, .dc-variance for the over/short, the
   .dc-figs figures, .dc-notice, .modal-wide, .form-grid, the deposit group.
   No new ink: the card's top edge takes the palette tokens (the lit emerald
   open, blue recorded), and every figure is the palette the tiles already
   wear. */

/* The two report cards inside the day's modal. Inside a modal a card takes
   the muted fill and a hairline — no shadow, no radius — so it reads as a
   panel of the sheet rather than a tile floating on it; the 3px meaning
   edge says at a glance which shift is still open. */
.dc-report-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 10px; margin: 0 0 12px; }
.dc-report-card {
  display: flex; flex-direction: column; gap: 8px; min-width: 0;
  padding: 12px 14px;
  background: var(--cal-bg-muted);
  border: 1px solid var(--cal-border-subtle);
  border-top: 3px solid var(--cal-border-subtle);
}
.dc-report-card[data-state="open"]   { border-top-color: var(--nbc-emerald-rich); }   /* a 3px bar, 4.42:1 on white */
.dc-report-card[data-state="closed"] { border-top-color: var(--chart-1); }
.dc-report-card-hd { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.dc-report-card-hd h3 { margin: 0; font-size: .88rem; font-weight: 600; color: var(--cal-text-emphasis); }
.dc-report-card-hd p { margin: 2px 0 0; font-size: .74rem; color: var(--cal-text-muted); font-variant-numeric: tabular-nums; }
.dc-report-card-hd > span { display: inline-flex; align-items: center; gap: 5px; flex: none; }
.dc-report-card > .form-hint { margin: 0; font-size: .8rem; line-height: 1.5; color: var(--cal-text-subtle); }

/* The five figures — expected, cash on hand, over/short, deposited, the
   difference — as a row, the same type as .dc-fig but without the boxes: a
   box inside a card inside a modal is the thing this theme exists to avoid.
   Five do not fit a card at one size, so the row folds to three and two
   where it must. The over/short and difference strongs carry .dc-variance,
   whose two-class rule outranks this one and paints them. */
.shift-figs { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 8px; }
.shift-fig { min-width: 0; }
.shift-fig span { display: block; font-size: .64rem; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--cal-text-muted); }
.shift-fig strong { display: block; margin-top: 2px; font-size: .95rem; font-weight: 700; color: var(--cal-text-emphasis); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Who counted, when, and what was handed over — read down the left edge. */
.shift-meta { list-style: none; margin: 0; padding: 0; font-size: .76rem; }
.shift-meta li { display: flex; justify-content: space-between; gap: 10px; padding: 3px 0; border-top: 1px solid var(--cal-border-muted); }
.shift-meta li:first-child { border-top: none; }
.shift-meta li span { color: var(--cal-text-muted); flex: none; }
.shift-meta li strong { font-weight: 500; color: var(--cal-text-emphasis); text-align: right; min-width: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
/* The buttons sit on the card's floor whatever the card above them holds, so
   a row of cards keeps its actions on one line. */
.shift-actions { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: auto; }
/* The folded denomination count on a card: the same pressable summary as
   the Reports breakdowns. */
.shift-denoms summary.form-hint { padding: 4px 0; font-weight: 500; color: var(--cal-text-subtle); cursor: pointer; user-select: none; }
.shift-denoms summary.form-hint:hover { color: var(--cal-text-emphasis); }
.shift-denoms .denom-grid { margin: 6px 0 0; }

/* The day close under the cards, ruled off from them; its own heading and
   the closed-by line, then the same frozen detail the print reads from. */
.dc-day-close { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--cal-border-subtle); }
.dc-day-close > h3 { margin: 0 0 2px; font-size: .8rem; font-weight: 600; color: var(--cal-text-emphasis); }
.dc-day-close > .form-hint { margin: 0 0 8px; }

/* The DENOMINATION GRID — the older paper form's centre, kept for the
   records that carry one: a card folds the count open, read-only.
   Denomination / number / value, bills then coins as group rows, a
   ruled-off total. Every figure is tabular so the value column reads as a
   column. Nothing writes one any more, so there is no input dress here. */
.denom-grid { width: 100%; border-collapse: collapse; margin: 0 0 12px; font-variant-numeric: tabular-nums; }
.denom-grid th {
  text-align: left; padding: 6px 8px;
  font-size: .66rem; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  color: var(--cal-text-muted); background: var(--cal-bg-subtle);
  border-bottom: 1px solid var(--cal-border-subtle);
  white-space: nowrap;
}
.denom-grid td { padding: 4px 8px; font-size: .82rem; color: var(--cal-text); border-bottom: 1px solid var(--cal-border-muted); vertical-align: middle; }
.denom-grid .num { text-align: right; }
.denom-grid td.denom-name { font-weight: 600; color: var(--cal-text-emphasis); white-space: nowrap; }
/* The number cell never wraps — a count of "1 000" split across two lines
   is misread. */
.denom-grid td.denom-n { white-space: nowrap; }
.denom-grid td.denom-value { white-space: nowrap; }
/* Bills / coins: a caption row in the table head's dress, so the two groups
   read as two groups without a second table. */
.denom-grid tr.denom-group td {
  padding: 6px 8px 3px;
  font-size: .64rem; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  color: var(--cal-text-muted); background: var(--cal-bg-subtle);
  border-bottom-color: var(--cal-border-subtle);
}
/* The total is ruled off from the lines it sums — the same rule the Reports
   totals row follows — and is the figure that must match the server's. */
.denom-grid tr.denom-total td {
  border-top: 1px solid var(--cal-border); border-bottom: none;
  background: var(--cal-bg-subtle);
  font-weight: 700; color: var(--cal-text-emphasis);
}
/* THE CONFIRM FORM. Two columns: what the system has on the left, read
   only; what the clerk has on the right, the only inputs. The left is the
   narrower track — a short sheet of category lines — and the right takes
   the fields, so the whole form fits one screen of the wide modal and of
   the wizard's card instead of a scroll the Save button hides at the end
   of. Stacks on anything narrower than the two columns need. */
.shift-form { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); gap: 4px 20px; align-items: start; }
.shift-form > div { min-width: 0; }
.shift-form .dc-notice { margin-bottom: 10px; }
@media (max-width: 760px) {
  .shift-form { grid-template-columns: minmax(0, 1fr); }
}
/* Each column is headed the way the confirm sheet heads its two. */
.sc-col-hd { margin: 0 0 8px; font-size: .8rem; font-weight: 600; color: var(--cal-text-emphasis); }

/* The window as one line — "4:30 AM – 1:15 PM · Change" — with the two
   time inputs folded under it. The times are right nine days in ten, so
   they are stated, not asked; Change opens them for the tenth. */
.sc-window { display: flex; align-items: baseline; gap: 8px; margin: 0 0 8px; font-size: .82rem; font-variant-numeric: tabular-nums; color: var(--cal-text-emphasis); }
.sc-window .btn-link { font-size: .76rem; color: var(--cal-text-subtle); }
.sc-window .btn-link:hover { color: var(--cal-text-emphasis); }
.sc-window-fields { margin: 0 0 6px; }

/* THE SYSTEM SHEET: what the drawer should hold, read down like a till
   slip. Category lines in the muted ink, each subtotal ruled off above and
   set in the emphasis ink, and the expected cash as the one bold line on
   the sheet — the figure the clerk's count is checked against — with the
   entry count in small type under its label. Tabular figures, so the
   amount column reads as a column; the same hairlines as .shift-meta. */
.sc-sheet { width: 100%; border-collapse: collapse; margin: 0 0 10px; font-size: .78rem; font-variant-numeric: tabular-nums; }
.sc-sheet td { padding: 5px 0; border-top: 1px solid var(--cal-border-muted); color: var(--cal-text-subtle); vertical-align: baseline; }
.sc-sheet tr:first-child td { border-top: none; }
.sc-sheet td.num { text-align: right; white-space: nowrap; color: var(--cal-text-emphasis); font-weight: 500; padding-left: 16px; }
/* The label may wrap; the figure beside it never should. */
.sc-sheet td:first-child { overflow-wrap: anywhere; }
.sc-sheet tr.sc-subtotal td { padding-top: 5px; padding-bottom: 7px; border-top-color: var(--cal-border-subtle); color: var(--cal-text-emphasis); font-weight: 600; }
.sc-sheet tr.sc-total td { padding-top: 7px; border-top: 1px solid var(--cal-border); color: var(--cal-text-emphasis); font-weight: 700; font-size: .88rem; }
.sc-sheet tr.sc-total td small { display: block; font-size: .68rem; font-weight: 400; color: var(--cal-text-muted); }
/* Any row may carry its own footnote now — the carried figure needs to say
   which days it has been waiting since. */
.sc-sheet td small { display: block; margin-top: 1px; font-size: .68rem; font-weight: 400; line-height: 1.35; color: var(--cal-text-muted); }
/* The money that is only there because it has not been banked: it is not
   this day's takings, and the receipt should not let it read as though it
   were. */
.sc-sheet tr.sc-carried td { color: var(--badge-warn-text); }
/* The footnote at full strength: faded to .85 the gold ink fell to 4.18:1 on
   white and 4.02:1 on the muted card — under the floor for .68rem type. */
.sc-sheet tr.sc-carried td small { color: var(--badge-warn-text); }
.sc-sheet tr.sc-carried + tr td { border-top-color: var(--cal-border-subtle); }
/* The window the figures were collected in, above the receipt. */
.sc-window-note { margin: -4px 0 8px; font-size: .74rem; line-height: 1.45; color: var(--cal-text-subtle); }

/* THE LEDGER INSIDE THE RECEIPT. The money is grouped the way the taxonomy
   groups it — income by category, then what was paid out — because "expected
   cash" is an answer nobody checks and "Fares $220, Parcel $60, less $40 of
   fuel" is one they can. A heading row, its categories indented under it, and
   the subtotal ruled off. */
.sc-sheet tr.sc-group td {
  padding-top: 8px; border-top: 1px solid var(--cal-border-subtle);
  font-size: .64rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--cal-text-muted);
}
.sc-sheet tr.sc-cat td { border-top: none; padding-left: 8px; }
.sc-sheet tr.sc-cat td.num { padding-left: 0; }
.sc-sheet tr.sc-cat + tr.sc-subtotal td { border-top-color: var(--cal-border-muted); }
/* A deduction that is income but not cash: it reads as money leaving. */
.sc-sheet tr.sc-deduct td { color: var(--cal-text-subtle); }
.sc-empty { margin: 0 0 8px; }

/* Cash on hand with the over/short live beside it, on the same line: the
   input takes the room, the verdict sits at the end of it and never wraps.
   The strong carries .dc-variance, which paints it. */
.sc-counted { display: flex; align-items: center; gap: 10px; }
.sc-counted .inp { flex: 1 1 auto; min-width: 0; }
.sc-counted strong { flex: none; font-size: .82rem; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--cal-text-emphasis); }

/* The shift cash-outs frozen on a day close, in the detail — the same
   heading as the versions list under it. */

/* ── The confirm sheet and the bank deposit (same view) ──
   .dc-sheet USED TO BE A TWO-COLUMN GRID — the old confirm sheet was a div
   with the figures in one half and the inputs in the other. It is a TABLE
   now (the close as a receipt), and the grid was still being applied to it:
   the rows were jammed into a 5fr track and the 6fr beside them rendered as
   a band of white space down the middle of the overlay. The layout the close
   needs is .dc-two-col above; the class stays only as the sheet's name. */

/* The by-category CARDS are gone. The close now shows the same ledger the
   count step shows (.sc-sheet / .dc-sheet above), so a reader checks one
   shape rather than learning two. */

/* Reopen, over the detail's facts; Count the drawer on a day not counted
   yet. A hint beside the buttons reads as their caption. */
.dc-detail-actions { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin: -4px 0 12px; }
.dc-detail-actions > .form-hint { margin: 0; flex: 1 1 260px; }

/* ── Start new close — the wizard PAGE (same view) ──
   Four steps on a page of its own (/day-close?new=1): the day, the shift,
   the count, done. The page keeps the app shell and the sticky .page-hd (8)
   — title, a subtitle that grows with the choices, one outline Cancel — and
   under it a centred 880px column: the STEPPER, then one white card holding
   the step, with the buttons in a bar along the card's foot. The day
   pick-list and the shift choice cards are radio groups drawn as rows and
   as cards — aria-checked and aria-disabled carry the state, and the CSS
   keys on those so the markup cannot say one thing and the paint another.
   Nothing here reaches the list or the day's modal: every rule is scoped to
   .wiz-page, .stepper or a wiz- class. */
.wiz-col { max-width: 880px; margin: 0 auto; }

/* Off-screen but read aloud: the "Step 2 of 4" the stepper's colours say to
   a sighted reader. The standard clip recipe, so no layout is disturbed. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* THE STEPPER. Four nodes across the full width, joined by a 2px rule. Each
   node draws the connector that leads INTO it (from the previous node's
   centre to its own), so painting the rule green up to the current step is
   a matter of that step's own state and needs no sibling selector. The node
   is a CIRCLE, like the avatar disc — a rounded rectangle would read as a
   badge — and the label sits under it with, once settled, a caption saying
   what the step decided. Upcoming: hairline ring, muted number. Current:
   solid brand, white number, bold label. Done: the logo's emerald with a
   white check (7.04:1). The connector is a STROKE, so it takes the lit
   emerald (--nbc-emerald-rich, 4.42:1 on the white card); the node carries
   a white glyph, so it takes the deeper one — white on the lit emerald
   would be 4.42:1, and that ink never carries white. */
.stepper { display: flex; list-style: none; margin: 0 0 14px; padding: 0; }
.stepper-step {
  position: relative; flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  padding: 0 4px; text-align: center;
}
/* top: the node's centre (28 / 2) less half the 2px rule. */
.stepper-step::before {
  content: ''; position: absolute; top: 13px; right: 50%; left: -50%; height: 2px;
  background: var(--cal-border-subtle);
}
.stepper-step:first-child::before { display: none; }
.stepper-step.is-done::before,
.stepper-step[aria-current="step"]::before { background: var(--nbc-emerald-rich); }
.stepper-node {
  position: relative; z-index: 1;
  display: grid; place-items: center; width: 28px; height: 28px;
  border: 1px solid var(--cal-border); border-radius: 50%;
  background: var(--cal-bg); color: var(--cal-text-muted);
  font-size: .74rem; font-weight: 600; font-variant-numeric: tabular-nums;
}
.stepper-node svg { width: 14px; height: 14px; }
.stepper-label { font-size: .76rem; font-weight: 500; color: var(--cal-text-muted); line-height: 1.2; }
.stepper-caption { margin-top: -3px; font-size: .68rem; color: var(--cal-text-muted); font-variant-numeric: tabular-nums; }
.stepper-step[aria-current="step"] .stepper-node { color: var(--cal-brand-text); background: var(--cal-brand); border-color: var(--cal-brand); }
.stepper-step[aria-current="step"] .stepper-label { font-weight: 600; color: var(--cal-text-emphasis); }
.stepper-step.is-done .stepper-node { color: var(--cal-brand-text); background: var(--nbc-emerald); border-color: var(--nbc-emerald); }
.stepper-step.is-done .stepper-label { color: var(--cal-text-subtle); }

/* THE CARD. White on the canvas with the panel's edge and the resting
   shadow; the step's title and body inside, and the bar of buttons along
   its foot, ruled off and on the muted fill so it reads as a bar and not as
   the last paragraph. Back on the left, the primary on the right; the
   right-hand group keeps its place when Back is hidden. */
.wiz-card {
  background: var(--cal-bg);
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-xl);
  box-shadow: var(--shadow-card);
}
.wiz-card-body { padding: 16px 20px 6px; }
.wiz-title { margin: 0 0 10px; font-size: var(--fs-h2); font-weight: 600; letter-spacing: -.005em; color: var(--cal-text-emphasis); }
.wiz-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap;
  padding: 10px 20px;
  border-top: 1px solid var(--cal-border-subtle);
  border-radius: 0 0 var(--radius-xl) var(--radius-xl);
  background: var(--cal-bg-muted);
}
.wiz-foot-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-left: auto; }
.wiz-foot .btn { min-width: 84px; }

/* Room the modal never had: the form's two columns keep their shape with a
   little more air between them; the day list takes the card's corner so the
   two read as one object. */
.wiz-page .shift-form { gap: 4px 24px; }
.wiz-page .pick-list { border-radius: var(--radius-md); overflow: hidden; }

/* DONE. A centred confirmation: the emerald check, "Saved", the sentence,
   the five figures the signatures vouch for (on the warm .dc-fig boxes),
   one hint. The buttons are the foot's. The check disc is a circle for the
   same reason the nodes are, and the logo's own emerald because it carries
   a white glyph (7.04:1). */
.wiz-done { display: flex; flex-direction: column; align-items: center; gap: 5px; padding: 10px 0 4px; text-align: center; }
.wiz-done-ic {
  display: grid; place-items: center; width: 40px; height: 40px; margin-bottom: 4px;
  border-radius: 50%; background: var(--nbc-emerald); color: var(--cal-brand-text);
}
.wiz-done-ic svg { width: 20px; height: 20px; }
.wiz-done h2 { margin: 0; font-size: 1.1rem; font-weight: 700; color: var(--cal-text-emphasis); }
.wiz-done > p { margin: 0; max-width: 52ch; font-size: .82rem; color: var(--cal-text-subtle); line-height: 1.5; }
.wiz-done .dc-figs.wiz-done-figs { width: 100%; max-width: 720px; margin: 10px 0 4px; grid-template-columns: repeat(auto-fit, minmax(116px, 1fr)); }
.wiz-done .dc-fig { text-align: center; }
.wiz-done > .form-hint { margin: 4px 0 0; max-width: 52ch; }

@media (max-width: 640px) {
  /* Four labelled nodes do not fit a phone; the current one keeps its name,
     the rest keep their rings, the rule stays. */
  .stepper-node { width: 24px; height: 24px; font-size: .7rem; }
  .stepper-node svg { width: 13px; height: 13px; }
  .stepper-step::before { top: 11px; }
  .stepper-label, .stepper-caption { display: none; }
  .stepper-step[aria-current="step"] .stepper-label { display: block; }
  .wiz-card-body { padding: 14px 14px 6px; }
  .wiz-foot { padding: 10px 14px; }
  .wiz-foot .btn { min-width: 0; }
}

/* The day pick-list: one row per day, the day on the left and where it
   stands on the right. The chosen row takes a 3px brand rule down its left
   edge — the nav's active marker, not a fill, so the row's text keeps its
   contrast. A row that cannot be picked is dimmed and keeps its reason in
   its title. */
.pick-list { display: flex; flex-direction: column; margin: 0 0 10px; border: 1px solid var(--cal-border-subtle); }
.pick-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; padding: 8px 10px;
  border: none; border-top: 1px solid var(--cal-border-muted);
  background: var(--cal-bg); color: var(--cal-text);
  font: inherit; font-size: .82rem; text-align: left; cursor: pointer;
}
.pick-row:first-child { border-top: none; }
.pick-row:hover { background: var(--cal-bg-muted); }
.pick-row:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--cal-border-emphasis); }
.pick-row[aria-checked="true"] { box-shadow: inset 3px 0 0 var(--cal-brand); color: var(--cal-text-emphasis); }
.pick-row[aria-checked="true"] .pick-main { font-weight: 600; }
.pick-row[aria-disabled="true"] { color: var(--cal-text-muted); background: var(--cal-bg-subtle); cursor: not-allowed; }
.pick-row .skeleton { width: 60%; }
.pick-main { display: inline-flex; align-items: center; gap: 8px; min-width: 0; font-weight: 500; }
.pick-main small { font-size: .64rem; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--cal-text-muted); }
.pick-state { display: inline-flex; align-items: center; gap: 5px; flex: none; font-size: .76rem; color: var(--cal-text-subtle); text-align: right; }
.pick-state small { font-size: .7rem; color: var(--cal-text-muted); }
/* "Another day…" and the date input it reveals share a line. */
.wiz-other { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.wiz-other .form-row { margin: 0; grid-auto-flow: column; align-items: center; }
.wiz-other .form-row .inp { width: auto; height: var(--ctl-h-row); }

/* The shift choice cards: the name, the time range under it, and whether
   it is already cashed out. Sized for the page, not a modal — two 240px
   cards side by side with room to read. The chosen card takes the brand
   edge; one already reported is dimmed and keeps its reason in its title. */
.choice-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; margin: 0 0 12px; }
/* A card-button, the wizard's tile: hairline and the control shadow at
   rest, a lift under a hand, the brand edge when chosen. */
.choice-card {
  display: flex; flex-direction: column; gap: 3px; min-width: 0;
  padding: 12px 14px;
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-lg);
  background: var(--cal-bg); color: var(--cal-text);
  box-shadow: var(--shadow-sm);
  font: inherit; text-align: left; cursor: pointer;
  transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.choice-card strong { font-size: .92rem; font-weight: 600; color: var(--cal-text-emphasis); }
.choice-card span { font-size: .8rem; color: var(--cal-text-subtle); font-variant-numeric: tabular-nums; }
.choice-card small { margin-top: 4px; font-size: .74rem; color: var(--cal-text-muted); font-variant-numeric: tabular-nums; }
.choice-card:hover { border-color: var(--cal-border); transform: translateY(-1px); box-shadow: var(--shadow-lift); }
.choice-card:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--cal-border-emphasis); }
.choice-card[aria-checked="true"] { border-color: var(--cal-brand); box-shadow: inset 0 0 0 1px var(--cal-brand), var(--shadow-sm); }
.choice-card[aria-disabled="true"] { background: var(--cal-bg-subtle); color: var(--cal-text-muted); border-color: var(--cal-border-subtle); cursor: not-allowed; transform: none; box-shadow: none; }
.choice-card[aria-disabled="true"] strong { color: var(--cal-text-muted); }

/* ============================================================
   17. CHARTS
   App.Chart.line(mountEl, spec) / App.Chart.bar(mountEl, spec) —
   public/components/charts.js. Hand-written inline SVG: this app has no
   build step and runs on a terminal machine that may be offline, so there is
   no chart library and no CDN. A chart that needs the network to draw is a
   chart that does not draw. A COMPONENT, not a view — nothing here may
   assume the Reports page, exactly as section 15 may not assume the
   dashboard.

   ── WHO PAINTS WHAT. Read this before adding a rule. ──
   charts.js paints the SVG ITSELF: every mark, axis, gridline, tick, legend
   key and tooltip is an inline style on the element it draws, and every one
   of those styles is a var(--token) reference — never a colour literal. It
   does that for the same reason calendar.js injects a per-event colour
   through --cal-event-bg: paint inside a chart is per-series and per-datum,
   and a chart must still draw correctly if this section is ever trimmed or
   arrives late, rather than coming out a black-on-white skeleton.

   So this section dresses the HTML CHROME AROUND the svg and nothing else.
   DO NOT add mark rules here — a stroke on .chart-line, a fill on
   .chart-bar, gridlines, ticks, a tooltip. They would be dead CSS at best,
   and those two names are already taken: .chart-line and .chart-bar are the
   ROOT's kind modifier, on a <div>, not the marks inside it. A fill or an
   opacity hung on either would land on the whole chart.

   THE CLASSES charts.js EMITS — all of them:
     .chart.chart-line | .chart.chart-bar   the root <div>, by kind
     .chart.is-loading                      a refetch in flight
     .chart-title                           the optional <h3> above the canvas
     .chart-canvas                          the box the <svg> is drawn into
     .chart-svg                             the chart itself (role=img, tabindex=0)
     .chart-empty                           the words shown when there is nothing to draw
   The tokens it reads are --chart-1/2/3 from section 1, plus --cal-text,
   --cal-text-emphasis, --cal-text-muted, --cal-border-subtle, --cal-border
   and --cal-bg. That list IS the contract, and it is what keeps a repalette
   a change to section 1 and nothing else.
   ============================================================ */
.chart {
  display: flex; flex-direction: column; gap: 8px;
  /* A grid or flex item's default min-width is min-content, and the
     component sizes every scale from root.clientWidth. Without this the box
     refuses to shrink, the measured width never comes back down, and the
     chart overflows its card instead of re-rendering narrower. */
  min-width: 0;
}

/* No padding and no border here, deliberately: clientWidth is what the
   component measures and it INCLUDES padding, so any padding on the chart's
   own boxes draws the svg wider than the space it has to sit in. Padding
   belongs to the card outside (.panel), which is not measured. */
.chart-canvas { position: relative; min-width: 0; }

.chart-title { margin: 0; font-size: .88rem; font-weight: 600; color: var(--cal-text-emphasis); }

/* The svg is drawn at the measured pixel width, so it is already exactly as
   wide as its box. max-width is the guard for the frame between a resize and
   the redraw — it clips nothing and it never rescales the type. */
.chart-svg { display: block; max-width: 100%; }
/* The svg is focusable (tabindex=0) because its aria-label is the written
   summary of the series — so it owes a visible focus ring like any other
   tab stop. */
.chart-svg:focus-visible { outline: 2px solid var(--cal-border-emphasis); outline-offset: 2px; border-radius: var(--radius-md); }

/* Nothing to draw says so IN WORDS — a blank plot box reads as broken, and
   the component is handed the caller's sentence rather than "no data",
   because which filters produced nothing is the whole question. It holds
   --chart-height so a card is the same size empty as drawn and the page
   below it does not jump when the numbers arrive. */
.chart-empty {
  display: flex; align-items: center; justify-content: center;
  min-height: var(--chart-height);
  padding: 16px 20px;
  text-align: center; font-size: .8rem; color: var(--cal-text-subtle);
}

/* A refetch KEEPS THE FRAME: the previous render stays on screen, dimmed,
   instead of collapsing to a skeleton and bouncing the page every time a
   filter moves. The component only toggles the class and sets aria-busy —
   the dimming is ours. The title is not stale, so it stays at full strength
   and the reader can still see what they are looking at. */
.chart.is-loading .chart-canvas { opacity: .45; transition: opacity .15s ease; }
@media (prefers-reduced-motion: reduce) {
  .chart.is-loading .chart-canvas { transition: none; }
}

/* Shorter plot on a phone, set on the COMPONENT the way section 15 narrows
   its own gutter — the token stays a token, it just resolves smaller here. */
@media (max-width: 640px) {
  .chart { --chart-height: 200px; }
  .chart-title { font-size: .85rem; }
}

/* ============================================================
   18. REPORTS — the admin overview
   public/views/reports.js. The global twin of Funds: every location at
   once, over a range, sliced by who recorded it, which route and which bus.
   ADMIN ONLY. Funds stays exactly as it is, and every rule below is scoped
   so it cannot reach it.

   Almost the whole screen is REUSED rather than restyled, which is why this
   section is short: .page-hd (8) · .panel (8) · .range-bar / .range-pills
   (11) · .table-wrap + .data-table.sheet (11) · .empty-state and .skeleton
   (12) · .stat-grid / .stat-tile (13) · .stat-value.net-pos|.net-neg and
   .amount-in|.amount-out (16) · .chart / .chart-canvas (17). What is new is
   only what the second filter row and the totals row genuinely need.
   ============================================================ */

/* ── The filter row ──
   Reports stacks a SECOND .range-bar under the range pills, holding the four
   slices — Location, Recorded by, Route, Bus — plus a Clear filters button.
   Two rows, one block: they scope everything below them together, so the
   gap between them is closed rather than left at a full 14px, which would
   read as two unrelated toolbars.
   Adjacency is the whole selector, so Funds — which has exactly one
   .range-bar — is untouched. */
.range-bar + .range-bar { margin-top: -6px; }

/* Each filter is a label.dash-scope wrapping the caption and the select,
   the same control the dashboard puts in its header. In a HEADER it reads
   as one line; four of them on one line is 1000px of row, so inside a
   filter row the caption sits ABOVE its select instead. Scoped to
   .range-bar, so the dashboard's and Funds' header selectors keep the
   inline form they were built for. */
.range-bar .dash-scope { flex-direction: column; align-items: flex-start; gap: 4px; min-width: 0; }
/* --ctl-h-row to match the pill track — the pills and the selects have to
   sit on one grid. 140px is about "Belize City Terminal" at this size
   without truncation; the cap stops one long route name from stretching
   the field past its neighbours. */
.range-bar .dash-scope-select {
  height: var(--ctl-h-row);
  min-width: 140px; max-width: 220px;
  font-size: .78rem;
}
/* Clear filters is a footer control on a row of stacked labels: centred it
   would float against the captions instead of lining up with the selects. */
.range-bar .btn { align-self: flex-end; }

@media (max-width: 640px) {
  /* Full width rather than four unreadable stubs — a select you cannot read
     the location out of is worse than a taller row. */
  .range-bar .dash-scope { flex: 1 1 100%; }
  .range-bar .dash-scope-select { min-width: 0; max-width: none; width: 100%; }
  /* On its own wrapped line it is a full-width target, not a 90px stub
     stranded under four full-width selects. */
  .range-bar .btn { align-self: stretch; width: 100%; }
}

/* ── The totals row ──
   Every figures table on this page ends with a Total row, and it is the
   figure the ministry reads first: it must reconcile with the tiles above
   and with /api/transactions/summary to the cent, so it is ruled off from
   the rows it sums instead of sitting in the banding as one more line.
   The row carries no class of its own — it is identified by being the last
   row that contains <strong> — and it is scoped to .panel, which is how
   Reports lays every table out and how Funds lays none of them out. A
   browser without :has() simply keeps the plain banded row: the figure is
   still bold, still correct, still last. */
.panel .data-table.sheet tbody tr:last-child:has(strong) td {
  border-top: 1px solid var(--cal-border);
  background: var(--cal-bg-subtle);
}
/* Only the LABEL takes emphasis ink. Colour on the whole row would beat
   .data-table tbody td.amount-in|.amount-out on specificity and repaint the
   total's Net — the one figure on this page that is allowed to say, in
   green or in red, which way the company went. Ratios hold on the tint:
   green 6.99:1, red 8.24:1, body 9.69:1. */
.panel .data-table.sheet tbody tr:last-child:has(strong) td:first-child { color: var(--cal-text-emphasis); }
/* The banding's hover would otherwise lift the total as if it were one more
   selectable row; it is a sum, not a record. */
.panel .data-table.sheet tbody tr:last-child:has(strong):hover td { background: var(--cal-bg-subtle); }

/* ── Folded breakdowns ──
   The by-route, by-bus and by-staff tables ride inside <details> so the page
   opens on the charts and the money, not on five tables. The summary is the
   only thing that has to look pressable — without this it reads as one more
   line of hint text that happens to have a triangle beside it. */
.panel summary.form-hint {
  padding: 5px 0;
  font-weight: 500;
  color: var(--cal-text-subtle);
  user-select: none;
}
.panel summary.form-hint:hover { color: var(--cal-text-emphasis); }
.panel summary.form-hint:focus-visible {
  outline: 2px solid var(--cal-border-emphasis);
  outline-offset: 2px;
  border-radius: var(--radius-md);
}

/* ============================================================
   18B. CASH OUTS — the counter's Cash out (public/views/cashout.js), the
   collector's To verify queue (public/views/handins.js), the verify window
   (public/components/handin-confirm.js, App.CashOutVerify) and the shared
   sheet and print kit (public/components/cashout-print.js).
   Two kinds of cash out, one queue: a conductor's BAG of fares and a
   counter person's DRAWER, both waiting for the terminal's two verifiers,
   the Cash Collector and the Regional Manager.
   Almost everything on these screens is REUSED: the range pills, .seg and
   the scope select (5, 8, 11), .data-table.sheet and .badge (11),
   .detail-list, the End of Day count's .shift-form / .sc-* sheet, the
   .dc-figs figures and the .dc-variance inks, .wiz-done, the read-only
   .denom-grid and the .shift-denoms fold. The conductor's own flow is
   public/components/handin-flow.js, styled by /conductor.css (the .cx-
   system) — nothing of it lives here. What is here is what a cash out adds
   and nothing else: the Waiting badge, which kind a row is, the lines the
   drawer sheet lists as NOT in the drawer, the folded list of every entry,
   the optional denomination grid, the verify block's rule, and the page's
   own spacing. Scoped to co- classes (and hi- for the verify block, whose
   name predates the rename). Tokens only.
   ============================================================ */

/* Waiting: the gold trio, the same "attention" ink as a reopened day —
   there is cash nobody has counted yet. Verified is .badge-ok, already
   defined. */
.badge-waiting { color: var(--badge-warn-text); background: var(--badge-warn-bg); border-color: var(--badge-warn-border); }

/* A toggle that sits in the header's action row beside the pills (Waiting
   | All on To verify) loses the bottom margin meant for a chart panel. */
.page-hd-actions .seg.seg-sm { margin: 0; }

/* Which kind of cash out a row is. A bag of fares takes the brand tint —
   it becomes a Fares row on the ledger when it is verified; a counter
   drawer takes the neutral fill — its sales are already on the ledger and
   verifying it writes nothing. Text on each fill was measured with the
   badge trios in section 1. */
.co-kind.is-bag    { color: var(--badge-role-text); background: var(--badge-role-bg); border-color: var(--badge-role-border); }
.co-kind.is-drawer { color: var(--cal-text-subtle); background: var(--cal-bg-subtle); border-color: var(--cal-border-subtle); }

/* A bag close to the 31 days a Fares row may be backdated: verify it now or
   the ledger will refuse the row. Gold ink on the gold tint (5.20:1),
   smaller than a status badge so it reads as a rider on one. Complete on
   its own, so it holds with or without .badge beside it. */
.co-age {
  display: inline-flex; align-items: center;
  margin-left: 5px; padding: 1px 5px;
  border: 1px solid var(--badge-warn-border); border-radius: var(--radius-md);
  background: var(--badge-warn-bg); color: var(--nbc-gold-ink);
  font-size: .64rem; font-weight: 600; line-height: 1.5; white-space: nowrap;
}

/* A second line inside a table cell — the role under a person's name. */
.data-table tbody td > small { display: block; margin-top: 1px; font-size: .7rem; font-weight: 400; color: var(--cal-text-muted); }

/* A cash-out row opens the verify window: pressable by mouse and by
   keyboard (tabindex=0, Enter), with the focus token as its outline. */
.data-table tbody tr[data-key] { cursor: pointer; }
.data-table tbody tr[data-key]:focus-visible { outline: 2px solid var(--cal-border-emphasis); outline-offset: -2px; }

/* THE DRAWER SHEET's own lines (App.CashOutPrint.sheetHTML), on top of the
   .sc-sheet it shares with End of Day. Under "Not in your drawer" sits what
   the cash out takes that the collector should NOT expect — card sales,
   payouts made by card, entries voided as keyed in error — each in the
   muted ink and indented like a category line, so the eye reads it as
   accounted for elsewhere, not as money missing. */
.sc-sheet tr.co-skip td { border-top: none; padding-left: 10px; color: var(--cal-text-subtle); }
.sc-sheet tr.co-skip td.num { padding-left: 0; font-weight: 400; color: var(--cal-text-subtle); }

/* A sentence under the sheet: entries recorded after the last cash out for
   an earlier time, which land in this one because nothing took them then. */
.co-note { margin: -4px 0 10px; font-size: .74rem; line-height: 1.45; color: var(--cal-text-subtle); }

/* Every entry, folded shut under the sheet — the working, for the one line
   somebody wants to check. The summary is a quiet link, like the
   denomination fold on End of Day; the table is compact, tabular, and ruled
   with the sheet's own hairline. A line that is not in the drawer is muted,
   and one voided before this cash out is struck through. */
.co-lines { margin: 0 0 10px; }
.co-lines > summary {
  padding: 4px 0; cursor: pointer; user-select: none;
  font-size: .74rem; font-weight: 500; color: var(--cal-text-subtle);
}
.co-lines > summary:hover { color: var(--cal-text-emphasis); text-decoration: underline; }
.co-lines > summary:focus-visible { outline: 2px solid var(--cal-border-emphasis); outline-offset: 2px; }
.co-lines table { width: 100%; margin-top: 5px; border-collapse: collapse; font-size: .72rem; font-variant-numeric: tabular-nums; }
.co-lines th {
  padding: 4px 6px; text-align: left; white-space: nowrap;
  font-size: .62rem; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  color: var(--cal-text-muted); border-bottom: 1px solid var(--cal-border-subtle);
}
.co-lines td { padding: 4px 6px; border-bottom: 1px solid var(--cal-border-subtle); color: var(--cal-text); vertical-align: top; overflow-wrap: anywhere; }
.co-lines .num { text-align: right; white-space: nowrap; }
.co-lines td.co-line-time,
.co-lines td.co-line-ref { white-space: nowrap; overflow-wrap: normal; }
.co-lines td.co-line-note { color: var(--cal-text-subtle); }
.co-lines tr.is-skip td { color: var(--cal-text-muted); }
.co-lines tr.is-voided td:not(.co-line-note) { text-decoration: line-through; }

/* THE CASH OUT PAGE. The form is one card on the canvas (the End of Day
   count's two columns inside it), the Done panel takes its place, and the
   history table sits under it as its own card. */
.co-card { padding: 16px 20px; }
@media (max-width: 640px) {
  .co-card { padding: 14px 14px; }
}
/* WHAT YOU TOOK — the ledger's side of the form, the left column (cashout.js
   paintForm: the first .sc-col, headed "What is not in a cash out yet") — is
   a WARM SURFACE: the pearl beige tint inside a beige hairline, so the
   figure the count is checked against reads as the summary it is, and the
   inputs on the right stay on the white card. Three tokens are re-pointed
   for this subtree only: the muted step (4.34:1 on the tint — it fails —
   to --on-beige-muted, 5.25:1) and the two decorative hairlines (cool greys
   that vanish on the tint, to the beige, 1.34:1) so the till slip's
   dividers still read. Every other ink the sheet uses was measured on the
   tint in section 1 (text 9.32 · subtle 5.25 · a carried row's gold ink
   5.02 · emphasis 15.11). */
.co-card .shift-form > .sc-col:first-child {
  padding: 12px 14px;
  background: var(--nbc-beige-tint);
  border: 1px solid var(--nbc-beige);
  border-radius: var(--radius-lg);
  --cal-text-muted: var(--on-beige-muted);
  --cal-border-muted: var(--nbc-beige);
  --cal-border-subtle: var(--nbc-beige);
}
/* Stacked (the form's own 760px break) the columns are 4px apart — right
   for two halves of one sheet, too tight once one of them is a card. */
@media (max-width: 760px) {
  .co-card .shift-form > .sc-col:first-child { margin-bottom: 12px; }
}
/* The total the denomination count worked out cannot be typed over; it
   reads as a figure, not as a dead field. */
.co-page .inp[readonly] { background: var(--cal-bg-muted); color: var(--cal-text-emphasis); font-weight: 600; }
.co-page .co-save { margin-top: 4px; }
.co-done-actions { display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; margin-top: 10px; }

/* The optional count by denomination, folded under the total: a pressable
   summary, then the grid — two across, four on a wide screen — of labelled
   34px number fields (a grid of twelve at the full control height would be
   taller than the form it hangs off), bills then coins under their own
   small caption. */
.co-denoms-fold { margin: 0 0 12px; }
.co-denoms-fold > summary.form-hint { padding: 4px 0; font-weight: 500; color: var(--cal-text-subtle); cursor: pointer; user-select: none; }
.co-denoms-fold > summary.form-hint:hover { color: var(--cal-text-emphasis); }
.co-denoms { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px 12px; margin-top: 6px; }
@media (min-width: 1280px) {
  .co-denoms { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
.co-denoms-hd {
  grid-column: 1 / -1; margin: 4px 0 0;
  font-size: .64rem; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  color: var(--cal-text-muted);
}
.co-denom { display: grid; grid-template-columns: 38px minmax(0, 1fr); align-items: center; gap: 6px; min-width: 0; }
.co-denom > span { font-size: .78rem; font-weight: 600; color: var(--cal-text-emphasis); font-variant-numeric: tabular-nums; white-space: nowrap; }
.co-denom .inp { height: 34px; min-width: 0; text-align: right; font-variant-numeric: tabular-nums; }

/* The history under the form: a heading with its controls on one line —
   Mine | Terminal for a manager, the range pills — then the table card. */
.co-history { margin-top: 22px; }
.co-history-hd { display: flex; align-items: center; justify-content: space-between; gap: 8px 14px; flex-wrap: wrap; margin: 0 0 10px; }
.co-history-hd h2 { margin: 0; font-size: .88rem; font-weight: 600; color: var(--cal-text-emphasis); }
.co-history-tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.co-history-tools .seg.seg-sm { margin: 0; }

/* The verify block, and what a verify recorded, in the window: ruled off
   from the facts above it, because the facts are the person's word and
   the block is the verifier's. Drawn by App.CashOutVerify; the hi- name
   predates it. */
.hi-confirm { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--cal-border-subtle); }
.hi-confirm > h3 { margin: 0 0 8px; font-size: .8rem; font-weight: 600; color: var(--cal-text-emphasis); }

/* THE VERIFY WINDOW (App.CashOutVerify, components/handin-confirm.js). It
   is the transaction overlay's own shell (10B, App.FundsOverlay.shell) —
   head, close button, ruled foot with its status line, backdrop and motion
   are all 10B's — so only what a cash out adds is here: the width for two
   columns, the status and kind in the head's sub line, the compact figure
   row, the evidence card on the right, and the right column's own scroll.
   Scoped to cv-. Tokens only. */

/* The facts and the sheet side by side need more than the record form's
   600px; 880px is the wide modal's width, which carried this sheet before. */
.overlay.cv-overlay { max-width: 880px; }

/* The status and the kind lead the sub line, badges inline with the words
   after them. vertical-align nudges the square badge onto the text's line. */
.cv-sub-tags { display: inline-flex; align-items: center; gap: 6px; margin-right: 8px; vertical-align: 1px; }

/* THE FIGURES — the same warm .dc-fig boxes (13), three across on any
   width: they are the glance, and a row that wrapped one box under the other
   two would cost the height the columns below need. A long route name is
   cut in its box; the facts list carries it whole. */
.dc-figs.cv-figs { grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0 0 14px; }
.cv-figs .dc-fig strong { overflow: hidden; text-overflow: ellipsis; }

/* THE COLUMNS — .detail-cols (16), the transaction detail's split: the
   facts and the verify on the left, the evidence on the right. Stacked on
   a phone (its own 640px break) they need the row gap .detail-cols never
   had to give. */
.detail-cols.cv-cols { row-gap: 14px; }
.cv-main, .cv-side { min-width: 0; }

/* THE EVIDENCE — the breakdown sheet and the denomination count — as a
   card, a faint fill inside a hairline, square, no shadow: End of Day's
   report card (.dc-col, whose bands the sheet takes), so the drawer reads
   the way the day does. When it scrolls on its own, the card's edge is
   where the eye sees it continue. */
.cv-side { background: var(--cal-bg-muted); border: 1px solid var(--cal-border-subtle); padding: 10px 12px; }
.cv-side > .sc-col-hd:not(:first-child) { margin-top: 14px; }
.cv-side > *:last-child { margin-bottom: 0; }

/* The one sentence that says what verifying records, closing the block —
   read, not skimmed, so a step darker than a hint. Empty while the amount
   cannot be read, and then it takes no line. The block's own foot margin
   gives the column's end air when it scrolls. */
.cv-verify { margin-bottom: 4px; }
.cv-sentence { margin: 0; line-height: 1.45; color: var(--cal-text-subtle); }
.cv-sentence:empty { display: none; }

/* The body's floor matches its head: 10B's 4px suits a form whose last
   field has its own margin, and the evidence card has none. */
.cv-overlay .overlay-body.cv-body { padding-bottom: 14px; }

/* EACH COLUMN SCROLLS ON ITS OWN, and the window does not, wherever there
   is room for two columns and a screen tall enough to scroll inside: the
   body stops being the scroller and becomes a column of figures over the
   two columns, which take what height is left (one row, minmax(0, 1fr),
   stretched) and scroll within it. A long sheet then never pushes the
   facts, the verify block or the foot off the screen. The left column
   keeps 4px of air on each side so an input's 3px focus ring is not cut by
   its own scroll box. Below either bound the body scrolls as 10B made it. */
@media (min-width: 641px) and (min-height: 560px) {
  .cv-overlay .overlay-body.cv-body { display: flex; flex-direction: column; }
  .cv-body > .cv-figs { flex: none; }
  .cv-body > .cv-cols {
    flex: 1 1 auto; min-height: 0;
    grid-template-rows: minmax(0, 1fr); align-items: stretch;
  }
  .cv-cols > .cv-main, .cv-cols > .cv-side { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
  .cv-cols > .cv-main { margin: 0 -4px; padding: 0 4px 2px; }
}

/* THE CASH-OUT FLOW (public/components/cashout-flow.js, App.CashOutFlow):
   the ONE drawer form. Mounted inline on a clerk's Cash out page it draws
   the .panel.co-card above unchanged; opened in the shared .overlay (10B)
   by a regional manager, a cash collector or an administrator cashing out
   someone at their terminal, it is the picker — everyone with a drawer
   there — and then the same form. Scoped to cof-. Tokens only. */

/* The two-column count needs more than the record form's 600px; the
   picker takes the same width so the panel never jumps when a person is
   picked. Inside it the body IS the card: no second edge, no second
   padding — the warm left column (.co-card above) still applies. */
.overlay.cof-overlay { max-width: 920px; }
.cof-overlay .overlay-body { padding-bottom: 16px; }
.cof-overlay .co-card.cof-card { padding: 0; }
/* The same two rules .co-page gives the form on its own page, so the
   locked total and the Save button read the same in the overlay. */
.cof-form .inp[readonly] { background: var(--cal-bg-muted); color: var(--cal-text-emphasis); font-weight: 600; }
.cof-form .co-save { margin-top: 4px; }

/* Whose drawer this is, ruled off over the two columns: "Cashing out
   Maria Pech's drawer", the role and the terminal under it, and — when
   the form was reached through the picker — the way back. */
.cof-form-hd {
  display: flex; align-items: baseline; justify-content: space-between; gap: 6px 14px; flex-wrap: wrap;
  margin: 0 0 12px; padding-bottom: 10px;
  border-bottom: 1px solid var(--cal-border-subtle);
}
.cof-form-hd h2 { margin: 0; font-size: .95rem; font-weight: 600; color: var(--cal-text-emphasis); overflow-wrap: anywhere; }
.cof-form-hd p { margin: 2px 0 0; font-size: .78rem; color: var(--cal-text-subtle); }
.cof-form-hd .btn-link { font-size: .76rem; color: var(--cal-text-subtle); }
.cof-form-hd .btn-link:hover { color: var(--cal-text-emphasis); }

/* THE PICKER. A search field with the glyph inside it (the page search's
   recipe, on a form field), then one pressable row per person: the name
   and the role on the left, what is pending and when they last cashed
   out on the right. A person with nothing pending is shown dimmed and
   disabled — "Nothing to cash out" — so the list still says who works
   here, and a cash out of nothing cannot be started. */
.cof-search { position: relative; margin: 0 0 10px; }
.cof-search-ic {
  position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
  display: flex; color: var(--cal-text-muted); pointer-events: none;
}
.cof-search-ic svg { width: 15px; height: 15px; }
.cof-search .inp { -webkit-appearance: none; appearance: none; padding-left: 33px; }
.cof-search .inp::-webkit-search-decoration { -webkit-appearance: none; }
.cof-people { display: grid; gap: 6px; }
.cof-person {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "name pending" "role last";
  gap: 1px 14px; align-items: baseline;
  width: 100%; padding: 9px 12px;
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-md);
  background: var(--cal-bg); color: var(--cal-text);
  font: inherit; text-align: left; cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.cof-person:hover:not(:disabled) { border-color: var(--cal-border); background: var(--cal-bg-subtle); }
.cof-person:focus-visible {
  outline: none; border-color: var(--cal-border-emphasis);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--cal-border-emphasis) 30%, transparent);
}
.cof-person:disabled, .cof-person.is-empty { cursor: default; background: var(--cal-bg-subtle); color: var(--cal-text-muted); }
.cof-person-name { grid-area: name; font-size: .86rem; font-weight: 600; color: var(--cal-text-emphasis); overflow-wrap: anywhere; }
.cof-person.is-empty .cof-person-name { color: var(--cal-text-subtle); }
.cof-person-role { grid-area: role; font-size: .72rem; color: var(--cal-text-muted); }
.cof-person-pending {
  grid-area: pending; justify-self: end;
  font-size: .82rem; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap;
  color: var(--cal-text-emphasis);
}
.cof-person.is-empty .cof-person-pending { font-weight: 500; color: var(--cal-text-muted); }
.cof-person-last { grid-area: last; justify-self: end; font-size: .72rem; color: var(--cal-text-muted); white-space: nowrap; }
/* On a phone the four facts stack; nothing is pushed off the right edge. */
@media (max-width: 480px) {
  .cof-person { grid-template-columns: minmax(0, 1fr); grid-template-areas: "name" "role" "pending" "last"; }
  .cof-person-pending, .cof-person-last { justify-self: start; white-space: normal; }
}
.cof-people-note { margin: 8px 0 0; font-size: .74rem; color: var(--cal-text-muted); }

/* ============================================================
   18D. DASHBOARD — the terminal desk (public/views/dashboard.js, the
   CLERK / MANAGER branch) and the notification cards (public/app.js).
   A clerk lives on this screen all day, so it is two columns: the
   calendar, and beside it the counter's one list — My sales today, the
   same tape for a clerk and a manager.
   Everything here is the staff theme: --cal-* / --nbc-* tokens, the
   radius-md / radius-xl corners, the card shadow; nothing from the
   conductor's cx system. The tonal circles reuse the three badge fills
   (role = brand tint, ok = green tint, warn = gold tint), whose text inks
   were measured on those very fills in section 1.
   ============================================================ */

/* The greeting header: the same frosted .page-hd, a little warmer — the
   greeting is the one page title that names a person. No buttons: the tiles
   under it are the actions. */
.desk-hd h1 { font-size: 1.35rem; font-weight: 600; letter-spacing: -.02em; }

/* ── THE QUICK-ACTION TILES ──
   LESS CLICKING. The desk's three (a manager's five) actions are direct
   card-buttons — Sell ticket · Cash out · Record transaction — each a
   tonal icon, a title, a sub-line that carries a live fact ("14 sold
   today", "3 ready") or the gate's sentence when the tile is disabled, and
   an arrow that steps right under a hand. The first is the PRIMARY: the
   solid button's own lit gradient (--brand-grad) with the brand glow, the
   one shining thing on the desk, because selling a ticket is what the desk
   is for. The grid fits as many as the width allows at 200px each — all
   five of a manager's on one row from a 1366px laptop. */
.qa-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
  margin: 0 0 var(--gap-section);
}
.qa-tile {
  display: grid;
  grid-template-columns: 36px minmax(0, 1fr) 18px;
  grid-template-areas: "ic title go" "ic sub go";
  column-gap: 12px; row-gap: 1px;
  align-items: center;
  min-height: 72px;
  padding: 14px 14px 14px 16px;
  text-align: left;
  background: var(--cal-bg);
  border: 1px solid var(--cal-border-subtle);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-card);
  color: var(--cal-text);
  font: inherit;
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.qa-tile:hover:not(:disabled) { transform: translateY(-2px); box-shadow: var(--shadow-lift); }
.qa-tile:hover:not(:disabled) .qa-tile-go { color: var(--cal-brand); transform: translateX(3px); }
.qa-tile:active:not(:disabled) { transform: none; }
.qa-tile:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--cal-bg), 0 0 0 4px var(--cal-brand), var(--shadow-card); }
.qa-tile-ic {
  grid-area: ic;
  width: 36px; height: 36px; display: grid; place-items: center;
  border-radius: var(--radius-md);
  background: var(--badge-role-bg); color: var(--badge-role-text);
}
.qa-tile-ic svg { width: 18px; height: 18px; }
.qa-tile-title { grid-area: title; font-size: .92rem; font-weight: 600; color: var(--cal-text-emphasis); line-height: 1.25; }
/* Holds a sentence OR a .badge — the live count rides here. */
.qa-tile-sub { grid-area: sub; font-size: var(--fs-small); color: var(--cal-text-subtle); line-height: 1.4; min-width: 0; }
.qa-tile-go { grid-area: go; display: inline-flex; color: var(--cal-text-muted); transition: transform var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease); }
.qa-tile-go svg { width: 16px; height: 16px; }
/* THE PRIMARY TILE: the brand button's own fill and glow — it IS the
   desk's solid button, drawn as a tile. Title and arrow white (5.69:1 at
   the lightest stop); the sub-line --on-brand-muted (4.62:1 there —
   --on-navy-muted would be 3.11:1 on this azure); the icon box a white
   wash (the white glyph on it 4.19:1, an icon needs 3:1). Hover lifts and
   deepens the glow but does NOT brighten the fill the way the button
   does: the sub-line would fall under 4.5:1 on the brighter stop, and the
   2px lift already answers the hand. */
.qa-tile.is-primary {
  background: var(--brand-grad);
  border-color: transparent;
  color: var(--on-navy);
  box-shadow: var(--shadow-brand);
}
.qa-tile.is-primary .qa-tile-ic { background: var(--on-navy-line); color: var(--on-navy); }
.qa-tile.is-primary .qa-tile-title, .qa-tile.is-primary .qa-tile-go { color: var(--on-navy); }
.qa-tile.is-primary .qa-tile-sub { color: var(--on-brand-muted); }
.qa-tile.is-primary:hover:not(:disabled) { box-shadow: var(--shadow-brand-hover); }
.qa-tile.is-primary:hover:not(:disabled) .qa-tile-go { color: var(--on-navy); }
/* Press goes FLAT and dark, as the solid button does (--cal-brand-emphasis:
   white 11.35:1, the sub-line 9.22:1, the icon on its wash 7.19:1). */
.qa-tile.is-primary:active:not(:disabled) { background: var(--cal-brand-emphasis); box-shadow: var(--shadow-brand); }
/* Gated: the muted fill, flat, no arrow — and the gate's sentence lives in
   the sub-line, which may wrap; the grid grows to hold it. The edge and the
   ink are named again because a gated PRIMARY would otherwise keep the
   gradient's transparent edge and white ink on the muted fill; source order
   (same specificity as .is-primary) is what makes these win. */
.qa-tile:disabled { cursor: not-allowed; background: var(--cal-bg-muted); border-color: var(--cal-border-subtle); color: var(--cal-text); box-shadow: none; transform: none; }
.qa-tile:disabled .qa-tile-title { color: var(--cal-text-muted); }
.qa-tile:disabled .qa-tile-ic { background: var(--cal-bg-subtle); color: var(--cal-text-muted); }
.qa-tile:disabled .qa-tile-go { visibility: hidden; }
.qa-tile:disabled .qa-tile-sub { color: var(--cal-text-subtle); }

/* The calendar on the left, My sales today on the right. The CALENDAR
   sets the row's height and the panel takes exactly that height — pinned
   to the aside's box — and scrolls inside it, so a long day of sales never
   pushes the page taller than the timetable beside it. Under 1200px the
   two stack and the panel caps its own height again. */
.desk-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--gap-section);
}
.desk-main { min-width: 0; }
.desk-side { min-width: 0; }
@media (min-width: 1200px) {
  .desk-split { grid-template-columns: minmax(0, 1fr) minmax(280px, 330px); }
  .desk-side { position: relative; }
  .desk-side > .desk-panel { position: absolute; inset: 0; max-height: none; }
}
@media (max-width: 1199px) {
  .desk-side > .desk-panel { max-height: 420px; }
}

.desk-panel {
  display: flex; flex-direction: column; min-width: 0;
  max-height: 320px;
  background: var(--cal-bg);
  border: 1px solid var(--cal-border-subtle);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-card);
  padding: 14px;
}
.desk-panel-hd { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; margin-bottom: 10px; min-width: 0; }
.desk-panel-hd h2 { flex: 1 1 auto; min-width: 0; margin: 0; font-size: .92rem; font-weight: 600; letter-spacing: -.005em; color: var(--cal-text-emphasis); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The counts drop under the heading when the column is narrow, rather
   than squeezing the heading onto three lines. */
.desk-panel-aside { margin-left: auto; font-size: .74rem; color: var(--cal-text-muted); white-space: nowrap; }

/* The icon circle in the header: brand tint by default; .is-gold is the
   Waiting badge's ink, for a panel of cash waiting for attention (none on
   the desk since the manager's pending panel went — kept for the next). */
.desk-panel-ic {
  width: 28px; height: 28px; flex: none;
  display: grid; place-items: center;
  border-radius: 999px;
  background: var(--badge-role-bg); color: var(--badge-role-text);
}
.desk-panel-ic.is-gold { background: var(--badge-warn-bg); color: var(--nbc-gold-ink); }
.desk-panel-ic svg { width: 16px; height: 16px; }

/* The body scrolls inside the panel, so the panel never grows past its cap
   and the calendar never drops below the fold. */
.desk-panel-body { flex: 1; min-height: 0; overflow-x: hidden; overflow-y: auto; margin: 0 -4px; padding: 0 4px; }

.desk-note { margin: 2px 0 6px; font-size: var(--fs-small); color: var(--cal-text-subtle); line-height: 1.45; }

/* A desk list row: 38px, a 28px tonal icon, a title over a sub line, and
   the right-hand side (the amount, an action). Drawn by the manager's
   pending panel until cash outs moved to the collector; unused on the desk
   now and left for the next list. A sale row is its own grid (.desk-sale,
   below) and borrows none of these. */
.desk-list { display: flex; flex-direction: column; }
.desk-row {
  display: flex; align-items: center; gap: 8px;
  min-height: 38px; padding: 4px 0;
  border-top: 1px solid var(--cal-border-muted);
}
.desk-row:first-child { border-top: none; }
.desk-row.is-past { opacity: .62; }
.desk-row-ic {
  width: 28px; height: 28px; flex: none;
  display: grid; place-items: center; border-radius: 999px;
  background: var(--cal-bg-subtle); color: var(--cal-text-subtle);
}
.desk-row-ic svg { width: 15px; height: 15px; }
.desk-row-ic.is-bus { background: var(--badge-role-bg); color: var(--badge-role-text); }
.desk-row-main { flex: 1; min-width: 0; }
.desk-row-title { font-size: .82rem; font-weight: 600; color: var(--cal-text-emphasis); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.desk-row-sub { font-size: .72rem; color: var(--cal-text-subtle); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
.desk-row-side { display: flex; align-items: center; gap: 6px; flex: none; }
.desk-amount { font-size: .88rem; font-weight: 700; color: var(--cal-text-emphasis); font-variant-numeric: tabular-nums; }
.desk-ago { font-size: .7rem; color: var(--cal-text-muted); white-space: nowrap; }

/* A clerk's sale: two lines, two columns. Line one is the TICKET NUMBER —
   what is printed on the passenger's slip, and what the clerk searches for
   — with the time it was sold beside it; line two is the trip. The right
   column is the money over the tender mark. Every text cell is its own grid
   item, so a block, and clips with an ellipsis inside its own track, so
   nothing can run under the amount at any width. A refund stays on the
   list, muted with the amount struck, because it happened.
   ITS SKELETON is .skel-row (section 12), cut to these numbers — the 6px
   padding, the 10px column gap, the 1.2rem / 1.1rem line boxes and the
   2px between them — so the list is already the height it will be while it
   loads. Change one, change the other. */
.desk-sale {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "head amt"
    "meta tag";
  column-gap: 10px;
  row-gap: 2px;
  align-items: baseline;
  /* Out over the panel body's 4px padding so the hover fills the row edge
     to edge. .desk-list is a flex column, so the stretch sets the width; an
     explicit width would overflow the moment the list gained padding. */
  margin: 0 -4px;
  padding: 6px;
  text-align: left;
  background: none;
  border: 0;
  border-top: 1px solid var(--cal-border-muted);
  border-radius: var(--radius-sm);
  font: inherit;                   /* first: the shorthand resets what follows */
  font-variant-numeric: tabular-nums;
  color: var(--cal-text);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
}
.desk-sale:first-child { border-top: 0; }
/* --cal-bg-subtle, not -muted: -muted is 1.06:1 on white and does not show.
   Muted text on it is still 4.51:1. */
.desk-sale:hover { background: var(--cal-bg-subtle); }
.desk-sale:focus-visible { outline: 2px solid var(--cal-border-emphasis); outline-offset: -2px; }

/* Line one: the number reads first and is never squeezed out by the time. */
.desk-sale-head { grid-area: head; display: flex; align-items: baseline; gap: 6px; min-width: 0; line-height: 1.2rem; }
.desk-sale-no,
.desk-sale-meta {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.desk-sale-no   { font-size: .8rem; font-weight: 600; letter-spacing: .01em; color: var(--cal-text-emphasis); }
/* The time GIVES WAY FIRST. "T-BZE-20260924-0012" and "10:42 AM" need
   ~205px and a 330px panel leaves ~225, so both fit; but a narrower panel
   (a 320px phone) must not clip the number's tail — the sequence digits
   are what the clerk searches for. flex-shrink 1000 against the number's
   1 hands nearly all the shortfall to the time (shrink is weighted by
   size), which ellipsizes down to nothing before the number gives a pixel.
   The time is still in the row's accessible name. */
.desk-sale-time { flex: 0 1000 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: .7rem; font-weight: 500; color: var(--cal-text-subtle); white-space: nowrap; }
/* The typed text inside the number: the gold tint the Waiting badge wears,
   with the row's own ink on it (15:1). */
.desk-mark { background: var(--badge-warn-bg); color: inherit; border-radius: 3px; padding: 0 1px; box-shadow: inset 0 -1px 0 var(--badge-warn-border); }

/* Line two: where to, then the bus and seats. */
.desk-sale-meta  { grid-area: meta; line-height: 1.1rem; font-size: .72rem; color: var(--cal-text-subtle); }
.desk-sale-route { font-weight: 600; color: var(--cal-text); }
.desk-sale-arrow { font-weight: 500; color: var(--cal-text-muted); }
/* Type comes from .desk-amount, shared with the .desk-row lists. This
   only places it. */
.desk-sale .desk-amount { grid-area: amt; justify-self: end; line-height: 1.2rem; white-space: nowrap; }
.desk-sale-card  { grid-area: tag; justify-self: end; }

/* The two tape marks keep .badge's colour trios, cut to a 15px box (a
   .8rem line and its 1px border) that sits inside the 1.1rem (17.6px)
   line, so a Card or a Refunded never makes its row taller than the rest.
   Card sits under the amount like a tender mark; Refunded leads line 2,
   followed by the reason. */
.desk-sale-tag { gap: 0; padding: 0 4px; font-size: .62rem; line-height: .8rem; }
.desk-sale-meta .desk-sale-tag { margin-right: 6px; }

/* Colour, not opacity: --cal-text-muted is 4.99:1 and must stay over 4.5:1.
   The amount is struck (it is no longer money held); the number is NOT, so
   a refunded ticket can still be read and searched for. */
.desk-sale.is-refunded .desk-amount { text-decoration: line-through; color: var(--cal-text-muted); }
.desk-sale.is-refunded .desk-sale-no,
.desk-sale.is-refunded .desk-sale-time,
.desk-sale.is-refunded .desk-sale-route,
.desk-sale.is-refunded .desk-sale-meta { color: var(--cal-text-muted); }

/* Nothing waiting: the check in the green tint, centred. No sales yet:
   the receipt in the brand tint — a start, not a success. */
.desk-empty { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 20px 10px; text-align: center; }
.desk-empty-ic { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 999px; background: var(--badge-ok-bg); color: var(--badge-ok-text); margin-bottom: 4px; }
.desk-empty-ic.is-brand { background: var(--badge-role-bg); color: var(--badge-role-text); }
.desk-empty-ic svg { width: 18px; height: 18px; }
.desk-empty strong { font-size: .85rem; color: var(--cal-text-emphasis); }
.desk-empty span:last-child { font-size: .76rem; color: var(--cal-text-subtle); max-width: 36ch; }

/* "All … →" under a capped list: a text link, not a button. */
.desk-link { display: inline-block; margin-top: 8px; padding: 0; border: none; background: none; font: inherit; font-size: .78rem; font-weight: 600; color: var(--cal-brand); cursor: pointer; }
.desk-link:hover { text-decoration: underline; }

@media (max-width: 640px) {
  .desk-panel,
  .desk-side > .desk-panel { max-height: none; }
  .desk-row-side { flex-wrap: wrap; justify-content: flex-end; }
}

/* ── Notification cards (App.notifyHandIn, public/app.js) ──
   "New cash out" — a bag or a drawer waiting for the Cash Collector, shown
   to the collector only (an admin gets the To verify badge). A fixed stack
   in the top-right, above the page and below a modal (z 100), so a card
   never covers the verify dialog it opened. Each card slides in from the
   right over 220ms; the motion is dropped for anyone who asked their OS for
   less of it. */
.notify-stack {
  position: fixed; top: 14px; right: 14px; z-index: 90;
  display: flex; flex-direction: column; gap: 8px;
  width: min(320px, calc(100vw - 28px));
  pointer-events: none;
}
.notify-card {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 12px 12px 10px;
  background: var(--cal-bg);
  border: 1px solid var(--cal-border-subtle);
  border-left: 3px solid var(--nbc-gold);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-overlay);
  pointer-events: auto;
  opacity: 0; transform: translateX(24px);
  transition: opacity 220ms ease, transform 220ms ease;
}
.notify-card.is-in { opacity: 1; transform: none; }
.notify-ic {
  width: 32px; height: 32px; flex: none;
  display: grid; place-items: center; border-radius: 999px;
  background: var(--badge-warn-bg); color: var(--nbc-gold-ink);
}
.notify-ic svg { width: 18px; height: 18px; }
.notify-main { flex: 1; min-width: 0; }
.notify-title { font-size: .74rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--nbc-gold-ink); }
.notify-sub { margin-top: 2px; font-size: .78rem; color: var(--cal-text); line-height: 1.4; }
.notify-amount { margin-top: 3px; font-size: 1.2rem; font-weight: 600; letter-spacing: -.01em; color: var(--cal-text-emphasis); font-variant-numeric: tabular-nums; }
.notify-actions { display: flex; align-items: center; gap: 6px; margin-top: 6px; }
.notify-more {
  align-self: flex-end;
  padding: 4px 10px; border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-pill);
  background: var(--cal-bg); color: var(--cal-text-subtle);
  font: inherit; font-size: .72rem; font-weight: 600; cursor: pointer;
  box-shadow: var(--shadow-sm); pointer-events: auto;
}
.notify-more:hover { color: var(--cal-text-emphasis); }
@media (prefers-reduced-motion: reduce) {
  .notify-card { transition: none; opacity: 1; transform: none; }
}
@media (max-width: 640px) {
  .notify-stack { top: 64px; }   /* under the mobile top bar */
}

/* ============================================================
   18E. TICKETS — the counter (public/views/tickets.js) and the printed
   ticket (public/components/ticket-receipt.js). One screen: the trip on
   the left (From, To with its chips, the date, the departures as a
   listbox), the sale on the right (the stepper, the price, the total, Cash
   | Card, the change, one solid Sell), today's sales under both. Every
   token is the staff theme's: the seats pills reuse the three badge trios
   whose text inks were measured on those fills in section 1, the Full
   pill is the error ink on its own tint, the short-change figure the same
   red. Nothing here is new ink.
   ============================================================ */

/* The keyboard hint in the header: the counter runs on keys, and the keys
   are named where the eye goes first. */
.tk-hint { margin: 0; font-size: .72rem; color: var(--cal-text-muted); white-space: nowrap; }
.tk-hint kbd {
  display: inline-block; padding: 0 5px; margin: 0 1px;
  border: 1px solid var(--cal-border-subtle); border-bottom-width: 2px; border-radius: 6px;   /* a keycap, not a control: the control radius on a 19px box is a pill */
  background: var(--cal-bg-subtle); color: var(--cal-text-subtle);
  font-family: inherit; font-size: .66rem; font-weight: 600; line-height: 1.5;
}

/* Two panels from 960px, the trip a little wider than the sale because the
   departures list is the thing being read; one under the other below it. */
.tk-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; align-items: start; }
@media (min-width: 960px) {
  .tk-grid { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); }
}
/* NOTE: the trip and sale PANELS are gone — selling is App.TicketFlow now,
   and its chrome is conductor.css section 15. What is left in this section is
   the Complete overlay, the printed receipt, the Card badge Funds draws, and
   the sales-table rules the "tickets sold" screen will want. The eight rules
   the flow re-declares (.tk-day .tk-from .tk-fare .tk-quick .tk-change .tk-err
   .tk-auto .tk-trip) were deleted rather than left dead: two stylesheets
   defining one class is a look that depends on load order. */
.tk-trip, .tk-sale { min-width: 0; }
.tk-sale { position: sticky; top: 84px; }   /* stays beside the list while it scrolls, clear of the sticky header bar (~73px dense, up to ~84 when its title wraps) */
@media (max-width: 959px) { .tk-sale { position: static; } }

/* THE IN-PAGE SEAT MAP IS GONE. Selling is a workflow now — App.TicketFlow
   (components/ticket-flow.js) draws the coach on its own screen — so the
   `.tk-seat*` / `.tk-bus*` / `.tk-legend*` rules that used to live here
   moved to conductor.css section 15, with the rest of the flow's chrome.
   They are NOT duplicated here: two stylesheets defining `.tk-seat` would
   have left the coach's look depending on which one loaded last. */

.tk-field { margin-bottom: 12px; }
.tk-field > label { display: block; font-size: .74rem; font-weight: 600; color: var(--cal-text-emphasis); margin-bottom: 5px; }
.tk-field > label small { font-weight: 400; color: var(--cal-text-muted); }
.tk-note { margin: 4px 0; font-size: .78rem; color: var(--cal-text-subtle); line-height: 1.45; }

/* The To field, with the search glyph inside it. */
.tk-search { position: relative; }
.tk-search svg { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: var(--cal-text-muted); pointer-events: none; }
.tk-search .inp { padding-left: 34px; font-size: .92rem; }
.tk-search .inp::-webkit-search-cancel-button { cursor: pointer; }

/* The place chips under it — the towns served that day, the last one sold
   first; a run count rides small on each. The pressed one is the brand. */
.tk-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.tk-chip {
  display: inline-flex; align-items: center; gap: 5px;
  height: 28px; padding: 0 12px;
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-pill);
  background: var(--cal-bg); color: var(--cal-text-emphasis);
  font: inherit; font-size: .78rem; font-weight: 500; cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.tk-chip small { font-size: .66rem; font-weight: 600; color: var(--cal-text-muted); }
.tk-chip:hover { background: var(--cal-bg-muted); border-color: var(--cal-border-emphasis); }
.tk-chip:focus-visible { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--cal-border-emphasis) 25%, transparent); }
.tk-chip.on { background: var(--cal-brand); border-color: var(--cal-brand); color: var(--cal-brand-text); }
.tk-chip.on small { color: var(--cal-brand-text); opacity: .8; }
.tk-chip-sm { height: 24px; padding: 0 8px; font-size: .74rem; font-variant-numeric: tabular-nums; }

/* The departures: a listbox of rows, 44px, the time large, the fare and the
   two pills on the right. The highlighted row (the keyboard's) carries the
   focus ring; the chosen one the brand tint. A bus that has left is dimmed
   and not an option. */
.tk-deps-aside { font-weight: 400; color: var(--cal-text-muted); margin-left: 6px; }
.tk-deps { border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-lg); background: var(--cal-bg); max-height: 320px; overflow-y: auto; }
.tk-deps:focus { outline: none; border-color: var(--cal-border-emphasis); box-shadow: 0 0 0 3px color-mix(in srgb, var(--cal-border-emphasis) 25%, transparent); }
.tk-deps .tk-deps-empty { margin: 0; padding: 14px 12px; }
.tk-dep {
  display: flex; align-items: center; gap: 10px;
  min-height: 44px; padding: 6px 10px;
  border-top: 1px solid var(--cal-border-muted);
  cursor: pointer;
}
.tk-dep:first-child { border-top: none; }
.tk-dep:hover { background: var(--cal-bg-muted); }
.tk-dep.is-past { opacity: .55; cursor: default; }
.tk-dep.is-past:hover { background: transparent; }
.tk-dep.is-active { background: var(--cal-bg-subtle); box-shadow: inset 3px 0 0 var(--cal-brand); }
.tk-dep.is-selected { background: var(--cal-nav-active); box-shadow: inset 3px 0 0 var(--cal-brand); }
.tk-dep-main { flex: 1; min-width: 0; }
.tk-dep-time { font-size: .92rem; font-weight: 700; color: var(--cal-text-emphasis); font-variant-numeric: tabular-nums; white-space: nowrap; }
.tk-dep-arrow { color: var(--cal-text-muted); font-weight: 400; margin: 0 2px; }
.tk-dep-sub { font-size: .72rem; color: var(--cal-text-subtle); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
.tk-dep-side { display: flex; align-items: center; gap: 6px; flex: none; }
/* The class tag: Express in the brand tint, Regular quiet. */
.tk-class { display: inline-block; margin-left: 6px; padding: 1px 6px; border-radius: var(--radius-md); font-size: .64rem; font-weight: 600; letter-spacing: .03em; text-transform: uppercase; vertical-align: 2px; color: var(--cal-text-subtle); background: var(--cal-bg-subtle); }
.tk-class.is-express { color: var(--badge-role-text); background: var(--badge-role-bg); }

/* The pills. Seats: plenty in the brand tint, five or fewer in gold, none
   left in the error ink on its tint, unknown a quiet dash. State: Next in
   the green trio, Gone as dimmed text with no fill. */
.tk-pill {
  display: inline-flex; align-items: center;
  padding: 2px 8px; border-radius: var(--radius-md);
  font-size: .7rem; font-weight: 600; line-height: 1.5; white-space: nowrap;
  border: 1px solid transparent; font-variant-numeric: tabular-nums;
}
.tk-pill.is-ok { color: var(--badge-role-text); background: var(--badge-role-bg); border-color: var(--badge-role-border); }
.tk-pill.is-low { color: var(--badge-warn-text); background: var(--badge-warn-bg); border-color: var(--badge-warn-border); }
.tk-pill.is-full { color: var(--cal-error); background: var(--cal-error-bg); border-color: var(--cal-error-border); }
.tk-pill.is-unknown { color: var(--cal-text-muted); background: var(--cal-bg-subtle); border-color: var(--cal-border-subtle); }
.tk-pill.is-next { color: var(--badge-ok-text); background: var(--badge-ok-bg); border-color: var(--badge-ok-border); }
.tk-pill.is-gone { color: var(--cal-text-muted); }

/* ── The sale ── */
.tk-sale-empty { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 28px 10px; text-align: center; }
.tk-sale-empty-ic { width: 36px; height: 36px; display: grid; place-items: center; border-radius: 999px; background: var(--badge-role-bg); color: var(--badge-role-text); margin-bottom: 4px; }
.tk-sale-empty-ic svg { width: 18px; height: 18px; }
.tk-sale-empty strong { font-size: .85rem; color: var(--cal-text-emphasis); }
.tk-sale-empty span:last-child { font-size: .76rem; color: var(--cal-text-subtle); max-width: 36ch; }

.tk-summary { margin: -4px 0 12px; padding: 6px 10px; font-size: .8rem; font-weight: 600; color: var(--badge-role-text); background: var(--badge-role-bg); border: 1px solid var(--badge-role-border); border-radius: var(--radius-md); }

.tk-sale-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 12px; }
@media (max-width: 420px) { .tk-sale-grid { grid-template-columns: 1fr; } }

/* The stepper: − field +, the buttons --ctl-h like every control (so they
   stand level with the field between them), out of the Tab ring
   (tabindex -1) so Tab walks the fields and the arrow keys step. */
.tk-stepper { display: flex; align-items: stretch; }
.tk-stepper button {
  width: var(--ctl-h); height: var(--ctl-h); flex: none;
  border: 1px solid var(--cal-border); background: var(--cal-bg); color: var(--cal-text-emphasis);
  font: inherit; font-size: 1.05rem; font-weight: 500; line-height: 1; cursor: pointer;
}
.tk-stepper button:first-child { border-radius: var(--radius-md) 0 0 var(--radius-md); border-right: none; }
.tk-stepper button:last-child { border-radius: 0 var(--radius-md) var(--radius-md) 0; border-left: none; }
.tk-stepper button:hover { background: var(--cal-bg-muted); }
.tk-stepper .inp { border-radius: 0; text-align: center; font-size: 1rem; font-weight: 700; font-variant-numeric: tabular-nums; -moz-appearance: textfield; }
.tk-stepper .inp::-webkit-outer-spin-button, .tk-stepper .inp::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.tk-stepper .inp:focus { position: relative; z-index: 1; }

/* Money fields wear a $ inside the box; figures are tabular. */
.tk-money-wrap { position: relative; }
.tk-money-wrap > span { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); font-size: .88rem; color: var(--cal-text-muted); pointer-events: none; }
.tk-money-wrap .inp { padding-left: 23px; }
.tk-money { font-size: .95rem; font-weight: 600; font-variant-numeric: tabular-nums; }

/* The total: the largest figure on the screen, ruled off above and below. */
.tk-total { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin: 2px 0 12px; padding: 8px 0; border-top: 1px solid var(--cal-border-subtle); border-bottom: 1px solid var(--cal-border-subtle); }
.tk-total span { font-size: .74rem; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--cal-text-muted); }
.tk-total strong { font-size: 1.6rem; font-weight: 700; letter-spacing: -0.02em; color: var(--cal-text-emphasis); font-variant-numeric: tabular-nums; line-height: 1.1; }

.tk-pay { width: 100%; }
.tk-pay button { flex: 1; height: 34px; padding: 0; font-size: .85rem; line-height: 1; }
.tk-pay button:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--cal-border-emphasis); }

.tk-sell { width: 100%; height: 42px; font-size: .92rem; }
.tk-sell svg { width: 16px; height: 16px; }

/* ── Complete ──
   What a finished sale leaves on screen. It is a layer over whatever page the
   clerk came back to, not a panel on the counter, so it takes the modal
   scrim and the modal shadow: the sale is over and the only two answers are
   the next ticket or nothing. Centred, because there is one thing to read —
   the change to hand back — and one button to press. */
.tk-done { width: 100%; max-width: 360px; padding: 22px 20px 18px; text-align: center; background: var(--cal-bg); border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-xl); box-shadow: var(--shadow-modal); transform: translateY(12px) scale(.98); transition: transform var(--dur) var(--ease); }
.modal-backdrop.show .tk-done { transform: none; }
.tk-done-ic { width: 44px; height: 44px; margin: 0 auto 10px; display: grid; place-items: center; border-radius: 999px; background: var(--badge-ok-bg); color: var(--badge-ok-text); }
.tk-done-ic svg { width: 24px; height: 24px; }
.tk-done h2 { margin: 0; font-size: .95rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--nbc-green-ink); }
.tk-done-amt { margin-top: 4px; font-size: 1.7rem; font-weight: 700; letter-spacing: -0.02em; line-height: 1.1; color: var(--cal-text-emphasis); font-variant-numeric: tabular-nums; }
.tk-done-no { margin-top: 2px; font-size: .8rem; font-weight: 600; color: var(--cal-text-subtle); font-variant-numeric: tabular-nums; }
.tk-done-trip { margin: 6px 0 0; font-size: .8rem; font-weight: 500; color: var(--cal-text); line-height: 1.45; }
/* The one figure the passenger is waiting on — the sheet's FIGURE BAND, on
   the warm surface: the pearl beige tint in a beige hairline, so the change
   to hand back is the one warm thing on a white sheet. Its caption reads
   the muted step, re-pointed here to --on-beige-muted (5.25:1; the muted
   step itself is 4.34:1 on the tint); the figure is --ledger-in, 6.73:1. */
.tk-done-change {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  margin-top: 12px; padding: 8px 10px;
  border: 1px solid var(--nbc-beige); border-radius: var(--radius-md);
  background: var(--nbc-beige-tint);
  --cal-text-muted: var(--on-beige-muted);
}
.tk-done-change span { font-size: .74rem; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--cal-text-muted); }
.tk-done-change strong { font-size: 1.3rem; font-weight: 700; color: var(--ledger-in); font-variant-numeric: tabular-nums; line-height: 1.1; }
.tk-done-rows { margin: 8px 0 0; padding: 0; text-align: left; }
.tk-done-actions { display: flex; gap: 8px; margin-top: 14px; }
.tk-done-actions .btn { flex: 1; height: var(--ctl-h); }
.tk-done-print { width: 100%; margin-top: 6px; height: var(--ctl-h-row); font-size: .78rem; }
.tk-done-print svg { width: 15px; height: 15px; }

/* ── Today's sales ── */
.tk-sales { margin-top: 18px; }
.tk-sales-hd { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
.tk-sales-hd h2 { margin: 0; font-size: .88rem; font-weight: 600; color: var(--cal-text-emphasis); }
.tk-totals { margin: 0; font-size: .78rem; font-weight: 500; color: var(--cal-text-subtle); font-variant-numeric: tabular-nums; }
.tk-sales-wrap { max-height: 420px; }
.tk-no { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.tk-day-tag { color: var(--cal-text-muted); font-size: .7rem; }
.tk-row-actions { display: inline-flex; align-items: center; gap: 6px; justify-content: flex-end; }

/* ── Card, elsewhere ──
   The Funds row badge after a card amount, the small "Card" figure beside
   an income tile, the Paid by control in the record form, and the ticket
   block in the transaction detail. */
.tk-card-badge { margin-left: 6px; vertical-align: 1px; }
.data-table tbody tr.row-voided td .tk-card-badge { text-decoration: none; }
.tk-card-fig { margin-left: 6px; font-weight: 600; color: inherit; white-space: nowrap; }
.form-grid .form-row.tk-payrow > .seg { width: auto; }
/* The track at --ctl-h, level with the fields beside it in the form grid. */
.form-grid .form-row.tk-payrow > .seg button { flex: none; height: calc(var(--ctl-h) - 8px); padding: 0 18px; line-height: 1; }
.tk-detail { margin-top: 12px; padding: 10px 12px; border: 1px solid var(--badge-role-border); border-radius: var(--radius-md); background: var(--badge-role-bg); }
.tk-detail-hd { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 5px; }
.tk-detail-hd strong { font-size: .95rem; font-weight: 700; color: var(--badge-role-text); font-variant-numeric: tabular-nums; }
.tk-detail p { margin: 0; font-size: .8rem; color: var(--cal-text); line-height: 1.5; }
/* Four figures on the Finish-the-day sheet once card payments are shown. */
.dc-sheet .dc-figs.dc-sheet-figs.has-card { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/* ── The printed ticket (App.TicketReceipt) ──
   Drawn inside .receipt, whose pre-wrap and monospace it overrides. Since
   2026-09-27 it is laid out like the departure board's boarding pass
   (ticket-flow.css, .tf-pass): the two times large at either end over their
   stop codes, the ride between them, then the facts in labelled strips.
   INK ONLY — an 80mm thermal head prints black and nothing else, and a grey
   comes out as a dither. So there is no fill, no tint and no muted colour
   here: hierarchy is size, weight, letter-spacing and rules, all in
   currentColor. These rules also draw it on screen. */
.tk-rc { white-space: normal; font-family: var(--font-sans); font-size: 11px; line-height: 1.35; }
.tk-rc-head { padding-bottom: 4px; border-bottom: 2px solid currentColor; }
.tk-rc-org { text-align: center; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; }
.tk-rc-kicker { display: flex; justify-content: space-between; margin-top: 2px; font-size: 9.5px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; }
.tk-rc-void { margin: 6px 0; padding: 4px; text-align: center; font-weight: 700; border: 2px solid currentColor; }
.tk-rc-no { margin-top: 6px; text-align: center; font-size: 15px; font-weight: 700; letter-spacing: .02em; font-variant-numeric: tabular-nums; }

/* The pass: here | the ride | there. The middle is fixed so both ends get
   the same room; nothing but a stop's name may wrap. */
.tk-rc-route { display: grid; grid-template-columns: minmax(0, 1fr) 15mm minmax(0, 1fr); align-items: center; gap: 4px; margin: 8px 0 6px; }
.tk-rc-end { min-width: 0; }
.tk-rc-to { text-align: right; }
.tk-rc-time { font-size: 21px; font-weight: 700; line-height: 1.05; letter-spacing: -.02em; font-variant-numeric: tabular-nums; white-space: nowrap; }
.tk-rc-time small { margin-left: 2px; font-size: 10px; letter-spacing: 0; }
.tk-rc-code { margin-top: 2px; font-size: 13px; font-weight: 700; letter-spacing: .08em; }
.tk-rc-name { font-size: 10.5px; overflow-wrap: anywhere; }
.tk-rc-mid { display: flex; flex-direction: column; align-items: center; gap: 1px; font-size: 9.5px; font-weight: 600; white-space: nowrap; }
/* The ride's line runs up to the bus from both sides, so no fill has to
   mask it — a background would not print. */
.tk-rc-rule { display: flex; align-items: center; gap: 2px; width: 100%; }
.tk-rc-rule::before, .tk-rc-rule::after { content: ''; flex: 1; border-top: 1px solid currentColor; }
.tk-rc-glyph { display: inline-flex; }
.tk-rc-glyph svg { display: block; width: 13px; height: 13px; }
/* A village on the slip is a REQUEST STOP, said under the trip so the
   conductor reads it with the two names (NT-4B). */
.tk-rc-stop { margin: -2px 0 6px; text-align: center; font-size: 11px; }

/* The fact strips: a small-caps label over a bold value, first cell left,
   last right, a middle one centred. The day is the longest value, so its
   cell takes the most room. */
.tk-rc-strip { display: flex; gap: 6px; padding: 4px 0; border-top: 1px solid currentColor; }
.tk-rc-cell { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; }
.tk-rc-cell:first-child { flex-grow: 1.7; }
.tk-rc-cell:nth-child(2):not(:last-child) { text-align: center; }
.tk-rc-cell:last-child:not(:first-child) { text-align: right; }
.tk-rc-label { font-size: 8.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; white-space: nowrap; }
.tk-rc-val { font-size: 12px; font-weight: 700; overflow-wrap: anywhere; }
.tk-rc-strip-one .tk-rc-val { font-size: 11px; font-weight: 600; }
/* The seat is what the passenger holds the slip to prove at the bay, and
   the total is what they paid: the two values printed largest. */
.tk-rc-seats .tk-rc-val { font-size: 15px; }
.tk-rc-total .tk-rc-val { font-size: 16px; }
/* The perforation between the trip and the money, as on the screen pass. */
.tk-rc-perf { margin-top: 2px; border-top: 2px dashed currentColor; }
.tk-rc-perf + .tk-rc-strip { border-top: 0; }
.tk-rc-paynote { margin-top: -2px; text-align: right; font-size: 10px; }

/* The stub: the QR beside the words, not under them — the biggest saving
   on the roll. 24mm still scans from a phone (about 1mm a module); on
   screen it is a 96px square. The id under it is the same value the code
   holds, so a torn or smudged code can still be keyed in. */
.tk-rc-stub { display: flex; align-items: center; gap: 8px; margin-top: 6px; padding-top: 6px; border-top: 1px solid currentColor; }
.tk-rc-stub-text { min-width: 0; font-size: 10px; line-height: 1.4; }
.tk-rc-foot { margin-bottom: 3px; font-size: 11px; font-weight: 700; }
.tk-rc-qr { flex: none; width: 24mm; margin: 0; text-align: center; }
.tk-rc-qr svg { display: block; width: 24mm; height: 24mm; margin: 0 0 2px; shape-rendering: crispEdges; }
.tk-rc-qr-id { display: block; font-size: 7.5px; letter-spacing: .02em; word-break: break-all; }
@media screen {
  .tk-rc-qr { width: 96px; }
  .tk-rc-qr svg { width: 96px; height: 96px; }
}

/* ── THE USB PRINTER'S CHIP (public/components/usb-printer.js, 2026-09-27) ──
   Fixed at the foot of the page column, clear of the rail: "Printer ready"
   in the ok trio for four seconds after the page opens, then it fades out
   of the way; "Printer offline — tap to reconnect" in the warn trio,
   pinned until it is, and a button because the tap IS the fix. Floating
   rung (--shadow-overlay). z 45: over the wizard's sticky foot (25) and
   under the toast (50) and the modal's scrim (100), which dims it. On the
   ticket page it rises above the wizard's foot pill (12px off the floor
   plus its 60px) rather than sitting on the Total. The dot is a MARK:
   --nbc-emerald-rich attached, the ring gold away — never type. */
.usbchip {
  position: fixed; left: calc(var(--rail-w) + var(--nav-inset)); bottom: 14px; z-index: 45;
  display: inline-flex; align-items: center; gap: 8px;
  height: var(--ctl-h-sm); padding: 0 12px 0 10px;
  border: 1px solid var(--badge-ok-border); border-radius: var(--radius-pill);
  background: var(--badge-ok-bg); color: var(--badge-ok-text);
  font-family: var(--font-sans); font-size: var(--fs-small); font-weight: 600; line-height: 1; white-space: nowrap;
  box-shadow: var(--shadow-overlay); cursor: default; user-select: none; -webkit-tap-highlight-color: transparent;
  opacity: 1; transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.usbchip.is-hidden { display: none; }
.usbchip.is-fading { opacity: 0; pointer-events: none; }
.usbchip.is-off { border-color: var(--badge-warn-border); background: var(--badge-warn-bg); color: var(--badge-warn-text); cursor: pointer; }
.usbchip.is-off:hover { transform: translateY(-2px); }
.usbchip:focus-visible { outline: 2px solid var(--cal-border-emphasis); outline-offset: 2px; }
body[data-view="tickets"] .usbchip { bottom: 84px; }
.usbchip-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--cal-border); }
.usbchip.is-ready .usbchip-dot, .usbchip-dot.is-ready { background: var(--nbc-emerald-rich); }
.usbchip.is-off .usbchip-dot, .usbchip-dot.is-off { background: var(--nbc-gold); animation: usbBlink 1.4s ease-in-out infinite; }
@keyframes usbBlink { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
/* The pairing modal's body (App.UsbPrinter.openSetup): the status line
   with the same dot, the row of small actions, and the two notes a clerk
   needs — where it works, and the Windows driver sentence for IT. */
.usbset-status { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; font-size: var(--fs-body); font-weight: 500; color: var(--cal-text-emphasis); }
.usbset-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 14px; }
.usbset-note { margin: 0 0 8px; font-size: var(--fs-small); line-height: 1.5; color: var(--cal-text-subtle); }
.usbset-note:last-child { margin-bottom: 0; }
@media (max-width: 768px) {
  .usbchip { left: 16px; }
}
@media (prefers-reduced-motion: reduce) {
  .usbchip { transition: none; }
  .usbchip.is-off:hover { transform: none; }
  .usbchip.is-off .usbchip-dot, .usbchip-dot.is-off { animation: none; }
}

/* ============================================================
   18F. ROUTE SETTINGS — the timetable (public/views/routes.js)
   Two plain tables of reference data, so almost everything here is the
   data-table from section 11. What is new is small: a two-line cell, the
   copyable document id, and the seven day pips that say which days a run
   goes out on. Since NT-2B the runs carry their line: the Line column,
   the line filter, the "Check times" badge, and the two run windows (the
   run and the run form), which wear 18H's network-window layout and add
   the run's stop list — see THE RUN WINDOWS below.
   ============================================================ */
.rs-note { display: flex; align-items: flex-start; gap: 8px; margin: 0 0 16px; padding: 8px 10px; border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-md); background: var(--cal-bg-muted); font-size: .78rem; line-height: 1.5; color: var(--cal-text-subtle); }
.rs-note svg { width: 15px; height: 15px; flex: none; margin-top: 2px; color: var(--cal-text-muted); }

/* The toolbar: which LIST on the left, which ROWS of it on the right. Two
   segmented controls at opposite ends read as two different questions; side
   by side they would read as one long one. */
.rs-bar { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.rs-tab-n { margin-left: 5px; padding: 1px 5px; border-radius: 999px; background: var(--cal-bg-muted); font-size: .68rem; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--cal-text-muted); }
.seg button.on .rs-tab-n { background: var(--on-navy-chip); color: inherit; }
/* A count not known yet — a network tab never opened, or one whose number
   a newer search made stale — is no chip at all, not an empty pill. */
.rs-tab-n:empty { display: none; }
/* The right end of the bar: the line filter (Runs only) beside All |
   Active | Retired. Both are toolbar-row height, so the pair sits level;
   the seg loses the bottom margin .seg.seg-sm gives a control standing
   over something, as .dc-head-tools does. */
.rs-bar-end { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.rs-bar-end .seg.seg-sm { margin: 0; }
.inp.rs-line-pick { width: auto; min-width: 150px; max-width: 220px; height: var(--ctl-h-row); padding: 0 10px; font-size: .78rem; }

/* The two-line cell every reference table uses: the thing, then what it is
   called elsewhere. */
.rs-cell { display: flex; flex-direction: column; gap: 1px; }
.rs-cell strong { font-weight: 600; color: var(--cal-text-emphasis); }
.rs-cell small { font-size: .7rem; color: var(--cal-text-muted); }

.rs-ref { white-space: nowrap; font-family: var(--font-mono); font-size: .72rem; font-variant-numeric: tabular-nums; color: var(--cal-text-subtle); }
.rs-trip { white-space: nowrap; }
.rs-arrow { color: var(--cal-text-muted); padding: 0 2px; }
.rs-time { white-space: nowrap; font-variant-numeric: tabular-nums; }
/* The journey and, on a run whose terminal time was worked out from the
   km, the "Check times" badge beside it — one line, so the row does not
   grow for a flag. */
.rs-sub { display: inline-flex; align-items: center; gap: 6px; }

/* The document id is read out, typed into the Firestore console and pasted
   into filters, so it is a button that copies itself rather than a string to
   retype by eye. The icon only appears on hover or focus — nine rows of it
   would read as a column of buttons instead of a column of ids. */
.rs-id { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 2px 6px; border: 1px solid transparent; border-radius: var(--radius-md); background: none; cursor: pointer; color: var(--cal-text-subtle); }
.rs-id code { font-family: var(--font-mono); font-size: .72rem; letter-spacing: .01em; }
.rs-id svg { width: 13px; height: 13px; opacity: 0; transition: opacity .12s ease; }
.rs-id:hover { border-color: var(--cal-border-subtle); background: var(--cal-bg-muted); color: var(--cal-text); }
.rs-id:hover svg, .rs-id:focus-visible svg { opacity: 1; }

/* Which days the run goes out. Seven boxes in week order, Sunday first, the
   same order the calendar reads daysOfWeek in. */
.rs-days { display: inline-flex; gap: 2px; }
/* The small radius: on a 16px box it makes a row of soft day-dots, which is
   what the seven of them are. */
.rs-pip { width: 16px; height: 16px; display: grid; place-items: center; border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-sm); font-size: .6rem; font-weight: 600; line-height: 1; color: var(--cal-text-muted); background: var(--cal-bg); }
.rs-pip.is-on { border-color: var(--cal-brand); background: var(--cal-brand); color: var(--cal-brand-text); }

/* A row opens its run, so it reads as something you can press: the pointer,
   the hover fill the table already has, and a real focus ring because the
   rows are in the tab ring and Enter opens one. */
.rs-row { cursor: pointer; }
.rs-row:focus-visible { outline: 2px solid var(--cal-brand); outline-offset: -2px; }
/* The buttons inside a clickable row keep their own pointer and do not read
   as part of it. */
.rs-row .rs-id, .rs-row .icon-btn { cursor: pointer; }

/* The detail sheet a row opens — where the document id and, for a unit, the
   runs it works now live. */
.rs-detail-id { font-family: var(--font-mono); font-size: .72rem; word-break: break-all; }
.detail-list .rs-id { margin: -2px 0; }
.rs-detail-runs { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--cal-border-subtle); }
.rs-detail-runs h3 { margin: 0 0 5px; font-size: .8rem; font-weight: 600; color: var(--cal-text-emphasis); }
.rs-detail-runs ul { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 3px; }
.rs-detail-runs li { font-size: .8rem; color: var(--cal-text); }
.rs-detail-runs li strong { font-variant-numeric: tabular-nums; margin-right: 6px; }
.rs-detail-runs p { margin: 0; }

/* The row's overflow button: quiet until the row is hovered or it is focused,
   so a column of them does not read as a column of buttons. */
.icon-btn.rs-menu { opacity: .55; transition: opacity .12s ease; }
.rs-row:hover .rs-menu, .rs-menu:focus-visible, .rs-menu[aria-expanded="true"] { opacity: 1; }
.btn.rs-danger { color: var(--cal-error); }
.btn.rs-danger:hover { background: var(--cal-error-bg); }

/* The day pips the run form sets — the same boxes the table draws. */
.rs-daypick { display: flex; gap: 5px; flex-wrap: wrap; }
.rs-daypick-day { position: relative; cursor: pointer; }
.rs-daypick-day input { position: absolute; opacity: 0; width: 0; height: 0; }
.rs-daypick-day span { display: grid; place-items: center; width: 34px; height: 34px; border: 1px solid var(--cal-border); border-radius: var(--radius-sm); background: var(--cal-bg); font-size: .76rem; font-weight: 600; color: var(--cal-text-subtle); transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease); }
.rs-daypick-day:hover span { border-color: var(--cal-brand); color: var(--cal-text-emphasis); }
.rs-daypick-day input:checked + span { border-color: var(--cal-brand); background: var(--cal-brand); color: var(--cal-brand-text); }
.rs-daypick-day input:focus-visible + span { outline: 2px solid var(--cal-brand); outline-offset: 2px; }

.rs-runs { font-size: .76rem; font-weight: 500; color: var(--cal-text); white-space: nowrap; }
.rs-none { font-size: .76rem; color: var(--cal-text-muted); }

/* THE RUN WINDOWS (plan NT-2B) — the run opened from its row, and the run
   form. Both are App.FundsOverlay.shell windows laid out like 18H's (the
   .nw-figs row, .nw-cols with .nw-main and the .nw-side card scrolling on
   their own), through the 18H classes routes.js adds to the panel; what is
   here is only what a run adds. Each selector carries its 18H class too,
   because 18H comes later in the file and would otherwise win. The form's
   two columns are equal: the run's fields on the left, its stops on the
   right. */
.overlay.nw-overlay.rs-run-sheet { max-width: 900px; }
.overlay.nw-line-overlay.rs-run-overlay { max-width: 1080px; }
@media (min-width: 641px) {
  .detail-cols.nw-line-cols.rs-run-cols { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}

/* The line is FOUND from the two ends, so it reads as a line of text; it
   becomes a seg only when two lines hold both ends. The box keeps a
   control's height, so the fields under it do not jump as it changes. */
.rs-line-box { min-height: var(--ctl-h); display: flex; flex-direction: column; justify-content: center; gap: 4px; }
.rs-line-box > .seg { align-self: flex-start; }
.rs-line-box > .form-hint { margin: 0; }
.rs-line-read { margin: 0; font-size: .85rem; color: var(--cal-text); }
.rs-line-read strong { font-weight: 600; color: var(--cal-text-emphasis); }
.rs-line-read.is-muted { font-size: var(--fs-small); color: var(--cal-text-muted); }
.rs-line-read.is-error { font-size: var(--fs-small); color: var(--cal-error); }
.overlay .rs-line-box > .seg[aria-invalid="true"] { border-color: var(--cal-error); }

/* Departs · Arrives · Journey — one row of three, because they are one
   question; the hint under them spans the row. */
.form-row.rs-times { grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: 12px; row-gap: 0; }
.rs-time-field { display: grid; gap: 5px; align-content: start; min-width: 0; }
.rs-time-field > label { font-size: .76rem; font-weight: 600; color: var(--cal-text-emphasis); }
.rs-time-field > label small { font-weight: 400; color: var(--cal-text-muted); }
.rs-times-hint { grid-column: 1 / -1; margin: 4px 0 0; }

/* A count beside a card's heading, as the line editor's reads. */
.sc-col-hd .rs-hd-n { margin-left: 6px; font-size: .74rem; font-weight: 400; color: var(--cal-text-muted); }

/* "Check times": a terminal time worked out from the km, not stated by
   head office. The warn tint the badge wears, as a line the reason reads
   in. */
.rs-check-note {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 0 0 8px; padding: 8px 10px;
  border: 1px solid var(--badge-warn-border); background: var(--badge-warn-bg);
  font-size: .78rem; line-height: 1.45; color: var(--cal-text);
}
.rs-check-note .badge { flex: none; }

/* THE RUN'S STOPS — 18H's white list (.nw-stops), one row per served stop
   in travel order: # · stop · kind · km from the start · the time ·
   remove. The ends are fixed — their times are Departs and Arrives — and
   sit on a faint fill; a village's time is the ≈ estimate, in the lighter
   ink; a terminal between the ends carries its own time field. The read
   version (the run window) leads with the time instead. Fixed tracks, so
   the times line up whatever the names. */
.rs-rstop {
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr) 70px 56px 104px 28px;
  align-items: center; column-gap: 8px;
  min-height: 40px;
  padding: 4px 8px 4px 6px;
  border-top: 1px solid var(--cal-border-muted);
}
.rs-rstop:first-child { border-top: 0; }
.rs-rstop.is-end { background: var(--cal-bg-muted); }
.rs-rstop.is-read { grid-template-columns: 76px minmax(0, 1fr) 70px 56px; padding-left: 10px; }
.rs-rstop-km { font-size: .74rem; color: var(--cal-text-muted); text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.rs-rstop-when { min-width: 0; text-align: right; }
.rs-rstop.is-read .rs-rstop-when { text-align: left; }
.rs-rstop-time { font-size: .8125rem; font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--cal-text-emphasis); }
strong.rs-rstop-time { font-weight: 700; }
.rs-rstop-time.is-est { font-weight: 400; color: var(--cal-text-subtle); }
.rs-rstop .inp.rs-rstop-inp { height: var(--ctl-h-sm); padding: 0 6px; font-variant-numeric: tabular-nums; }
.rs-rstop-btn { display: inline-flex; justify-content: flex-end; }
.rs-rstop.is-invalid { background: var(--cal-error-bg); }
.rs-rstop-err { grid-column: 2 / -1; margin: 2px 0 0; font-size: var(--fs-small); line-height: 1.4; color: var(--cal-error); }
.rs-rstop-err:empty { display: none; }

/* A phone: the kind and the km give way to the stop's name and its time,
   and the three times pair up. */
@media (max-width: 640px) {
  .rs-rstop { grid-template-columns: 22px minmax(0, 1fr) 96px 28px; }
  .rs-rstop .nw-kind, .rs-rstop .rs-rstop-km { display: none; }
  .rs-rstop.is-read { grid-template-columns: 70px minmax(0, 1fr); }
  .form-row.rs-times { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* A retired row is still readable — the ledger points at it — but it is not
   part of today's timetable, so it recedes. */
.data-table tbody tr.rs-retired td { opacity: .62; }
.data-table tbody tr.rs-retired td .badge { opacity: 1; }

/* COLUMN WIDTHS, so nothing is squeezed to fit. Only the Run column is
   allowed to take the slack — every other column is sized to its content and
   told not to wrap, which is what stops "Orange Walk → Corozal" folding onto
   three lines when the window narrows. The run's own name wraps instead,
   because it is the one cell with somewhere to go. */
.rs-table { table-layout: auto; }
.rs-table th, .rs-table td { white-space: nowrap; }
.rs-table .rs-col-run { white-space: normal; min-width: 190px; }
.rs-table .rs-ref { width: 1%; }
.rs-table .col-actions { width: 1%; }
.rs-table .rs-cell small { white-space: nowrap; }

/* Under ~1000px the timetable genuinely does not fit, and a cramped table is
   worse than a scrolled one — but only the LAST columns move, because the
   handle and the run are what you scroll back to. */
@media (max-width: 1000px) {
  .rs-table-runs { min-width: 960px; }
}
@media (max-width: 760px) {
  .rs-table-fleet { min-width: 660px; }
  .rs-bar { gap: 8px; }
  .rs-bar .seg { flex: 1 1 auto; }
}

/* ============================================================
   18G. DEPOSITS — the terminal's cash on its way to the bank
   (public/views/deposits.js, nav id `deposits`).
   The last link of the cash chain: verified cash outs are ticked into a
   deposit, the slip is recorded against it, and head office completes or
   queries it. Almost everything is REUSED: .page-hd and the scope select
   (8), .seg and the range pills (5, 11), .data-table.sheet and the badges
   (11) — Prepared wears .badge-waiting, In review .badge-role, Queried
   .badge-danger, Completed .badge-ok —, .form-grid, .dc-variance for the
   slip − expected figure, and inside the deposit the End of Day receipt
   (.dc-two-col, .sc-sheet), .detail-list, .dc-notice and .dc-timeline.
   What is here is only what the page adds: two sections on one card, the
   tick column, the fold for a slip already in hand, the bar with the live
   total and the one solid button, and — scoped dpw- at the end of this
   section — the deposit WINDOW (the transaction overlay's shell, 10B, laid
   out like the cash-out verify window): its bank slip card, the slip form,
   the attachments drop zone and thumbnails. Scoped to dp- / dpw- classes.
   Tokens only.
   ============================================================ */

/* Two sections on one page — what is ready, and the deposits made of it —
   each with a heading row that carries its own controls. */
.dp-section + .dp-section { margin-top: 24px; }
.dp-section-hd { display: flex; align-items: flex-end; justify-content: space-between; gap: 8px 14px; flex-wrap: wrap; margin: 0 0 10px; }
.dp-section-hd h2 { margin: 0; font-size: .88rem; font-weight: 600; color: var(--cal-text-emphasis); }
.dp-section-hd p { margin: 2px 0 0; font-size: .76rem; color: var(--cal-text-subtle); }
.dp-section-tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* The Open | All toggle and the range stand in a heading row, not over a
   chart, so they lose the bottom margin they carry elsewhere. */
.dp-section-tools .seg.seg-sm,
.dp-section-tools .range-bar { margin: 0; }

/* THE TICK COLUMN. As narrow as the box it holds, and the box in the brand
   ink so a ticked row reads as chosen. The whole row toggles its box — a
   16px square is a small target for somebody going down a list of bags. */
.dp-ready-table th.dp-check,
.dp-ready-table td.dp-check { width: 1%; padding-right: 0; }
.dp-check input[type="checkbox"] { width: 16px; height: 16px; margin: 0; vertical-align: middle; accent-color: var(--cal-brand); cursor: pointer; }
.dp-ready-table tbody tr[data-pick-row] { cursor: pointer; }
/* An unticked cash out stays on the list — it is still ready, only not in
   this deposit — so it steps back rather than disappearing. */
.dp-ready-table tbody tr.is-off td:not(.dp-check) { color: var(--cal-text-muted); }
.dp-ready-table tbody tr.is-off td .badge { opacity: .6; }

/* "I already have the slip": folded shut, because the slip is usually a
   morning away; the summary is a quiet link like the other folds, and the
   fields open on a panel of their own so they read as one optional group. */
.dp-fold { margin: 12px 0 0; }
.dp-fold > summary { padding: 4px 0; cursor: pointer; user-select: none; font-size: .78rem; font-weight: 500; color: var(--cal-text-subtle); }
.dp-fold > summary:hover { color: var(--cal-text-emphasis); text-decoration: underline; }
.dp-fold > summary:focus-visible { outline: 2px solid var(--cal-border-emphasis); outline-offset: 2px; }
.dp-fold[open] > summary { margin-bottom: 6px; }
.dp-fold-body { padding: 12px 12px 2px; background: var(--cal-bg-muted); border: 1px solid var(--cal-border-subtle); }
.dp-fold-body .dc-variance-row { background: var(--cal-bg); }
.dp-note-row { margin: 12px 0 0; max-width: 560px; }

/* THE BAR: the count and the live total on the left — the figure that
   should reach the bank — and the page's one solid button on the right.
   A WARM SURFACE: the pearl beige tint in a beige hairline, still on the
   rest rung, so the total about to leave for the bank reads as the page's
   summary rather than one more white card. Its inks were measured on the
   tint (the caption --cal-text-subtle 5.25:1, the total 15.11:1); the
   muted step is re-pointed for anything a later edit puts in it. */
.dp-ready-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 10px 14px; flex-wrap: wrap;
  margin-top: 12px; padding: 10px 14px;
  background: var(--nbc-beige-tint); border: 1px solid var(--nbc-beige);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-card);
  --cal-text-muted: var(--on-beige-muted);
}
.dp-ready-sum { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
.dp-ready-sum span { font-size: .78rem; color: var(--cal-text-subtle); }
.dp-ready-sum strong { font-size: 1.1rem; font-weight: 700; color: var(--cal-text-emphasis); font-variant-numeric: tabular-nums; }
.dp-ready-hint { margin: 6px 0 0; }

/* A deposit row opens the deposit: pressable by keyboard as well as mouse. */
.dp-list-table tbody tr[data-id]:focus-visible { outline: 2px solid var(--cal-border-emphasis); outline-offset: -2px; }
/* Head office confirms from the list: the slip number is read against the
   bank statement (tabular, never wrapped), and the inline Confirm sits in a
   narrow last column that does not stretch the table. */
.dp-list-table .dp-slip-no { white-space: nowrap; font-variant-numeric: tabular-nums; }
.dp-list-table .dp-act-col { width: 1%; white-space: nowrap; text-align: right; }

/* THE DEPOSIT, OPENED — as a modal. The deposit now opens in its window
   (dpw-, below); the dp-modal, dp-stage-note and dp-review rules here drew
   the modal it replaced and are left in place, unused, rather than removed
   under anything that still names them. The stage rides beside the title, where the day's stage rides on
   End of Day; the trail is its own card under both columns, the way a
   close's is. */
.dp-modal .modal-head h2 .badge { margin-left: 6px; vertical-align: 2px; }
.dp-modal .dc-timeline { margin-top: 16px; padding: 12px 14px; background: var(--cal-bg-muted); border: 1px solid var(--cal-border-subtle); }
.dp-stage-note { margin: 0 0 10px; font-size: .8rem; line-height: 1.5; color: var(--cal-text-subtle); }
/* Head office's check, ruled off from the record's facts below it: the
   facts are the terminal's word, the check is head office's. */
.dp-review { margin: 0 0 12px; padding: 0 0 12px; border-bottom: 1px solid var(--cal-border-subtle); }
.dp-review h3 { margin: 0 0 4px; font-size: .8rem; font-weight: 600; color: var(--cal-text-emphasis); }
.dp-review .form-hint { margin: 0; }
.dp-review .form-row { margin: 8px 0; }

@media (max-width: 640px) {
  .dp-ready-bar { padding: 10px; }
  .dp-ready-bar .btn { width: 100%; justify-content: center; }
  .dp-note-row { max-width: none; }
  .dp-fold-body { padding: 10px 10px 2px; }
}

/* THE DEPOSIT WINDOW (openDeposit in public/views/deposits.js). It is the
   transaction overlay's own shell (10B, App.FundsOverlay.shell) — head,
   close button, ruled foot with its status line, backdrop and motion are
   all 10B's — laid out like the cash-out verify window (the cv- rules):
   the three figures as one row, the record on the left, the evidence on
   the right in a card that scrolls on its own. Only what a deposit adds is
   here: the width, the stage badge beside the title, the bank slip card
   and its form, the attachments drop zone and thumbnails, the quiet note.
   Scoped to dpw-. Tokens only. */

/* The record and the slip card side by side need the verify window's room
   and a little more: the card carries a two-up form. */
.overlay.dpw-overlay { max-width: 920px; }
/* The stage rides beside the reference, where it rode on the modal. */
.dpw-overlay .overlay-title .badge { margin-left: 8px; vertical-align: 3px; }
/* The body's floor matches its head: 10B's 4px suits a form whose last
   field has its own margin, and the evidence card has none. */
.dpw-overlay .overlay-body.dpw-body { padding-bottom: 14px; }

/* THE FIGURES — Expected · Slip amount · Difference, the warm .dc-fig boxes
   three across on any width, the verify window's row. */
.dc-figs.dpw-figs { grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0 0 14px; }
.dpw-figs .dc-fig strong { overflow: hidden; text-overflow: ellipsis; }

/* THE COLUMNS — .detail-cols (16). Stacked on a phone (its own 640px
   break) they need the row gap .detail-cols never had to give. */
.detail-cols.dpw-cols { row-gap: 14px; }
.dpw-main, .dpw-side { min-width: 0; }

/* LEFT — the record. Head office's reason leads it when it was sent back;
   the items take the End of Day receipt (.sc-sheet); the trail and the
   check are ruled off below, each its own part of the story. */
.dpw-query { font-size: .8rem; }
.dpw-hd { margin-top: 14px; }
.dpw-hd small { margin-left: 6px; font-size: .74rem; font-weight: 400; color: var(--cal-text-muted); }
.dpw-main .sc-sheet.dpw-items { margin-bottom: 4px; }
.dpw-main .detail-list.dpw-note { margin-top: 4px; }
.dpw-timeline { padding-top: 12px; border-top: 1px solid var(--cal-border-subtle); }
.dpw-timeline li.dc-tl-submitted .dc-tl-dot { background: var(--cal-brand); }
/* The reason a deposit came back is the sentence somebody acts on. */
.dpw-timeline .dpw-tl-reason { color: var(--cal-error); font-weight: 500; }
.dpw-check .form-hint { margin: 0 0 8px; }
.dpw-check .form-row { margin-bottom: 0; }

/* RIGHT — THE EVIDENCE CARD: the bank slip and its photos, a faint fill
   inside a hairline, no shadow — the verify window's evidence card, so the
   two windows read alike. Its two parts are ruled apart. */
.dpw-side { background: var(--cal-bg-muted); border: 1px solid var(--cal-border-subtle); padding: 10px 12px; }
.dpw-side > .sc-col-hd:not(:first-child) { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--cal-border-subtle); }
.dpw-side > *:last-child { margin-bottom: 0; }
.dpw-empty { margin: 0; }

/* THE SLIP FORM — .form-grid two-up inside the card (Amount | Slip number,
   Bank | Deposited on, Carried by across), a little tighter than the page's
   because the card is half a window wide. The difference line sits on
   white so it reads as a result, not one more field. */
.dpw-lead { margin: 0 0 8px; }
.dpw-slip-form .form-grid { gap: 4px 12px; }
.dpw-slip-form .dc-variance-row { margin: 2px 0 10px; background: var(--cal-bg); }
.dpw-slip-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 12px; margin-top: 2px; }
.dpw-slip-actions .status.dpw-slip-status { flex: 1 1 160px; min-width: 0; min-height: 0; margin: 0; line-height: 1.4; }
.dpw-slip-actions .status.dpw-slip-status:empty { display: none; }
.dpw-slip-read { margin-top: 0; }

/* THE DROP ZONE — a dashed hairline around a white box, the words and the
   two ways in (choose files, take a photo). It tints brand on hover and
   while a file is dragged over it, so the hand knows where to let go; a
   deposit holding its six steps back to the quiet grey and stops answering. */
.dpw-drop {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 16px 12px;
  text-align: center;
  background: var(--cal-bg);
  border: 1px dashed var(--cal-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.dpw-drop:hover,
.dpw-drop.is-over { background: var(--badge-role-bg); border-color: var(--cal-brand); }
.dpw-drop-ic {
  display: grid; place-items: center;
  width: 34px; height: 34px;
  border-radius: var(--radius-md);
  background: var(--badge-role-bg); color: var(--cal-brand);
}
.dpw-drop:hover .dpw-drop-ic,
.dpw-drop.is-over .dpw-drop-ic { background: var(--cal-bg); }
.dpw-drop-ic svg { display: block; width: 18px; height: 18px; }
.dpw-drop-text { margin: 0; font-size: .8rem; line-height: 1.45; color: var(--cal-text-subtle); }
.dpw-drop-text .btn-link { color: var(--cal-brand); font-weight: 600; }
.dpw-drop.is-full,
.dpw-drop.is-full:hover { cursor: default; background: var(--cal-bg-subtle); border-color: var(--cal-border-subtle); }
.dpw-drop.is-full .dpw-drop-ic { background: var(--cal-bg); color: var(--cal-text-muted); }
.dpw-drop.is-full .btn-link { color: var(--cal-text-muted); text-decoration: none; cursor: default; }

/* The one quiet line under the zone that says nothing is kept yet, and the
   line that says what an add left out or that the deposit is full. */
.dpw-attach-note { margin: 6px 0 0; font-size: .72rem; line-height: 1.4; color: var(--cal-text-muted); }
.dpw-attach-msg { margin: 8px 0 0; font-size: .76rem; line-height: 1.4; color: var(--cal-text-subtle); }
.dpw-attach-msg:empty { display: none; }

/* THE THUMBNAILS — a grid of small cards: the photo cropped to fill, or a
   PDF tag, with the name and size under it and a remove button in the
   corner. [hidden] is restated because the grid's display would beat it. */
.dpw-thumbs {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 8px;
  margin: 10px 0 0; padding: 0;
  list-style: none;
}
.dpw-thumbs[hidden] { display: none; }
.dpw-thumb {
  position: relative; min-width: 0;
  background: var(--cal-bg);
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-md);
  overflow: hidden;
}
.dpw-thumb-media { display: grid; place-items: center; aspect-ratio: 4 / 3; background: var(--cal-bg-subtle); }
.dpw-thumb-media img { display: block; width: 100%; height: 100%; object-fit: cover; }
.dpw-pdf {
  padding: 4px 8px;
  font-family: var(--font-mono); font-size: .74rem; font-weight: 700; letter-spacing: .06em;
  color: var(--badge-role-text); background: var(--badge-role-bg);
  border: 1px solid var(--badge-role-border); border-radius: var(--radius-sm);
}
.dpw-thumb-meta { display: flex; flex-direction: column; gap: 1px; min-width: 0; padding: 5px 7px 6px; }
.dpw-thumb-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .72rem; font-weight: 500; color: var(--cal-text-emphasis); }
.dpw-thumb-meta small { font-size: .66rem; color: var(--cal-text-muted); font-variant-numeric: tabular-nums; }
/* Remove: a square target on the photo's corner, white so it reads on any
   picture; red only under the pointer, where it is about to act. */
.dpw-thumb-remove {
  position: absolute; top: 4px; right: 4px;
  display: grid; place-items: center;
  width: 26px; height: 26px; padding: 0;
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-sm);
  background: var(--cal-bg); color: var(--cal-text-subtle);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.dpw-thumb-remove:hover { color: var(--cal-error); background: var(--cal-error-bg); border-color: var(--cal-error-border); }
.dpw-thumb-remove:focus-visible {
  outline: none;
  border-color: var(--cal-border-emphasis);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--cal-border-emphasis) 30%, transparent);
}
.dpw-thumb-remove svg { display: block; width: 14px; height: 14px; }

/* EACH COLUMN SCROLLS ON ITS OWN, and the window does not, wherever there
   is room for two columns and a screen tall enough to scroll inside — the
   verify window's rule: the body becomes a column of the figures over the
   two columns, which take what height is left and scroll within it, so a
   long trail never pushes the slip card or the foot off the screen. The
   left column keeps 4px of air each side so the review note's 3px focus
   ring is not cut by its own scroll box. Below either bound the body
   scrolls as 10B made it. */
@media (min-width: 641px) and (min-height: 560px) {
  .dpw-overlay .overlay-body.dpw-body { display: flex; flex-direction: column; }
  .dpw-body > .dpw-figs { flex: none; }
  .dpw-body > .dpw-cols {
    flex: 1 1 auto; min-height: 0;
    grid-template-rows: minmax(0, 1fr); align-items: stretch;
  }
  .dpw-cols > .dpw-main, .dpw-cols > .dpw-side { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
  .dpw-cols > .dpw-main { margin: 0 -4px; padding: 0 4px 2px; }
}

/* On a phone the columns stack, and for whoever is working the slip the
   card comes FIRST: they are standing at the bank with the slip in one
   hand and the phone in the other, and the record is what they already
   know. */
@media (max-width: 640px) {
  .dpw-cols.is-editable > .dpw-side { order: -1; }
  .dpw-drop { padding: 14px 10px; }
}

@media (prefers-reduced-motion: reduce) {
  .dpw-drop, .dpw-thumb-remove { transition: none; }
}

/* ============================================================
   18H. NETWORK — stops, lines, fares (public/components/network-*.js,
   fare-tables.js)
   The Stops, Lines and Fares tabs of Route Settings (plan NT-1B, NT-3B —
   the Fares tab and its grid are THE FARES TAB at the end of this
   section). The lists are 18F's reference tables — .rs-table, .rs-row,
   .rs-cell, .rs-retired, the three-dot .rs-menu — with a stop's or a
   line's code in the handle pill (.ref-pill, 11). The windows are the
   transaction overlay's shell (10B, App.FundsOverlay.shell), laid out like
   the verify and deposit windows: the warm .dc-fig row, .detail-cols with
   the facts on the left and the evidence card on the right, each column
   scrolling on its own. What is here is only what the network adds: the
   Lines column, the form's field-level error line and switch row, and the
   line editor's ordered stop list — handle, number, stop, kind, the km
   with its est. tag, the arrows and remove — with its drag and drop
   markers and the Add stop menu that rides above the window. Scoped to
   nw-. Tokens only.
   ============================================================ */

/* THE STOPS LIST. The Lines column is the one cell that may wrap — a stop
   on Western and Southern names both — so the row grows rather than the
   table widening past the screen. A retired line is named, muted. */
.rs-table .nw-col-lines { white-space: normal; min-width: 160px; }
.nw-on { display: inline-block; margin-right: 10px; white-space: nowrap; }
.nw-on small { font-size: .72rem; color: var(--cal-text-muted); font-variant-numeric: tabular-nums; }
.nw-on.is-retired { color: var(--cal-text-muted); }

/* Under ~1000px the lists scroll inside their card rather than squeeze —
   the Runs table's rule (18F). */
@media (max-width: 1000px) {
  .nw-table-stops { min-width: 860px; }
  .nw-table-lines { min-width: 760px; }
}

/* THE WINDOWS. A stop's facts beside the lines that list it need the
   verify window's room; the line editor's stop list needs more — its rows
   carry a km field and three buttons. */
.overlay.nw-overlay { max-width: 820px; }
.overlay.nw-line-overlay { max-width: 1000px; }
/* The body's floor matches its head: 10B's 4px suits a form whose last
   field has its own margin, and the evidence card has none. */
.nw-overlay .overlay-body.nw-body,
.nw-line-overlay .overlay-body.nw-body { padding-bottom: 14px; }

/* THE FIGURES — three warm boxes across on any width, the verify window's
   row; a long "Corozal → Belize City" is cut in its box and read whole in
   the title. */
.dc-figs.nw-figs { grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0 0 14px; }
.nw-figs .dc-fig strong { overflow: hidden; text-overflow: ellipsis; }

/* THE COLUMNS — .detail-cols (16). The line editor gives its stop list the
   wider track; stacked on a phone (the .detail-cols 640px break). */
.detail-cols.nw-cols { row-gap: 14px; }
.nw-main, .nw-side { min-width: 0; }
@media (min-width: 641px) {
  .detail-cols.nw-line-cols { grid-template-columns: minmax(0, 4fr) minmax(0, 7fr); }
}

/* THE EVIDENCE CARD — the lines a stop is on, a line's stops: a faint fill
   inside a hairline, no shadow, the verify window's card. */
.nw-side { background: var(--cal-bg-muted); border: 1px solid var(--cal-border-subtle); padding: 10px 12px; }
.nw-side .detail-list { margin-top: 0; }
.nw-side > *:last-child { margin-bottom: 0; }
.nw-form + .detail-list { margin-top: 6px; }

/* EACH COLUMN SCROLLS ON ITS OWN, and the window does not, wherever there
   is room for two columns and a screen tall enough to scroll inside — the
   verify window's rule, so a sixteen-stop line never pushes the settings or
   the foot off the screen. The left column keeps 4px of air each side so a
   field's 3px focus ring is not cut by its own scroll box. */
@media (min-width: 641px) and (min-height: 560px) {
  .nw-overlay .overlay-body.nw-body,
  .nw-line-overlay .overlay-body.nw-body { display: flex; flex-direction: column; }
  .nw-body > .nw-figs { flex: none; }
  .nw-body > .nw-cols {
    flex: 1 1 auto; min-height: 0;
    grid-template-rows: minmax(0, 1fr); align-items: stretch;
  }
  .nw-cols > .nw-main, .nw-cols > .nw-side { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
  .nw-cols > .nw-main { margin: 0 -4px; padding: 0 4px 2px; }
}

/* THE FORMS — the stop form (.form-grid) and the line's settings. A
   segmented control keeps its own width rather than stretching across the
   grid row; the code is set in the handle's mono so SPB reads as a code. */
.nw-form .form-row > .seg { justify-self: start; }
.nw-code-inp { font-family: var(--font-mono); text-transform: uppercase; letter-spacing: .02em; }
.nw-form .nw-num { max-width: 140px; }
/* "Passengers can change buses here": the switch and its sentence as one
   target, set as a sentence rather than as a field label. */
.nw-form .form-row > label.nw-switch-row {
  display: flex; align-items: center; gap: 10px;
  font-size: .82rem; font-weight: 500; color: var(--cal-text-emphasis);
  cursor: pointer;
}
.nw-switch-row .switch { flex: none; }
.nw-switch-row .switch input:focus-visible + span { box-shadow: 0 0 0 2px var(--cal-bg), 0 0 0 4px var(--cal-brand); }

/* THE SERVER'S SENTENCE ON ITS FIELD. The line under the control, in the
   error red (9.11:1 on white), and the control's own edge in the same red
   — the filled field's transparent border (5) is what it replaces. Empty,
   it takes no line. */
.nw-field-err { margin: 0; font-size: var(--fs-small); line-height: 1.4; color: var(--cal-error); }
.nw-field-err:empty { display: none; }
.overlay .inp[aria-invalid="true"],
.overlay .seg[aria-invalid="true"] { border-color: var(--cal-error); }

/* THE LINE'S STOPS — a white list inside the evidence card, one row per
   stop in line order: handle · number · stop · kind · km (+ est.) ·
   up / down / remove. Fixed tracks, so every row's km sits in one column
   whether or not its est. tag is showing (the tag hides by visibility, not
   display, for the same reason). */
.nw-list-hd { display: flex; align-items: center; justify-content: space-between; gap: 6px 10px; flex-wrap: wrap; }
.nw-list-hd .sc-col-hd { margin: 0; }
.nw-list-hd .sc-col-hd small { margin-left: 6px; font-size: .74rem; font-weight: 400; color: var(--cal-text-muted); }
.nw-list-lead { margin: 2px 0 8px; }
.nw-stops {
  list-style: none; margin: 0; padding: 0;
  background: var(--cal-bg);
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-md);
}
.nw-stop {
  display: grid;
  grid-template-columns: 22px 22px minmax(0, 1fr) 70px 132px 94px;
  align-items: center; column-gap: 6px;
  padding: 5px 8px 5px 4px;
  border-top: 1px solid var(--cal-border-muted);
}
.nw-stop:first-child { border-top: 0; }
.nw-stop.is-read { grid-template-columns: 22px minmax(0, 1fr) 70px 110px; padding-left: 8px; }
.nw-stop-empty { padding: 14px 12px; font-size: .8rem; line-height: 1.45; color: var(--cal-text-subtle); text-align: center; }

/* The handle: a quiet target that says "grab" under the pointer. Only it
   makes the row draggable, so the km field in the row selects like any
   other field. */
.nw-grip {
  display: grid; place-items: center;
  width: 22px; height: 28px; padding: 0;
  border: 0; border-radius: var(--radius-sm);
  background: none; color: var(--cal-text-muted);
  cursor: grab;
}
.nw-grip:hover { background: var(--cal-bg-subtle); color: var(--cal-text-emphasis); }
.nw-grip:active { cursor: grabbing; }
.nw-grip svg { width: 14px; height: 14px; }

.nw-seq { font-size: .72rem; font-weight: 600; color: var(--cal-text-muted); text-align: right; font-variant-numeric: tabular-nums; }
.nw-stop-name strong, .nw-stop-name small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nw-kind { min-width: 0; }

/* The km: a small filled field, right-aligned tabular figures, "km" after
   it and the est. tag after that. The field takes the small control height
   so a row stays a list row, not a form. */
.nw-km { display: inline-flex; align-items: center; gap: 5px; font-size: .74rem; color: var(--cal-text-muted); }
.nw-km .inp.nw-km-inp { width: 70px; height: var(--ctl-h-sm); padding: 0 8px; text-align: right; font-variant-numeric: tabular-nums; }
.nw-km-read { min-width: 40px; text-align: right; font-size: .8125rem; color: var(--cal-text-emphasis); font-variant-numeric: tabular-nums; }
.nw-est.is-off { visibility: hidden; }

/* Up, down and remove: the keyboard's and the touch screen's way to do
   what the handle does with a mouse. The up arrow is the down chevron
   turned, so the icon set stays the one in App.ICONS. */
.nw-row-btns { display: inline-flex; justify-content: flex-end; gap: 3px; }
.icon-btn.nw-mini { width: 28px; height: 28px; }
.icon-btn.nw-mini svg { width: 14px; height: 14px; }
.nw-up svg { transform: rotate(180deg); }

/* A row the km hint or the server has something to say about: the error
   surface, and the sentence under the stop across the remaining tracks. */
.nw-stop.is-invalid { background: var(--cal-error-bg); }
.nw-row-err { grid-column: 3 / -1; margin: 2px 0 0; font-size: var(--fs-small); line-height: 1.4; color: var(--cal-error); }
.nw-stop.is-read .nw-row-err { grid-column: 2 / -1; }
.nw-row-err:empty { display: none; }

/* DRAG AND DROP. The row in the hand fades; a 2px brand rule on the edge
   of the row under it says where it will land. Inset shadows, so the
   marker never changes a row's height. */
.nw-stop.is-dragging { opacity: .45; }
.nw-stop.is-drop-before { box-shadow: inset 0 2px 0 var(--cal-brand); }
.nw-stop.is-drop-after { box-shadow: inset 0 -2px 0 var(--cal-brand); }

/* ADD STOP — the search field and the button that opens the menu, and the
   line that says where a stop will go. */
.nw-add { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
.nw-add .inp { flex: 1 1 auto; min-width: 0; }
.nw-add-where { margin: 4px 0 0; }
/* The menu is App.dropdown's (14), appended to <body> at the floating
   layer; opened from inside a window it has to ride above the window's
   backdrop (10B, z-index 100), or it opens underneath it. */
.dropdown-menu.nw-pick-menu { z-index: 110; }

/* A phone: the number and the kind give way to the stop's name, and the
   sentence of a row moves under the name. */
@media (max-width: 640px) {
  .nw-stop { grid-template-columns: 22px minmax(0, 1fr) 124px 94px; }
  .nw-stop .nw-seq, .nw-stop .nw-kind { display: none; }
  .nw-stop .nw-row-err { grid-column: 2 / -1; }
  .nw-stop.is-read { grid-template-columns: minmax(0, 1fr) 110px; }
  .nw-stop.is-read .nw-row-err { grid-column: 1 / -1; }
}

/* THE FARES TAB (plan NT-3B, public/components/fare-tables.js). One fare
   table at a time: the picker bar, the rules as the warm figures, the
   stretches with no fare yet, the stretch fares and the overrides as two
   cards, and the grid — every stop by every stop on one line — scrolling
   INSIDE its own box with its head row and stop column pinned, so the page
   never scrolls sideways however many stops a line has. */
.nw-fr-bar { display: flex; align-items: center; justify-content: space-between; gap: 10px 12px; flex-wrap: wrap; margin: 0 0 12px; }
.nw-fr-pickwrap { display: flex; align-items: center; gap: 8px 10px; flex-wrap: wrap; min-width: 0; }
.nw-fr-pick-l { font-size: .76rem; font-weight: 600; color: var(--cal-text-emphasis); }
.inp.nw-fr-pick { width: auto; min-width: 260px; max-width: 100%; height: var(--ctl-h-row); padding: 0 10px; font-size: .8rem; }
.nw-fr-tags { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.nw-fr-tags:empty { display: none; }
.nw-fr-bar-end { display: flex; align-items: center; gap: 8px; }
.nw-fr-bar-end:empty { display: none; }

/* A locked or never-used table says so on one line, with its way out —
   New fare table — at the end of it. */
.rs-note.nw-fr-note-bar { align-items: center; margin: 0 0 12px; }
.rs-note.nw-fr-note-bar > span { flex: 1 1 auto; min-width: 0; }
.rs-note.nw-fr-note-bar .btn { flex: none; }
.rs-note.nw-fr-note-bar > svg { margin-top: 0; }

.dc-figs.nw-fr-figs { margin: 0 0 12px; }

/* The stretches with no fare yet: the warn trio (11), one line each. Not
   an error — the counter still sells, the seller types the price. */
.nw-fr-warns { list-style: none; display: grid; gap: 4px; margin: 0 0 12px; padding: 8px 10px; border: 1px solid var(--badge-warn-border); border-radius: var(--radius-md); background: var(--badge-warn-bg); }
.nw-fr-warns:empty { display: none; }
.nw-fr-warns li { display: flex; align-items: flex-start; gap: 8px; font-size: .78rem; line-height: 1.45; color: var(--badge-warn-text); }
.nw-fr-warns svg { width: 14px; height: 14px; flex: none; margin-top: 2px; }

/* THE TWO CARDS — the stretch fares take the wider track, the overrides
   the narrower; stacked under 1100px, where side by side would squeeze
   both. A card is a hairline on white: a resting surface, no shadow. */
.nw-fr-cols { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: 12px; align-items: start; margin: 0 0 12px; }
@media (max-width: 1100px) {
  .nw-fr-cols { grid-template-columns: minmax(0, 1fr); }
}
.nw-fr-card { min-width: 0; padding: 12px 14px 14px; border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-lg); background: var(--cal-bg); }
.nw-fr-card-hd { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px 14px; flex-wrap: wrap; }
.nw-fr-card-title { flex: 1 1 320px; min-width: 0; }
.nw-fr-card-hd .sc-col-hd { margin: 0; }
.nw-fr-card .sc-col-hd small { margin-left: 6px; font-size: .74rem; font-weight: 400; color: var(--cal-text-muted); }
.nw-fr-lead { margin: 2px 0 10px; line-height: 1.45; }
.nw-fr-empty { margin: 0; padding: 14px 12px; border: 1px dashed var(--cal-border-subtle); border-radius: var(--radius-md); font-size: .8rem; line-height: 1.45; color: var(--cal-text-subtle); text-align: center; }
.nw-fr-skel { display: grid; gap: 10px; padding: 6px 0 14px; }
.nw-fr-skel .skeleton { height: 18px; }

/* The cards' tables: 11's data-table inside a hairline box rather than a
   second floating card, scrolling sideways inside it on a narrow screen.
   Each line's name is a row of its own over its stretches. */
.nw-fr-tablebox { overflow-x: auto; border: 1px solid var(--cal-border-muted); border-radius: var(--radius-md); }
.nw-fr-table thead th { position: static; padding: 7px 12px; }
.nw-fr-table tbody td { padding: 8px 12px; }
.nw-fr-table tbody tr.nw-fr-group:hover { background: none; }
.nw-fr-table tr.nw-fr-group th {
  padding: 6px 12px;
  background: var(--cal-bg-muted);
  border-bottom: 1px solid var(--cal-border-muted);
  text-align: left; font-size: var(--fs-micro); font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--cal-text-muted);
}
.nw-fr-table .nw-est { margin-left: 4px; }
.nw-fr-muted { font-size: .76rem; color: var(--cal-text-muted); }
.nw-fr-table .nw-fr-notecell { white-space: normal; min-width: 120px; color: var(--cal-text-subtle); }

/* THE GRID — one line, one class, from the server. */
.nw-fr-grid-tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.nw-fr-grid-tools .seg.seg-sm { margin: 0; }
.nw-grid-legend { list-style: none; display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 0 0 8px; padding: 0; font-size: .74rem; color: var(--cal-text-subtle); }
.nw-grid-legend li { display: inline-flex; align-items: center; gap: 6px; }
/* A key is a sample cell: the same shading the grid uses, from the same
   classes, so the legend cannot drift from what it explains. Before the
   cell rules, which set the shade over its plain white. */
.nw-key { display: inline-grid; place-items: center; min-width: 44px; height: 22px; padding: 0 6px; border: 1px solid var(--cal-border-muted); border-radius: var(--radius-sm); background: var(--cal-bg); font-size: .7rem; font-variant-numeric: tabular-nums; color: var(--cal-text); }

.nw-grid-wrap {
  max-width: 100%;
  max-height: clamp(300px, calc(100vh - 300px), 720px);
  overflow: auto; overscroll-behavior: contain;
  border: 1px solid var(--cal-border-subtle); border-radius: var(--radius-md);
  background: var(--cal-bg);
}
/* separate, not collapse: a collapsed border is drawn by the table, and a
   pinned cell would scroll off without its rules. */
.nw-grid { border-collapse: separate; border-spacing: 0; font-size: .76rem; font-variant-numeric: tabular-nums; }
.nw-grid.is-busy { opacity: .55; }
.nw-grid th, .nw-grid td { border-right: 1px solid var(--cal-border-muted); border-bottom: 1px solid var(--cal-border-muted); }
.nw-grid thead th {
  position: sticky; top: 0; z-index: 2;
  min-width: 62px; padding: 7px 6px;
  background: var(--cal-bg-muted);
  font-size: .68rem; font-weight: 600; letter-spacing: .02em; color: var(--cal-text-subtle);
  text-align: center; white-space: nowrap;
}
.nw-grid thead th abbr { text-decoration: none; cursor: help; }
.nw-grid thead th.is-terminal { font-weight: 700; color: var(--cal-text-emphasis); }
.nw-grid thead th.nw-grid-corner { left: 0; z-index: 3; min-width: 160px; text-align: left; font-weight: 500; color: var(--cal-text-muted); }
.nw-grid tbody th {
  position: sticky; left: 0; z-index: 1;
  min-width: 160px; padding: 4px 10px;
  background: var(--cal-bg-muted);
  text-align: left; font-weight: 500; white-space: nowrap; color: var(--cal-text);
}
.nw-grid tbody th span { display: block; font-size: .78rem; }
.nw-grid tbody th small { display: block; font-size: .66rem; font-weight: 400; color: var(--cal-text-muted); }
.nw-grid tbody th.is-terminal span { font-weight: 700; color: var(--cal-text-emphasis); }

/* The cells, shaded by where the fare came from: an override in the
   role blue (7.06:1), the minimum muted, no fare in the error trio
   (7.92:1), a terminal-to-terminal stretch fare in bold. Hover and focus
   ring the cell inside its own edge, so the shade underneath still reads. */
.nw-cell { height: 36px; padding: 0 8px; text-align: right; white-space: nowrap; color: var(--cal-text); background: var(--cal-bg); cursor: default; }
.nw-cell:hover { box-shadow: inset 0 0 0 2px var(--cal-border); }
.nw-cell:focus { outline: none; box-shadow: inset 0 0 0 2px var(--cal-brand); }
.nw-cell-self { background: var(--cal-bg-subtle); color: var(--cal-text-muted); text-align: center; }
.nw-cell-anchor { font-weight: 700; color: var(--cal-text-emphasis); }
.nw-cell-override { background: var(--badge-role-bg); color: var(--badge-role-text); font-weight: 700; }
.nw-cell-min { background: var(--cal-bg-muted); color: var(--cal-text-muted); }
.nw-cell-missing { background: var(--cal-error-bg); color: var(--cal-error); text-align: center; }
.nw-grid-why { margin: 8px 0 0; min-height: 1.45em; font-size: .78rem; line-height: 1.45; color: var(--cal-text-subtle); }

/* THE FARE EDITOR — the line editor's window (nw-overlay: its column
   scroll, 10B's shell), wider, with the date, note and rules on the left
   and the stretch fares and overrides in the card on the right. */
.overlay.nw-fare-overlay { max-width: 1080px; }
@media (min-width: 641px) {
  .detail-cols.nw-fare-cols { grid-template-columns: minmax(0, 4fr) minmax(0, 7fr); }
}
.nw-fare-overlay .nw-body > .nw-fr-warns { flex: none; margin: 12px 0 0; }
.nw-fr-rules-hd { margin: 16px 0 10px; padding-top: 12px; border-top: 1px solid var(--cal-border-subtle); }
.nw-fr-unit { display: inline-flex; align-items: center; gap: 6px; font-size: .82rem; color: var(--cal-text-subtle); }
.nw-fr-unit .inp { width: 110px; text-align: right; font-variant-numeric: tabular-nums; }

/* A stretch row: the stretch (its km under it) · Regular · Express ·
   clear, under one column head that names the two fares once. */
.nw-fr-colhd, .nw-fr-srow { display: grid; grid-template-columns: minmax(0, 1fr) 92px 92px 28px; align-items: center; column-gap: 8px; }
.nw-fr-colhd { padding: 0 9px 4px; font-size: var(--fs-micro); font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--cal-text-muted); }
.nw-fr-colhd span:nth-child(2), .nw-fr-colhd span:nth-child(3) { text-align: right; }
.nw-fr-group-hd { display: flex; align-items: center; gap: 6px; margin: 10px 0 5px; font-size: .74rem; font-weight: 600; color: var(--cal-text-emphasis); }
.nw-fr-group-hd:first-child { margin-top: 0; }
.nw-fr-srow, .nw-fr-orow { padding: 6px 8px; border-top: 1px solid var(--cal-border-muted); }
.nw-fr-srow:first-child, .nw-fr-orow:first-child { border-top: 0; }
.nw-fr-sname strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nw-fr-sname small { display: flex; align-items: center; gap: 6px; min-width: 0; }
.inp.nw-fr-inp { width: 100%; min-width: 0; height: var(--ctl-h-sm); padding: 0 8px; text-align: right; font-variant-numeric: tabular-nums; }
.nw-fr-read { font-size: .8125rem; text-align: right; font-variant-numeric: tabular-nums; color: var(--cal-text-subtle); }
/* An entry no stretch uses: the warn tint until it is taken off. */
.nw-fr-srow.is-orphan { background: var(--badge-warn-bg); }
.nw-fr-srow.is-invalid, .nw-fr-orow.is-invalid { background: var(--cal-error-bg); }
.nw-fr-srow .nw-row-err, .nw-fr-orow .nw-row-err { grid-column: 1 / -1; }
.nw-fr-ov-hd { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--cal-border-subtle); }

/* An override row: the pair · class · fare · remove, its note under them. */
.nw-fr-orow { display: grid; grid-template-columns: minmax(0, 1fr) 104px 84px 28px; align-items: center; column-gap: 8px; row-gap: 6px; }
.inp.nw-fr-cls { height: var(--ctl-h-sm); padding: 0 6px; font-size: .78rem; }
.inp.nw-fr-notein { grid-column: 1 / -1; height: var(--ctl-h-sm); font-size: .78rem; }

/* Add override: the two stop searches and the button, wrapping in a
   narrow card; the menus are .nw-pick-menu, above the window. */
.nw-fr-add { display: flex; align-items: center; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.nw-fr-add .inp { flex: 1 1 140px; min-width: 0; }
.nw-fr-add .btn { flex: none; }

@media (max-width: 640px) {
  .inp.nw-fr-pick { width: 100%; min-width: 0; }
  .nw-fr-colhd, .nw-fr-srow { grid-template-columns: minmax(0, 1fr) 76px 76px 28px; column-gap: 6px; }
  .nw-fr-orow { grid-template-columns: minmax(0, 1fr) 92px 72px 28px; column-gap: 6px; }
}

/* ── Motion off, on request ──
   THE one block for the entrances and the lifts, and it is LAST on
   purpose: a rule inside a media query carries no extra specificity, so
   `.btn { transition: none }` beats `.btn { transition: … }` (section 4)
   only by coming after it — the same source-order trap the phone
   overrides for .page-hd and .page-hero fell into. It sits before section
   19, which is paper and has no motion to quiet. The modal and the sale's
   Complete sheet (.tk-done) are named here beside the overlay's own block
   in 10B: they arrive with the same 12px rise, and an operator who asked
   for reduced motion gets neither. App.modal closes on a timer, not on
   transitionend, so a transition of none cannot strand the backdrop.
   The dashboard's two entrances (.cascade, .fade-in) stop here too: with
   no animation there is no `backwards` fill, so every block and every
   row simply shows. The dashboard's own view fade is named in full
   because its selector (1,2,1) outranks #view.view-in (1,1,0) and would
   otherwise keep playing. */
@media (prefers-reduced-motion: reduce) {
  .rise, .cascade, .fade-in, #view.view-in, body[data-view="dashboard"] #view.view-in,
  .dropdown-menu, .avatar-menu, .cal-pop, .app-search-pop { animation: none; }
  .btn, .qa-tile, .qa-tile-go, .stat-tile, .cal-event, .choice-card, .tf-tile, .icon-btn,
  .modal-backdrop, .modal, .tk-done { transition: none; }
  .modal, .tk-done { transform: none; }
}

/* ============================================================
   19. PRINT
   Receipts print on the terminal's own printer, so the browser print
   path has to produce a clean page — no chrome, no tinted surfaces.
   This is the one place hex is allowed outside section 1: these are
   absolute paper and ink, deliberately NOT palette tokens.
   ============================================================ */
@media print {
  .sidebar,
  .sidebar-backdrop,
  .mobile-topbar,
  /* The global search is chrome, not content — in its fallback row and
     homed in a page's header alike. */
  .app-header,
  .app-search,
  .page-hd-actions,
  .row-actions,
  .toast,
  .dropdown-menu,
  .modal-backdrop,
  /* An open overlay is a form, not a document. Printing the page under it (a
     ledger, a receipt) must not print the panel or its scrim across the top. */
  .overlay-backdrop,
  /* Calendar chrome: navigation and a view toggle mean nothing on paper, and
     the now-line marks a moment that has already passed by the time the page
     comes off the printer. */
  .cal-nav,
  .cal-views,
  .cal-now,
  /* The Reports filter selects and its Clear button are controls, not
     findings — what the report was filtered to is already written into the
     page subtitle, which is the whole reason the view puts it there. Scoped
     to a .range-bar so it reaches only Reports: Funds' range bar holds
     pills and date inputs and no control of either kind, and the pills stay
     because the highlighted one still says which range this page is. */
  .range-bar .dash-scope,
  .range-bar .btn { display: none !important; }

  /* The hour grid has to print whole, not just the slice that was scrolled
     into view. Height auto, and the sticky head is a plain row on paper. */
  .cal-body { height: auto !important; overflow: visible !important; }
  .cal-week-head { position: static !important; }
  .cal-week, .cal-month, .cal-week-head, .cal-allday { break-inside: auto; }

  .app-main { margin-left: 0; padding: 0; min-height: 0; }
  .page-card { border: none; box-shadow: none; padding: 0; min-height: 0; border-radius: 0; }

  /* Flatten every tinted surface to paper and every colour to ink — a faint
     blue-cast grey costs toner and prints as mud. */
  html, body, .app-body, .page-card, .stat-tile, .table-wrap {
    background: #fff !important;
    color: #000 !important;
  }
  /* Forced colour only inherits into elements that declare none of their own,
     so it has to reach descendants explicitly or the palette prints in colour. */
  .page-card, .page-card * { color: #000 !important; }
  .data-table thead th,
  .data-table tbody td,
  .stat-label, .stat-value, .stat-note,
  .page-hd h1, .page-hd p { background: transparent !important; }
  .avatar { background: transparent !important; color: #000 !important; border: 1px solid #999 !important; }
  .table-wrap, .data-table thead th, .data-table tbody td { border-color: #999 !important; }
  .data-table tbody tr:hover { background: transparent !important; }
  .badge { border-color: #999 !important; color: #000 !important; background: transparent !important; }
  /* A solid blue block prints as a solid blue block and its white type
     disappears into it. Outline the event instead and let the ink be black. */
  .cal-event, .cal-chip { background: transparent !important; color: #000 !important; border: 1px solid #999 !important; }
  .cal-daynum.today, .cal-month-cell.today .cal-month-date, .cal-month-date.today {
    background: transparent !important; color: #000 !important; border: 1px solid #000 !important;
  }
  .cal-col.weekend, .cal-daycol-head.weekend, .cal-month-cell.other-month { background: transparent !important; }
  .cal-week, .cal-month, .cal-col, .cal-hour, .cal-daycol-head, .cal-month-cell, .cal-month-dow, .cal-allday, .cal-allday > * { border-color: #999 !important; }

  /* A panel's own edge is a 1.30:1 hairline, which is a white page printed on
     a white page — the surface that separates one section of the report from
     the next has to survive the trip to paper. */
  .panel { border-color: #999 !important; box-shadow: none !important; }

  /* ── Charts on paper ──
     The MARKS KEEP THEIR COLOUR. A calendar event is flattened to an outline
     because a solid blue block swallows its white type; a chart is the
     opposite case — black both series and the two lines become one
     indistinguishable tangle, and the reader loses the comparison the page
     exists for. --chart-1 and --chart-2 separate by LIGHTNESS as well as hue
     (8.18:1 vs 5.02:1 on white), so they hold up on a mono printer too, and
     that is exactly why the palette in section 1 is the one it is.
     The blanket `.page-card * { color: #000 }` above cannot reach them —
     SVG paint is fill and stroke, not color — so the marks survive on their
     own. This is the guard against a browser dropping them as "background
     graphics" instead. */
  .chart-svg {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  /* A chart split across a page break is two half charts and no chart. */
  .chart { break-inside: avoid; }

  a[href]::after { content: ""; }   /* no printed URLs — these are receipts, not articles */
  tr, .stat-tile, .panel { break-inside: avoid; }

  /* Thermal-style receipt block: 80mm roll, monospace, full bleed. */
  .receipt {
    width: 80mm;
    margin: 0;
    padding: 0;
    font-family: var(--font-mono);
    font-size: 11px;
    line-height: 1.35;
    color: #000 !important;
    background: #fff !important;
    white-space: pre-wrap;
  }
  @page { margin: 8mm; }
}

/* The receipt block is also rendered on screen (preview before printing). */
.receipt {
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.4;
  white-space: pre-wrap;
  color: var(--cal-text-emphasis);
}

/* ============================================================
   Idle lock + sign in again — Security (2026-09-27)
   The counter's idle lock (App.idleLock, public/app.js): after ten minutes
   without input the app is covered by the sign-in field itself — the login
   page's navy, its card, its field. The cover is OPAQUE from its first
   frame (only the card rises in), so no figure on the page underneath can
   be read; the page itself stays as it was under it.
   The conductor's "sign in again" banner (components/conductor-shell.js)
   rides inside the phone's sticky app bar and reads the .cx- tokens.
   ============================================================ */
.idle-lock {
  position: fixed;
  inset: 0;
  z-index: 1000;            /* over every other layer (the highest elsewhere is 120) */
  display: grid;
  place-items: center;
  padding: 32px 16px;
  overflow-y: auto;
  background: var(--auth-field);
}
.idle-lock .auth-col { max-width: 380px; }
.idle-lock-card { animation: idle-lock-rise var(--dur) var(--ease); }
.idle-lock-card .auth-title { overflow-wrap: anywhere; }
.idle-lock-other { margin: 14px 0 0; font-size: var(--fs-small); color: var(--cal-text-subtle); }
.idle-lock-other .link-inline { font-size: inherit; font-weight: 600; }
@keyframes idle-lock-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .idle-lock-card { animation: none; } }

.cx-reauth { background: var(--cx-danger-container); color: var(--cx-danger-ink); border-top: 1px solid var(--cx-outline); }
.cx-reauth[hidden] { display: none; }
.cx-reauth-in { display: flex; align-items: center; gap: 10px; max-width: var(--cx-column); margin: 0 auto; padding: 8px var(--cx-gutter); }
.cx-reauth-ic { flex: none; display: flex; }
.cx-reauth-ic svg { width: 18px; height: 18px; }
.cx-reauth-text { flex: 1; min-width: 0; font-size: 13px; line-height: 18px; font-weight: 500; }
.cx-reauth .cx-btn { flex: none; min-height: 34px; padding: 0 14px; font-size: 13px; }
.cx-reauth-lead { margin-top: 6px; color: var(--cx-ink-2); }
.cx-reauth-form { margin-top: 14px; }

/* ── Reports: the Exceptions pane (security 2026-09-27) ──
   public/views/reports.js mountExceptions. The Overview | Exceptions strip
   in the page header, and the compact two-line cells of its tables: the
   value, then a muted sub-line (a role, a stretch, a run). Short is the
   net-negative red, over the gold ink — both text-safe on white. */
.rpt-tabs { flex: none; }
.xc-body[aria-busy="true"] { opacity: .6; transition: opacity var(--dur-fast) var(--ease); }
.xc-hint { margin: -4px 0 12px; }
.xc-table td { vertical-align: top; }
.xc-main { color: var(--cal-text-emphasis); overflow-wrap: anywhere; }
.xc-sub { font-size: var(--fs-small); color: var(--cal-text-muted); overflow-wrap: anywhere; }
.xc-zero { color: var(--cal-text-muted); }
.xc-short { color: var(--ledger-net-neg); }
.xc-over { color: var(--nbc-gold-ink); }
.xc-person { color: var(--cal-brand); font-weight: 600; text-align: left; }
