/* OSA Production — shared application stylesheet.
 *
 * Extracted verbatim from the two inline <style> blocks in base.html, which had
 * grown to 351 lines and were re-shipped, uncacheable, on every page load.
 * Behaviour is unchanged: this <link> sits exactly where those blocks did --
 * and still before any page CSS, because pages inject their <style> inside
 * {% block content %} (in the BODY), not into <head>.
 *
 * THIS FILE IS NOW THE WHOLE CASCADE. Bootstrap 5.3.3 used to load ahead of it
 * and is gone: 227KB carrying 2,025 class definitions, of which the app used 38
 * -- utilities and a grid, in four templates. What it really provided was Reboot,
 * invisibly, and that was a liability rather than a service. Its reset set
 * `body { color: #212529 }`, off-palette, on every page, and beat app.css until
 * the D6 audit caught it; its `:root` shipped --bs-blue #0d6efd and --bs-red
 * #dc3545 into a codebase whose whole point is that OSA Red and OSA Blue are the
 * only red and blue; and the colour guard could not see any of it, because
 * SKIP_PARTS skips the file named bootstrap. The reset below replaces Reboot's
 * load-bearing parts and is byte-identical to osa-operations' -- which is the
 * point: the two sites now differ in colour, not in architecture.
 *
 * Linked via static_v() so ?v=<mtime> busts the cache on deploy. Inline CSS was
 * self-busting (it shipped with the HTML); a cacheable stylesheet is not, so the
 * versioned URL is load-bearing, not decorative.
 *
 * The :root token block here is the home for the design-system tokens. The three
 * scales are shared with osa-operations and must not be forked -- see
 * osa-operations/docs/STYLE_GUIDE.md.
 */
        :root {
            /* ══ SHARED DESIGN SYSTEM ══ byte-identical to osa-operations'
               app.css. These three scales are the "one product family" part:
               Production and Operations differ in COLOUR, not in geometry or
               type. Do not fork these values here -- the canonical guide is
               osa-operations/docs/STYLE_GUIDE.md; change them there first.

               Always reference the token, never the literal: it keeps a later
               px->rem move a five-line change instead of another app-wide sweep. */

            /* Type scale — five steps. Production's dominant sizes (13px x51,
               11px x40, 15px, 22px) were ALREADY these values; the sprawl was
               12/14/10 plus one-off half-pixel sizes around them. */
            --fs-sm:   11px;  /* dense — table cells, meta, badges, uppercase labels */
            --fs-base: 13px;  /* body, inputs */
            --fs-lg:   15px;  /* subheads */
            --fs-xl:   18px;  /* card / section headings */
            --fs-2xl:  22px;  /* page titles */

            /* Spacing — 4px base. Bootstrap 5's default spacers are 4/8/16/24/48,
               so this is not a new convention here; --sp-1/-4/-7 are the additions
               that Operations' dense, table-heavy UI needed. */
            --sp-1:  2px;
            --sp-2:  4px;
            --sp-3:  8px;
            --sp-4: 12px;
            --sp-5: 16px;
            --sp-6: 24px;
            --sp-7: 32px;
            --sp-8: 48px;

            /* Corner radius. 8px was already Production's dominant value (x33),
               and 6px (x13) and 12px (x4) line up with -sm and -lg. */
            --radius-sm:   6px;
            --radius:      8px;
            --radius-lg:  12px;
            --radius-pill: 999px;  /* previously spelled 99px here */

            /* Content-column width: the cap on a single-column reading page,
               per osa-operations/docs/STYLE_GUIDE.md ("forms & reading, one
               field per row, short line"). NOT part of the pinned shared-token
               set: test_brand_colours ignores --content-* precisely because one
               app may define a layout token the other does not. Operations also
               carries --content-wide (960px) for list/card pages; copy it over
               if a Production page ever needs that tier rather than inventing a
               third number. */
            --content-narrow: 720px;

            /* ══ NEUTRALS ══ one true-neutral grey scale (R=G=B), identical to
               Operations — pinned by tests/test_brand_colours.py. */
            /* ══════════════════════════════════════════════════════════
               CANONICAL PALETTE — shared with osa-operations.
               Reference: osa-operations/docs/brand/palette.svg

               Every hue is OSA Red, OSA Blue, one of their complements, or
               the bisector of a gap between those four:

                 red 26 ANCHOR · orange 54 bisector Red–Gold · gold 83
                 complement of Blue · green 144 bisector Gold–Teal ·
                 teal 206 complement of Red · blue 263 ANCHOR ·
                 violet 324 bisector Blue–Red

               Do NOT fork these values here. Change an anchor and the whole
               set regenerates — see the reference.
               ══════════════════════════════════════════════════════════ */

            /* Neutrals — TRUE neutral, R=G=B. Identical to Operations. */
            --bg:        #ffffff;
            --bg2:       #f6f6f6;
            --bg3:       #eeeeee;
            --border:    #e3e3e3;
            --border2:   #c8c8c8;
            --text:      #1c1c1c;
            --text2:     #474747;
            --text3:     #777777;

            /* Status hues. `-bg` is the tint tier; the bare token is deep,
               which is the text-on-tint rung, so they pair by construction. */
            --blue:        #1f3d7c;  /* ANCHOR — OSA Blue */
            --blue-bg:     #ebeff5;
            --green:       #148624;
            --green-bg:    #e1f5e0;
            --gold:        #8c6811;
            --gold-bg:     #f5eee0;
            --orange:      #a65611;
            --orange-bg:   #f5ede7;
            --red:         #ad2828;
            --red-bg:      #f5eceb;
            --teal:        #147d88;
            --teal-bg:     #e0f3f5;
            --violet:      #9d44a5;
            --violet-bg:   #f5ebf5;

            /* Fill rungs — phase squares. A direction pair shares one hue and
               separates by these, 10 L* apart. `ink` is the label colour ON a
               fill; the `deep` rung is too light for that and collapses to
               1.4-3.0:1. */
            --blue-f1:     #b4c7e9;  --blue-f2:     #89a6dc;  --blue-f3:   #6b8ed3;
            --green-f1:    #99d999;  --green-f2:    #6fbb70;  --green-f3:  #5ba35d;
            --gold-f1:     #e6c077;  --gold-f2:     #c1a163;  --gold-f3:   #a78b54;
            --orange-f1:   #e9bb9d;  --orange-f2:   #db9362;  --orange-f3: #bd7e54;
            --teal-f1:     #80d6e1;  --teal-f2:     #6ab4bd;
            --violet-f1:   #e6b1e9;  --violet-f2:   #d680dd;  --violet-f3: #c461cc;
            --red-f1:      #eab8b3;  --red-f2:      #df8c84;
            --blue-ink:    #021852;  --gold-ink:    #2a1d03;
            --violet-ink:  #350539;  --green-ink:   #042707;  --orange-ink: #331703;

            /* Brand, exact. Everything else derives FROM these. */
            --osa-blue:  #1f3d7c;
            --osa-red:   #d9272d;

            /* Deliberately the SAME in day and night — the dev banner must not
               change appearance with the theme. Defined here and never in a
               [data-theme] block, so both themes resolve to this value.
               white on #ad2828 = 6.75:1 in both. */
            /* PANEL EDGES. A tinted panel's fill flips but its border did not:
               --*-f1 is a pastel tuned to sit quietly on a LIGHT fill, so after
               dark the same 1px went from a whisper to a highlighter ring around
               .warn-box / .alert-box / .flash-* / .badge-*. Light keeps the exact
               pastel it has always had; night derives the edge from that theme's
               own hue + panel, so it is always one step off the fill it borders. */
            --red-edge:   #eab8b3;
            --green-edge: #99d999;
            --gold-edge:  #e6c077;

            /* The lighter half of the "OSA Production" lockup. Same reason:
               --blue-f1 never flipped. Light is unchanged. */
            --blue-soft:  #b4c7e9;

            /* Icon strokes baked into data-URI backgrounds. A var() cannot reach
               inside a data URI, so the whole url() is the token and each theme
               names its own. */
            --icon-date:    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23474747' stroke-width='2'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E");
            --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%235a5a56' stroke-width='3'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");

            --banner-danger-bg: #ad2828;
            --banner-danger-fg: #ffffff;

            /* ══ THEME-DEPENDENT SURFACE TOKENS ══════════════════════════════
               Everything below has a counterpart in the dark block; a literal
               used here instead of a token is a literal that cannot flip.
               tests/test_theme_tokens.py enforces the pairing.
               Byte-identical to osa-operations' set (minus --cdv-today, which
               is its dashboard day-view and has no counterpart here). */

            /* Ink that sits ON a hue fill (avatar discs, the card headers). In
               light the hue tokens are the DEEP rung so white reads on them; in
               dark they invert to the LIGHT rung and white would drop to
               ~1.9:1. This token is what makes that rule survive the flip. */
            --on-accent:   #ffffff;

            /* Elevation. Black at low alpha is near-invisible on a dark
               surface, so dark does not merely reuse these — it raises alpha. */
            --shadow-drop: 0 4px 16px rgba(0,0,0,.10);
            --shadow-pop:  0 6px 20px rgba(0,0,0,.14);
            --overlay:     rgba(0,0,0,.35);
            --focus-ring:  rgba(31,61,124,.12);

            /* Timeline hatches — the "nothing here" squares. Deliberately only
               just visible against the card they sit on, in both themes. */
            --jtl-hatch-a: #faf9f7;  --jtl-hatch-b: #efece5;
            --jtl-ref-a:   #f4f2ee;  --jtl-ref-b:   #e8e4dc;

            /* The transparent end of a --bg fade. MUST be --bg at zero alpha,
               not `transparent` and not a hard-coded white: gradients
               interpolate through the stated colour, so the wrong one paints a
               pale (or sooty) haze across the fade in the other theme. */
            --bg-fade:     rgba(255,255,255,0);
            --scroll-cue:  rgba(0,0,0,.13);

            /* Filled card-header band (job detail's .card-hd, x11 on the page).
               It needs its OWN pair rather than --blue, because --blue is the
               DEEP rung in day and the LIGHT rung in night: a small button or
               avatar inverting to light-blue-with-dark-ink is the correct dark
               convention, but a full-width band doing it turns every card into
               a pale slab. The band stays dark in both themes and the ink stays
               light; only the depth changes. Day is byte-for-byte what --blue
               gave before, so light mode is unchanged.
               The band's secondary text keeps the blue-f1 fill rung (fills do
               not flip, and it clears AA on both bands — 6.1:1 day, 8.0:1
               night), so it needs no pair of its own.
               NB: written without the leading dashes on purpose — the canonical
               token check in tests/test_brand_colours.py scrapes `--name:` with
               a regex that does not skip comments, so a token named in prose
               inside this block is read as a declaration. */
            --card-hd-bg:  #1f3d7c;
            --card-hd-fg:  #ffffff;

            /* Primary-button fill. --blue could not do this job because it is
               the DEEP rung in day and the LIGHT rung in night, so every primary
               button turned into a pale chip after dark. Day is OSA Blue; night
               is the card-header navy, so a button sits flush with the band
               above it. White ink on both.
               Identity chips and indicators (the initials disc, status dots,
               the upload progress bar) deliberately keep the flipping hue —
               they are not actions and read well light-on-dark. */
            --accent-bg:   #1f3d7c;
            --accent-fg:   #ffffff;

            /* Hover for that fill — DERIVED, never a second hex. Two hand-picked
               hexes have already been wrong here: --blue-ink made the button
               SINK in night (so the resting state read as the hover), and its
               replacement was a near-miss of OSA Blue, i.e. a visible hue shift
               on hover, which reads as a bug rather than a state.
               This is the same treatment the card header's upload button uses
               (.hd-upload:hover) — 12% of the ink washed over the resting fill.
               Same hue, same chroma, just brighter: it can only ever lift, and
               it cannot drift off-palette because it has no value of its own.
               Every primary button in the app resolves through this one token
               (.btn-primary, .btn-signin, .pr-btn-primary, .cp-btn-primary,
               .obc-btn, .add-btn, .btn-modal-save), so it stays consistent by
               construction rather than by seven parallel edits. */
            --accent-hover: color-mix(in srgb, var(--accent-fg) 12%, var(--accent-bg));

            /* Ink on a --*-bg tinted panel (the travel date chips). In night the
               hue rung reads at 6.8:1 nominal, but .chip-label runs it at
               opacity .65, which lands at 3.82:1 — under AA and visibly flat.
               Near-white takes the same label to 5.89:1. Day keeps the blue. */
            --chip-ink:    #1f3d7c;

            /* Tell the BROWSER which theme it is painting into. Without this,
               every control the UA draws itself stays light whatever the tokens
               say: checkbox and radio boxes, the range-input track, scrollbars,
               date/time picker panels, number spinners and Chrome's autofill
               wash. None of those are reachable from CSS colours, so the tokens
               could not fix them — and did not. */
            color-scheme: light;
        }

        /* ══════════════════════════════════════════════════════════════════
           DARK THEME — parity with osa-operations; same contract, same values.
           Tri-state: day / night / auto. `auto` is the DEFAULT and is spelled
           as the ABSENCE of a data-theme attribute, so a user who never touches
           the control follows their OS.

             (no attribute)        -> auto: the media query below decides
             data-theme="light"    -> day, pinned
             data-theme="dark"     -> night, pinned

           The declarations are written TWICE on purpose. CSS cannot share one
           block between a media query and a plain selector, and the
           `:not([data-theme="light"])` guard is what stops a dark OS from
           overriding someone who explicitly picked day. The duplication is a
           drift risk, so tests/test_theme_tokens.py asserts the two copies are
           token-for-token identical and that every colour token in :root has a
           counterpart here.
           ══════════════════════════════════════════════════════════════════ */
        @media (prefers-color-scheme: dark) {
            :root:not([data-theme="light"]) {

            /* ── Neutrals ── true neutral (R=G=B), same rule as light. The
               light ramp's ORDINAL is preserved rather than mirrored: --bg is
               still the raised surface, --bg2 still the subtle fill, --bg3
               still the recessed page, so every rule that uses --bg2 without
               stating whether it means "hover" or "recessed" stays correct.
               Text tiers clear 4.5:1 on every dark surface (13.8 / 8.4 / 5.0
               on --bg, the LIGHTEST surface and so the hardest test for light
               ink). --text3 is one step off perfectly-even L* spacing to buy
               that margin; measured in a browser, the LIGHT --text3 lands at
               3.86-4.14:1 on --bg2/--bg3, so this is stricter, not looser. */
            --bg:        #212121;
            --bg2:       #1a1a1a;
            --bg3:       #121212;
            --border:    #333333;
            --border2:   #4d4d4d;
            --text:      #ededed;
            --text2:     #bbbbbb;
            --text3:     #8f8f8f;
            --on-accent: #1a1a1a;

            /* ── Status hues ── derived, not picked. Each is its LIGHT
               counterpart re-lit in OKLCh: same hue angle, same chroma, new
               lightness (L .78 for the ink rung, L .30 / C<=.05 for the tint).
               Hue drift from the light set is <=0.4deg on all seven, so a
               status colour is recognisably the same colour in either theme.
               Every ink-on-tint pair clears 4.5:1 (worst 6.45, violet). */
            --blue:        #92b7ff;
            --blue-bg:     #202d47;
            --green:       #6dd270;
            --green-bg:    #1d341d;
            --gold:        #d8b165;
            --gold-bg:     #3a2b0d;
            --orange:      #f7a064;
            --orange-bg:   #412614;
            --red:         #ff968c;
            --red-bg:      #442320;
            --teal:        #6dc8d3;
            --teal-bg:     #02353a;
            --violet:      #ec8ef4;
            --violet-bg:   #3a253c;

            /* Fill rungs (--*-f1/f2/f3) and --*-ink are deliberately NOT
               redefined: they inherit from :root. The phase squares are
               mid-light pastels that read well on a dark page (the calendar-app
               convention), and --*-ink clears 4.5:1 on every rung as-is (worst
               5.13 on blue-f3). Darkening the fills for dark mode would REDUCE
               that margin, since the ink is near-black — the change that looks
               more "dark mode" is the one that breaks contrast.

               --osa-blue / --osa-red are brand-exact and never derived, so they
               do not flip either. */

            --shadow-drop: 0 4px 16px rgba(0,0,0,.50);
            --shadow-pop:  0 6px 20px rgba(0,0,0,.55);
            --overlay:     rgba(0,0,0,.60);
            --focus-ring:  rgba(146,183,255,.24);

            --jtl-hatch-a: #242424;  --jtl-hatch-b: #1c1c1c;
            --jtl-ref-a:   #2b2b2b;  --jtl-ref-b:   #202020;
            --bg-fade:     rgba(33,33,33,0);
            --scroll-cue:  rgba(0,0,0,.55);
            /* Navy band, not the inverted light rung. #202d47 is --blue-bg's
               dark value, so this invents no new colour; it sits L* 18.5 over
               the card's 12.7, i.e. clearly a header, and carries --text at
               11.7:1. */
            --card-hd-bg:  #202d47;
            --card-hd-fg:  #ededed;
            /* Rest at the header band's navy so a button sits flush with the
               card header above it. Hover is derived from these two — see the
               note in :root; do NOT put a hex back here. */
            --accent-bg:   #202d47;
            --accent-fg:   #ffffff;
            --accent-hover: color-mix(in srgb, var(--accent-fg) 12%, var(--accent-bg));
            --chip-ink:    #ededed;
            --red-edge:   color-mix(in srgb, var(--red) 45%, var(--red-bg));
            --green-edge: color-mix(in srgb, var(--green) 45%, var(--green-bg));
            --gold-edge:  color-mix(in srgb, var(--gold) 45%, var(--gold-bg));
            --blue-soft:  color-mix(in srgb, var(--blue) 75%, var(--bg));
            --icon-date:    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%238f8f8f' stroke-width='2'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E");
            --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%238f8f8f' stroke-width='3'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
            color-scheme: dark;
            }
        }
        :root[data-theme="dark"] {

            /* ── Neutrals ── true neutral (R=G=B), same rule as light. The
               light ramp's ORDINAL is preserved rather than mirrored: --bg is
               still the raised surface, --bg2 still the subtle fill, --bg3
               still the recessed page, so every rule that uses --bg2 without
               stating whether it means "hover" or "recessed" stays correct.
               Text tiers clear 4.5:1 on every dark surface (13.8 / 8.4 / 5.0
               on --bg, the LIGHTEST surface and so the hardest test for light
               ink). --text3 is one step off perfectly-even L* spacing to buy
               that margin; measured in a browser, the LIGHT --text3 lands at
               3.86-4.14:1 on --bg2/--bg3, so this is stricter, not looser. */
            --bg:        #212121;
            --bg2:       #1a1a1a;
            --bg3:       #121212;
            --border:    #333333;
            --border2:   #4d4d4d;
            --text:      #ededed;
            --text2:     #bbbbbb;
            --text3:     #8f8f8f;
            --on-accent: #1a1a1a;

            /* ── Status hues ── derived, not picked. Each is its LIGHT
               counterpart re-lit in OKLCh: same hue angle, same chroma, new
               lightness (L .78 for the ink rung, L .30 / C<=.05 for the tint).
               Hue drift from the light set is <=0.4deg on all seven, so a
               status colour is recognisably the same colour in either theme.
               Every ink-on-tint pair clears 4.5:1 (worst 6.45, violet). */
            --blue:        #92b7ff;
            --blue-bg:     #202d47;
            --green:       #6dd270;
            --green-bg:    #1d341d;
            --gold:        #d8b165;
            --gold-bg:     #3a2b0d;
            --orange:      #f7a064;
            --orange-bg:   #412614;
            --red:         #ff968c;
            --red-bg:      #442320;
            --teal:        #6dc8d3;
            --teal-bg:     #02353a;
            --violet:      #ec8ef4;
            --violet-bg:   #3a253c;

            /* Fill rungs (--*-f1/f2/f3) and --*-ink are deliberately NOT
               redefined: they inherit from :root. The phase squares are
               mid-light pastels that read well on a dark page (the calendar-app
               convention), and --*-ink clears 4.5:1 on every rung as-is (worst
               5.13 on blue-f3). Darkening the fills for dark mode would REDUCE
               that margin, since the ink is near-black — the change that looks
               more "dark mode" is the one that breaks contrast.

               --osa-blue / --osa-red are brand-exact and never derived, so they
               do not flip either. */

            --shadow-drop: 0 4px 16px rgba(0,0,0,.50);
            --shadow-pop:  0 6px 20px rgba(0,0,0,.55);
            --overlay:     rgba(0,0,0,.60);
            --focus-ring:  rgba(146,183,255,.24);

            --jtl-hatch-a: #242424;  --jtl-hatch-b: #1c1c1c;
            --jtl-ref-a:   #2b2b2b;  --jtl-ref-b:   #202020;
            --bg-fade:     rgba(33,33,33,0);
            --scroll-cue:  rgba(0,0,0,.55);
            /* Navy band, not the inverted light rung. #202d47 is --blue-bg's
               dark value, so this invents no new colour; it sits L* 18.5 over
               the card's 12.7, i.e. clearly a header, and carries --text at
               11.7:1. */
            --card-hd-bg:  #202d47;
            --card-hd-fg:  #ededed;
            /* Rest at the header band's navy so a button sits flush with the
               card header above it. Hover is derived from these two — see the
               note in :root; do NOT put a hex back here. */
            --accent-bg:   #202d47;
            --accent-fg:   #ffffff;
            --accent-hover: color-mix(in srgb, var(--accent-fg) 12%, var(--accent-bg));
            --chip-ink:    #ededed;
            --red-edge:   color-mix(in srgb, var(--red) 45%, var(--red-bg));
            --green-edge: color-mix(in srgb, var(--green) 45%, var(--green-bg));
            --gold-edge:  color-mix(in srgb, var(--gold) 45%, var(--gold-bg));
            --blue-soft:  color-mix(in srgb, var(--blue) 75%, var(--bg));
            --icon-date:    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%238f8f8f' stroke-width='2'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E");
            --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%238f8f8f' stroke-width='3'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
            color-scheme: dark;
        }
        /* ══ RESET ══ byte-identical to osa-operations' app.css.
           Replaces Bootstrap Reboot, which used to load ahead of this file.
           Reboot's box-sizing was load-bearing on every padded element; the rest
           of it was opinion (h1 2.5rem, p margin-bottom 1rem, ul padding-left
           2rem) that nothing here relied on -- verified by diffing computed
           styles across five pages at two widths before and after removal. */
        *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

        /* Reduced motion, honoured site-wide.
           Lives here rather than beside the one animation that prompted it: the
           preference is a global accessibility setting, not a property of any
           page, and a per-page copy is a rule someone has to remember to add
           next time. The standard formulation — collapse rather than remove, so
           an animation that ends in a meaningful final state still reaches it,
           and a one-shot fade still "completes" instead of being skipped. */
        @media (prefers-reduced-motion: reduce) {
            *, *::before, *::after {
                animation-duration: 0.01ms !important;
                animation-iteration-count: 1 !important;
                transition-duration: 0.01ms !important;
                scroll-behavior: auto !important;
            }
        }

        /* Form controls do NOT inherit font by default -- the UA gives them
           13.33px Arial. Reboot papered over that; without this line every
           input, select, textarea and unstyled button reverts to it. */
        button, input, select, textarea, optgroup {
            font-family: inherit; font-size: inherit; line-height: inherit;
        }
        /* iOS Safari force-zooms the whole page when a focused text control's
           font-size is under 16px, and the zoomed viewport pans horizontally —
           on a phone the form reads as "the page width is broken". Inherited
           --fs-base is 13px, so EVERY control tripped it. Reported from the
           field on the onboarding form, which crew fill in on phones almost
           exclusively; invisible to the Chromium overflow gate because only
           Safari zooms (the layout itself never overflows — measured 0px in
           pristine, errored and dropdown-open states at 320-414px).
           16px exactly: it is Safari's threshold, and the zoom is about the
           focused CONTROL, so buttons/checkboxes/radios are exempt. */
        /* !important is deliberate and load-bearing: this is a behavioural
           FLOOR, not a design choice, and page-scoped rules like
           `.ob-field select` (0,1,1) silently outweigh a bare element
           selector — that is exactly how the first version of this fix left
           every <select> still zooming. The mobile CI gate
           (test_mobile_overflow) fails any page that slips under 16px. */
        @media (max-width: 768px) {
            input:not([type=checkbox]):not([type=radio]), select, textarea {
                font-size: 16px !important;
            }
        }
        /* Reboot set headings to 1.2; without it they inherit the body's 1.5 and
           every card header grows ~4px taller. Sizes are NOT reset here — every
           heading in this app already sets its own. */
        h1, h2, h3, h4, h5, h6 { line-height: 1.2; }
        table { border-collapse: collapse; }
        img, svg { vertical-align: middle; }
        summary { cursor: pointer; }
        textarea { resize: vertical; }
        /* Links with no class: Reboot painted these --bs-blue #0d6efd, and with
           it gone the UA default is #0000EE. Both are foreign blues. */
        a { color: var(--blue); }

        body {
            background: var(--bg3);
            color: var(--text);
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            font-size: var(--fs-base);
            line-height: 1.5;
        }
        .topnav {
            background: var(--bg);
            border-bottom: 0.5px solid var(--border);
            display: flex;
            align-items: center;
            padding: 0 var(--sp-6);
            height: 52px;
            position: sticky;
            /* Sits below the sticky dev banner when present (var set by JS to the
               banner's measured height); 0 in production where there's no banner. */
            top: var(--dev-banner-h, 0px);
            z-index: 100;
        }
        .topnav-brand {
            font-size: var(--fs-lg);
            font-weight: 500;
            color: var(--text);
            text-decoration: none;
            margin-right: var(--sp-7);
            display: flex;
            align-items: center;
            gap: var(--sp-3);
        }
        /* was #8a8a84, a warm grey off the neutral ramp — and Operations'
           .topnav-brand span already used --text3. This restores that parity;
           the subtitle darkens ~6 L* to match the sibling app exactly. */
        .topnav-brand span { color: var(--text3); font-weight: 400; }
        .topnav-logo { height: 26px; width: auto; display: block; }
        .topnav-links { display: flex; gap: var(--sp-2); flex: 1; }
        .topnav-links a {
            padding: var(--sp-2) var(--sp-4);
            border-radius: var(--radius);
            color: var(--text2);
            text-decoration: none;
            font-size: var(--fs-base);
            transition: background .12s, color .12s;
        }
        .topnav-links a:hover { background: var(--bg2); color: var(--text); }
        .topnav-links a.active { background: var(--bg2); color: var(--text); font-weight: 500; }
        .topnav-user {
            display: flex;
            align-items: center;
            gap: var(--sp-4);
            font-size: var(--fs-base);
            color: var(--text2);
            position: relative;
        }
        .user-menu-btn {
            background: none;
            border: none;
            font-size: var(--fs-base);
            color: var(--text2);
            cursor: pointer;
            font-family: inherit;
            padding: var(--sp-2) 0;
            display: flex;
            align-items: center;
            gap: var(--sp-2);
        }
        .user-menu-btn:hover { color: var(--text); }
        .user-dropdown {
            display: none;
            position: absolute;
            top: calc(100% + 8px);
            right: 0;
            background: var(--bg);
            border: 0.5px solid var(--border2);
            border-radius: var(--radius);
            box-shadow: var(--shadow-drop);
            min-width: 180px;
            z-index: 200;
            overflow: hidden;
        }
        .user-dropdown a {
            display: block;
            padding: var(--sp-3) var(--sp-4);
            font-size: var(--fs-base);
            color: var(--text);
            text-decoration: none;
            border-bottom: 0.5px solid var(--border);
        }
        .user-dropdown a:last-child { border-bottom: none; }
        .user-dropdown a:hover { background: var(--bg2); }
        .user-dropdown a.danger { color: var(--red); }
        .user-dropdown.open { display: block; }
        /* ── Theme toggle (top bar) ── day / night / auto, cycling in that
           order on click. Sits BEFORE .nav-toggle so it survives the 768px
           breakpoint: both .topnav-links and .topnav-user are display:none
           there, and this is a crew portal that is mostly used on a phone — a
           control only reachable on desktop would be the wrong half.
           margin-left:auto is a no-op on desktop (.topnav-links already has
           flex:1 and eats the free space) and is what right-packs the button
           beside the hamburger once those siblings are hidden.

           Three icons live in the markup and CSS picks one, rather than JS
           swapping innerHTML: the choice has to be correct in the very first
           painted frame, and the pre-paint script in base.html only sets an
           attribute. AUTO is the default and is the ABSENCE of data-theme, so
           the auto icon is the base state and the other two are overrides. */
        .theme-toggle {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            background: none;
            border: none;
            cursor: pointer;
            padding: var(--sp-3);
            /* Operations puts margin-left:auto on the FIRST icon button of the
               top-bar cluster; that is what right-packs the icons and the user
               menu. Here the theme toggle IS the first icon, so it carries it.
               On desktop it is a no-op (.topnav-links has flex:1 and has already
               eaten the free space); at <=768px, where .topnav-links and
               .topnav-user are display:none, it is what keeps the toggle and the
               hamburger right-aligned instead of stacked beside the brand. */
            margin-left: auto;
            margin-right: var(--sp-2);
            color: var(--text2);
            border-radius: var(--radius);
            line-height: 0;
            transition: background .12s, color .12s;
        }
        .theme-toggle:hover { background: var(--bg2); color: var(--text); }
        .theme-toggle .icon-sun,
        .theme-toggle .icon-moon { display: none; }
        .theme-toggle .icon-auto { display: inline-block; }
        html[data-theme="light"] .theme-toggle .icon-auto,
        html[data-theme="dark"]  .theme-toggle .icon-auto { display: none; }
        html[data-theme="light"] .theme-toggle .icon-sun  { display: inline-block; }
        html[data-theme="dark"]  .theme-toggle .icon-moon { display: inline-block; }
        /* Pinned states read as "on". */
        html[data-theme="light"] .theme-toggle,
        html[data-theme="dark"]  .theme-toggle { color: var(--blue); background: var(--blue-bg); }

        .nav-toggle { display:none; background:none; border:none; cursor:pointer; padding:var(--sp-2); color:var(--text2); font-size:var(--fs-xl); line-height:1; }
        .mobile-nav { display:none; position:fixed; top:52px; left:0; right:0; bottom:0; background:var(--bg); z-index:99; padding:var(--sp-4); border-top:0.5px solid var(--border); overflow-y:auto; }
        .mobile-nav a { display:block; padding:var(--sp-4) var(--sp-5); font-size:var(--fs-lg); text-decoration:none; color:var(--text); border-radius:var(--radius); margin-bottom:var(--sp-2); }
        .mobile-nav a:hover, .mobile-nav a.active { background:var(--bg2); }
        .mobile-nav .divider { height:0.5px; background:var(--border); margin:var(--sp-3) 0; }
        /* Bootstrap's .card supplied the background, the column flex and the
           min-width:0 that stops a long unbroken string from blowing the track
           out; this rule only ever added the radius and border on top. All of it
           lives here now. `bg-white` was in the markup to compensate and is gone. */
        .card {
            position: relative;
            display: flex;
            flex-direction: column;
            min-width: 0;
            background: var(--bg);
            border-radius: var(--radius);
            border: 0.5px solid var(--border);
        }

        /* Responsive card grid — replaces Bootstrap's .row.g-3 + .col-12
           .col-md-6 .col-lg-4. Same breakpoints (768 / 992) and the same 16px
           gutter, without the negative-margin row or the 12-column arithmetic.
           Lives here rather than in the page so the media queries stay out of a
           template <style> block (D4). */
        .card-grid {
            display: grid;
            grid-template-columns: 1fr;
            gap: var(--sp-5);
        }
        @media (min-width: 768px) { .card-grid { grid-template-columns: repeat(2, 1fr); } }
        @media (min-width: 992px) { .card-grid { grid-template-columns: repeat(3, 1fr); } }
        /* Production had NO .badge base rule — Bootstrap's supplied the shape
           (inline-block, .35em/.65em padding, weight 700, 6px radius) and only
           the colours lived here. This is osa-operations' .badge verbatim, which
           is the point: the badge is the most-used component in both apps and it
           should be one component, not two that happen to look similar.

           It is also the one deliberate visual change in this pass. The status
           key on /jobs/ and the chips on job detail move from Bootstrap's shape
           to the shared one: weight 700 -> 500, radius 6px -> pill. Every badge
           that already carried .jl-badge-sm was ALREADY overriding toward these
           values, so most of the surface does not move at all. */
        .badge {
            display: inline-flex;
            align-items: center;
            padding: var(--sp-1) var(--sp-3);
            border-radius: 20px;
            font-size: var(--fs-sm);
            font-weight: 500;
            /* A status chip never wraps its label — see osa-operations. */
            white-space: nowrap;
        }
        .badge-active { background: var(--green-bg); color: var(--green); }
        .badge-held { background: var(--gold-bg); color: var(--gold); }
        .badge-complete { background: var(--blue-bg); color: var(--blue); }
        .badge-closed { background: var(--bg2); color: var(--text2); }
		.badge-inquiry     { background: var(--bg2); color: var(--text2); }
		.badge-provisional { background: var(--gold-bg); color: var(--gold); }
		.badge-confirmed   { background: var(--green-bg); color: var(--green); }
		.badge-cancelled   { background: var(--red-bg); color: var(--red); }
        .flash-error { background: var(--red-bg); border: 0.5px solid var(--red-edge); color: var(--red); padding: var(--sp-3) var(--sp-4); border-radius: var(--radius); margin-bottom: var(--sp-5); font-size: var(--fs-base); }
        .flash-success { background: var(--green-bg); border: 0.5px solid var(--green-edge); color: var(--green); padding: var(--sp-3) var(--sp-4); border-radius: var(--radius); margin-bottom: var(--sp-5); font-size: var(--fs-base); }
        .flash-info { background: var(--blue-bg); border: 0.5px solid var(--border2); color: var(--blue); padding: var(--sp-3) var(--sp-4); border-radius: var(--radius); margin-bottom: var(--sp-5); font-size: var(--fs-base); }
        @media(max-width:768px) {
            .topnav-links { display: none; }
            .topnav-user { display: none; }
            .nav-toggle { display: block; }
        }
        .job-card-hover { cursor: pointer; transition: border-color .15s; }
        .job-card-hover:hover { border-color: var(--blue); }
        /* Invariant in BOTH directions — ink AND ground. A "this is not prod"
           bar has to look the same at 2am as at noon, so neither half may come
           from a flipping token. It previously took its ground from --red,
           which is a SIGNAL rung: #ad2828 in day but #ff968c in night, so the
           white ink fell from 6.75:1 to 2.1:1 and the bar became unreadable
           after dark. osa-operations never had this — its rule still carries
           the literal. Both halves are now named invariants (see :root). */
        .dev-banner { background:var(--banner-danger-bg); color:var(--banner-danger-fg);
                      text-align:center; padding:var(--sp-2) var(--sp-4);
                      font-size:var(--fs-sm); font-weight:500; letter-spacing:.04em;
                      position:sticky; top:0; z-index:101; }
        .topnav-links a.nav-crumb { color:var(--blue); background:var(--blue-bg); }
        .mobile-nav a.nav-crumb { color:var(--blue); }
        .page-shell { max-width:1440px; margin:0 auto; }
        /* was `px-4 pt-3` — Bootstrap's 1.5rem sides / 1rem top, on the scale. */
        .flash-wrap { padding: var(--sp-5) var(--sp-6) 0; }
        /* Auth-card layout, shared by /auth/* templates that
           override the app_layout block */
        body.auth-layout { min-height: 100vh; display: flex; flex-direction: column; }
        .auth-wrap { flex:1; display:flex; align-items:center; justify-content:center; padding:var(--sp-5); }
        /* Size matches Operations' .auth-card (max-width 400 / sp-7 sp-8 padding)
           — shared by every /auth/* page (login, change/forgot/reset/set-password,
           the SAML denied page). */
        .auth-card { background:var(--bg); border-radius:var(--radius-lg); padding:var(--sp-7) var(--sp-8);
                     width:100%; max-width:400px; box-shadow:var(--shadow-drop);
                     border:0.5px solid var(--border); }
        .auth-title { font-size:var(--fs-xl); font-weight:500; color:var(--blue); margin-bottom:var(--sp-2); }
        .auth-title span { font-weight:400; color:var(--blue-soft); }
        .auth-title img { float:left; height:42px; width:auto; margin:var(--sp-2) var(--sp-4) 0 0; }
        /* The brand mark on the login card is a quiet SSO shortcut (auth/login.html):
           a returning staff user clicks it to jump straight to Entra. Subtle hover,
           no loud button. */
        .sso-logo img { cursor:pointer; transition:opacity .12s ease; }
        .sso-logo img:hover { opacity:.72; }
        /* Own block-formatting context so the sub sits BESIDE the floated logo as
           a column (media-object pattern): every wrapped line aligns under the
           title text instead of the 2nd line dropping to the far left under the
           logo. Shared by all /auth/* + memo cards using this header. */
        .auth-sub { display:flow-root; font-size:var(--fs-base); color:var(--text2); margin-bottom:var(--sp-6); }
        .auth-card label { display:block; font-size:var(--fs-base); font-weight:500; color:var(--text); margin-bottom:var(--sp-2); }
        .auth-card label .label-note { font-size:var(--fs-sm); color:var(--text3); font-weight:400; }
        .auth-card input[type=email],
        .auth-card input[type=password],
        .auth-card input[type=text] { width:100%; height:44px; border:1px solid var(--border2);
                                      border-radius:var(--radius); padding:0 var(--sp-4); font-size:var(--fs-base);
                                      color:var(--text); background:var(--bg); outline:none; margin-bottom:var(--sp-5); }
        .auth-card input:focus { border-color:var(--blue); box-shadow:0 0 0 3px var(--focus-ring); }
        .auth-card input:disabled { background:var(--bg2); color:var(--text3); }
        .auth-card .hint { background:var(--blue-bg); border-left:3px solid var(--osa-red);
                           border-radius:0 var(--radius-sm) var(--radius-sm) 0; padding:var(--sp-3) var(--sp-4);
                           font-size:var(--fs-base); color:var(--blue); line-height:1.5; margin-bottom:var(--sp-6); }
        .btn-signin { width:100%; height:48px; background:var(--accent-bg); color:var(--accent-fg); border:none;
                      border-radius:var(--radius); font-size:var(--fs-lg); font-weight:500; cursor:pointer; margin-top:var(--sp-2); }
        .btn-signin:hover { background:var(--accent-hover); }
        .btn-decline { width:100%; height:44px; background:var(--bg); color:var(--red); border:1px solid var(--red-edge);
                       border-radius:var(--radius); font-size:var(--fs-base); font-weight:500; cursor:pointer; margin-top:var(--sp-3); }
        .btn-decline:hover { background:var(--red-bg); }  /* was #FEF3F2 — the button is red-themed; --red-bg is its own tint tier */
        /* Memo accept/decline pages — replaces the mb-*/mt-3/text-muted
           utilities they borrowed from Bootstrap. Same resolved values: mb-3 and
           mt-3 are 1rem (--sp-5), mb-2 is .5rem (--sp-3), mb-1 is .25rem
           (--sp-2). .text-muted was rgba(33,37,41,.75) — L* 41 on white, which
           is --text2's rung, not --text3's. */
        .auth-card .memo-intro { margin-bottom:var(--sp-5); }
        .auth-card .memo-field { margin-bottom:var(--sp-3); }
        .auth-card .memo-leg   { margin-bottom:var(--sp-2); }
        .auth-card .memo-form  { margin-top:var(--sp-5); }
        .auth-card .memo-muted { color:var(--text2); }

        .auth-card .form-foot { text-align:center; margin-top:var(--sp-5); font-size:var(--fs-base); }
        .auth-card .form-foot a { color:var(--blue); text-decoration:none; }
        .auth-card .form-foot a:hover { text-decoration:underline; }
        .auth-card .flash-error { background:var(--red-bg); border:1px solid var(--red-edge); color:var(--red);
                                  padding:var(--sp-3) var(--sp-4); border-radius:var(--radius); margin-bottom:var(--sp-5); font-size:var(--fs-base); }
        .auth-card .flash-success { background:var(--green-bg); border:1px solid var(--green-edge); color:var(--green);
                                    padding:var(--sp-3) var(--sp-4); border-radius:var(--radius); margin-bottom:var(--sp-5); font-size:var(--fs-base); }
        .auth-card .flash-info { background:var(--blue-bg); border:1px solid var(--border2); color:var(--blue);
                                 padding:var(--sp-3) var(--sp-4); border-radius:var(--radius); margin-bottom:var(--sp-5); font-size:var(--fs-base); }

        /* ── Address autocomplete dropdown (Google Places via server proxy) ──
           Ported from Operations base.html; concrete colours substituted for
           the Operations CSS variables. */
        .addr-autocomplete { position: relative; }
        .addr-dropdown {
            display: none;
            position: absolute;
            top: calc(100% + 2px);
            left: 0;
            right: 0;
            background: var(--bg);
            border: 0.5px solid var(--border2);
            border-radius: var(--radius);
            z-index: 250;
            max-height: 240px;
            overflow-y: auto;
            box-shadow: var(--shadow-drop);
        }
        .addr-dropdown.open { display: block; }
        .addr-option {
            padding: var(--sp-3) var(--sp-4);
            font-size: var(--fs-base);
            cursor: pointer;
            border-bottom: 0.5px solid var(--border);
        }
        .addr-option:last-child { border-bottom: none; }
        .addr-option:hover,
        .addr-option.active { background: var(--bg2); }

        /* ── Airport autocomplete dropdown ── */
        .airport-ac { position: relative; }
        .airport-dropdown {
            display: none;
            position: absolute;
            top: calc(100% + 2px);
            left: 0;
            right: 0;
            background: var(--bg);
            border: 0.5px solid var(--border2);
            border-radius: var(--radius);
            z-index: 250;
            max-height: 240px;
            overflow-y: auto;
            box-shadow: var(--shadow-drop);
        }
        .airport-dropdown.open { display: block; }
        .airport-option {
            display: flex;
            align-items: center;
            gap: var(--sp-3);
            padding: var(--sp-3) var(--sp-4);
            font-size: var(--fs-base);
            cursor: pointer;
            border-bottom: 0.5px solid var(--border);
        }
        .airport-option:last-child { border-bottom: none; }
        .airport-option:hover,
        .airport-option.active { background: var(--bg2); }
        .airport-code {
            font-weight: 600;
            min-width: 34px;
            font-variant-numeric: tabular-nums;
            letter-spacing: .04em;
        }
        .airport-detail { color: var(--text2); font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

        /* ── Shared date-field rendering (mirrors osa-operations base.html) ──
           One global enhancer so every date input behaves identically across the
           site. The value is ALWAYS #111 (filled, editable or disabled),
           !important so no page-specific rule can re-grey it; an empty field
           shows nothing (no grey mm/dd/yyyy) unless it's being edited. The JS
           enhancer (below) toggles .date-empty + injects the Clear control. */
        /* Date/time inputs are drawn by US, not the platform (ported from
           Operations). A native-appearance control sizes itself from font-size
           and ignores our height/padding, so on iOS/iPad Safari these render
           visibly taller than the text inputs beside them while looking fine in
           Chrome. Opting out puts our own sizing back in charge. The calendar
           affordance the native widget gave us is redrawn as an inline SVG, and
           the picker's Clear link is already replaced by .date-clear-btn below,
           so nothing is lost. */
        input[type="date"], input[type="time"], input[type="datetime-local"] {
            -webkit-appearance:none; -moz-appearance:none; appearance:none;
            background-image:var(--icon-date);
            background-repeat:no-repeat; background-position:right 10px center;
            padding-right:var(--sp-6);
        }
        /* WebKit still reserves room for the (now hidden) native indicator. */
        input[type="date"]::-webkit-calendar-picker-indicator,
        input[type="time"]::-webkit-calendar-picker-indicator {
            opacity:0; -webkit-appearance:none; appearance:none;
            width:18px; margin:0; padding:0; cursor:pointer;
        }
        /* WebKit puts 1px of vertical padding on the fields wrapper inside the
           shadow DOM, so a date input measures 2px TALLER than the text input
           beside it — visible as one box standing proud in a row of four.
           Measured in Chromium: 39.5px vs 37.5px, identical font, padding and
           border; zeroing this makes them equal.
           Fixing the cause rather than pinning a height keeps the controls
           sized by their padding, so changing the type scale still moves both
           together. Setting line-height (outer OR on ::-webkit-datetime-edit)
           does NOT help — the wrapper's padding is the whole difference. */
        input[type="date"]::-webkit-datetime-edit-fields-wrapper,
        input[type="time"]::-webkit-datetime-edit-fields-wrapper {
            padding:0;
        }
        /* iOS sizes the inner value box from the font — pin it to the line. */
        input[type="date"]::-webkit-date-and-time-value,
        input[type="time"]::-webkit-date-and-time-value { margin:0; line-height:1.2; }
        input[type="date"] { color:var(--text) !important; -webkit-text-fill-color:var(--text) !important; }
        input[type="date"].date-empty:not(:focus),
        input[type="date"].date-empty:not(:focus)::-webkit-datetime-edit { color:transparent !important; -webkit-text-fill-color:transparent !important; }
        input[type="date"]:focus::-webkit-datetime-edit-text,
        input[type="date"]:focus::-webkit-datetime-edit-month-field,
        input[type="date"]:focus::-webkit-datetime-edit-day-field,
        input[type="date"]:focus::-webkit-datetime-edit-year-field { color:var(--text); }
        /* Clear control sits BELOW the input, right-aligned; the row reserves a
           little height even when the button is hidden so the field doesn't jump. */
        .date-clear-row { display:flex; justify-content:flex-end; min-height:13px; margin-top:var(--sp-1); }
        .date-clear-btn {
            font-size:var(--fs-sm); color:var(--text2); background:none; border:none; padding:0;
            font-weight:normal; letter-spacing:0; white-space:nowrap; cursor:pointer; user-select:none;
        }
        .date-clear-btn:hover { color:var(--osa-blue); text-decoration:underline; }
        .date-clear-btn[hidden] { display:none; }
        /* Brand accent on every checkbox/radio. Also stops the OS desaturating
           them to grey when the browser window is inactive — the page shouldn't
           change appearance just because it isn't the focused window. */
        /* Explicit box, not the platform's (ported from Operations). A native
           checkbox derives its size from font-size, so it visibly grew/shrank
           as the page crossed a breakpoint that moves the type scale — reading
           as the checkboxes "zooming" while resizing. A fixed size makes them
           identical at every width, on every page. */
        input[type="checkbox"], input[type="radio"] {
            accent-color:var(--osa-blue);
            width:15px; height:15px; flex:none;
        }

        /* ── Horizontal scroller (ported from Operations) ──
           One utility for every element that scrolls sideways, so they all
           announce hidden content the same way instead of each page inventing
           it (or, mostly, not).
           How it works: the two `local` gradients scroll WITH the content and
           mask the shadow when you are at that end; the two `scroll` radials
           stay pinned to the box. Net effect — a soft edge shadow appears only
           on the side that still has content, and fades as you reach it. Pure
           CSS: no scroll listener, nothing to keep in sync. */
        .hscroll {
            overflow-x:auto;
            -webkit-overflow-scrolling:touch;
            background:
                linear-gradient(90deg, var(--bg), var(--bg-fade)) 0 0/44px 100% no-repeat local,
                linear-gradient(90deg, var(--bg-fade), var(--bg)) 100% 0/44px 100% no-repeat local,
                radial-gradient(farthest-side at 0 50%, var(--scroll-cue), transparent) 0 0/16px 100% no-repeat scroll,
                radial-gradient(farthest-side at 100% 50%, var(--scroll-cue), transparent) 100% 0/16px 100% no-repeat scroll;
        }
        /* Children must not wrap, or there is nothing to scroll. */
        .hscroll > * { flex-shrink:0; }

    /* Self-service PII preview modal */
    #pii-reveal-modal { position:fixed; inset:0; z-index:1200; display:flex;
        align-items:center; justify-content:center; padding:var(--sp-5); background:var(--overlay); }
    #pii-reveal-modal[hidden] { display:none !important; }
    .pr-dialog { background:var(--bg); border-radius:14px; width:100%; max-width:420px;
        box-shadow:var(--shadow-pop); padding:26px 26px var(--sp-6); font-family:inherit; }
    .pr-dialog h2 { font-size:var(--fs-xl); font-weight:600; color:var(--blue); margin:0 0 var(--sp-3); }
    /* was #4b5563 — a Tailwind slate, one of the cool-grey leftovers the palette
       comment describes. --text2 is the secondary-text tier (nearest rung). */
    .pr-dialog p { font-size:var(--fs-base); color:var(--text2); line-height:1.55; margin:0 0 var(--sp-4); }
    .pr-lock { font-size:26px; line-height:1; margin-bottom:var(--sp-2); }
    .pr-actions { display:flex; gap:var(--sp-3); margin-top:var(--sp-5); }
    .pr-btn { flex:1; padding:var(--sp-4); font-size:var(--fs-base); font-weight:500; border-radius:var(--radius);
        border:none; cursor:pointer; font-family:inherit; }
    .pr-btn-primary { background:var(--accent-bg); color:var(--accent-fg); }
    .pr-btn-primary:hover { background:var(--accent-hover); }
    .pr-btn-ghost { background:var(--bg2); color:var(--text); }
    .pr-btn-ghost:hover { background:var(--border); }
    .pr-code-input { width:100%; box-sizing:border-box; text-align:center; font-size:26px;
        letter-spacing:10px; padding:var(--sp-4); border:1px solid var(--border2); border-radius:var(--radius);
        font-family:inherit; color:var(--text); background:var(--bg); }
    .pr-code-input:focus { outline:none; border-color:var(--blue); box-shadow:0 0 0 3px var(--focus-ring); }
    .pr-msg { min-height:18px; font-size:var(--fs-sm); margin-top:var(--sp-3); }
    .pr-msg-error { color:var(--red); }
    .pr-intro-msg { font-size:var(--fs-sm); color:var(--red); margin-top:var(--sp-3); }
    .pr-dest { font-weight:600; color:var(--blue); }
    .pr-resend { background:none; border:none; color:var(--blue); font-size:var(--fs-sm); cursor:pointer;
        padding:var(--sp-2) 0; font-family:inherit; text-decoration:underline; }
    .pr-center { text-align:center; }
    .pr-spinner { width:34px; height:34px; margin:var(--sp-2) auto var(--sp-4); border-radius:50%;
        border:3px solid var(--border); border-top-color:var(--blue); animation:pr-spin .8s linear infinite; }
    @keyframes pr-spin { to { transform:rotate(360deg); } }

    /* ── Crew avatars + photo upload (P5). Mirrors the ops app's vocabulary so
       the two sites read identically. .avatar* is reused site-wide (topnav,
       roster, finder); .crew-photo* is the onboarding-form pick/crop widget. */
    .avatar { border-radius: 50%; object-fit: cover; background: var(--bg3); display: inline-block; vertical-align: middle; flex: none; }
    .avatar-mono { display: inline-flex; align-items: center; justify-content: center; background: var(--accent-bg); color: var(--accent-fg); font-weight: 600; text-transform: uppercase; line-height: 1; }
    .avatar-xs { width: 32px; height: 32px; font-size: 13px; }
    .avatar-sm { width: 48px; height: 48px; font-size: 18px; }
    .avatar-md { width: 120px; height: 120px; font-size: 44px; }
    .avatar-lg { width: 200px; height: 200px; font-size: 72px; }

    /* No margin-bottom: the strip is an .ob-card's own content now, so its old
       spacing stacked on the card's padding and left the card visibly
       bottom-heavy. The gap it used to provide before the error banner is now
       that banner's own margin-top. Same in osa-operations. */
    .crew-photo { display: flex; align-items: center; gap: var(--sp-4); }
    .crew-photo-actions { display: flex; flex-direction: column; gap: var(--sp-2); }
    /* Crew resume tile — the "there is a resume" affordance beside the avatar,
       and the thing you click to read it. Deliberately the SAME 120px footprint
       as .avatar-md: that makes the Photo and Resume cells identical in height,
       so .crew-photo's align-items:center lines them up and needs no change.
       Tinted rather than solid so it reads as a sibling of the avatar without
       competing with it; the empty state is the muted dashed treatment already
       used for "nothing on file". Hover steps DOWN the blue fill ladder
       (--blue-bg -> --blue-f1, ~14 L*) rather than shifting opacity: opacity on
       a near-white tint over a white card lightens toward the background, which
       on a 120px surface was imperceptible and gave no hint the tile was
       clickable. --blue-f1 keeps the icon (--blue) at 6.1:1. The empty tile is a
       <span>, not a link, so it deliberately has no hover. */
    .resume-tile { width: 120px; height: 120px; flex: none; box-sizing: border-box;
                   border-radius: var(--radius); display: inline-flex;
                   align-items: center; justify-content: center;
                   background: var(--blue-bg); color: var(--blue); text-decoration: none; }
    .resume-tile:hover { background: color-mix(in srgb, var(--blue) 14%, var(--bg)); }
    .resume-tile.empty { background: transparent; border: 1px dashed var(--border2);
                         color: var(--text3); }
    .resume-tile.empty:hover { background: transparent; }
    /* One full-screen backdrop, two callers. `.modal-overlay` (the job
       detail page's call-time modal) declared this same set page-locally;
       once both moved onto --overlay they resolved identically, so the
       page copy was deleted rather than left as a duplicate. Matches
       osa-operations, where the backdrop lives in shared CSS only. */
    .crew-photo-modal,
    .modal-overlay { position: fixed; inset: 0; z-index: 1000; display: none; align-items: center; justify-content: center; background: var(--overlay); }
    .crew-photo-modal.is-open { display: flex; }
    .crew-photo-modal-inner { background: var(--bg); border-radius: var(--radius); padding: var(--sp-5); max-width: 92vw; }
    .crew-photo-crop-wrap { max-width: 70vw; max-height: 60vh; }
    .crew-photo-crop-wrap img { display: block; max-width: 100%; }
    .crew-photo-modal-actions { display: flex; justify-content: flex-end; gap: var(--sp-3); margin-top: var(--sp-4); }
    .crew-photo-modal .cropper-view-box, .crew-photo-modal .cropper-face { border-radius: 50%; }
    .crew-photo-zoom { display: block; width: 100%; margin-top: var(--sp-3); }
    body.crew-photo-lock { overflow: hidden; }
    .crew-photo-error { color: var(--red); font-size: var(--fs-sm);
                        margin-top: var(--sp-5); margin-bottom: var(--sp-5); }
    /* Scoped buttons — prod has no shared .btn-sm/.btn-primary system, so the
       widget carries its own (blue primary, outline secondary). */
    .cp-btn { padding: var(--sp-2) var(--sp-4); border: 1px solid var(--border2); border-radius: var(--radius);
              background: var(--bg); color: var(--text); font-size: var(--fs-sm); font-weight: 500; cursor: pointer; }
    .cp-btn:hover { background: var(--bg2); }
    .cp-btn-primary { background: var(--accent-bg); color: var(--accent-fg); border-color: var(--accent-bg); }
    .cp-btn-primary:hover { background: var(--accent-hover); }

    /* ── Global site footer ──
       Rendered from base.html OUTSIDE the app_layout block, so it appears on the
       authenticated pages AND on the templates that replace that block whole
       (login, set/forgot/reset-password, memos/accept). body.auth-layout is
       already a column flex with .auth-wrap{flex:1}, so on those pages this
       lands at the bottom of the viewport with no sticky-footer hack. */
    .site-footer { display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
                   gap: var(--sp-3); padding: var(--sp-5) var(--sp-6); margin-top: var(--sp-6);
                   border-top: 0.5px solid var(--border); font-size: var(--fs-sm); color: var(--text3); }
    .site-footer a { color: var(--text2); text-decoration: none; }
    .site-footer a:hover { color: var(--blue); text-decoration: underline; }
    .site-footer-sep { color: var(--border2); }

    /* ── Legal documents (/legal/privacy, /legal/terms) ──
       Long-form prose, so the measure is capped far below .page-shell's 1440px:
       a 1400px line of 13px text is unreadable. Type comes from the shared
       scale; only line-height is looser than the app default, which is right for
       reading rather than scanning a table. */
    .legal-wrap { max-width: var(--content-narrow); margin: 0 auto; padding: var(--sp-6); }
    .legal-head { margin-bottom: var(--sp-6); padding-bottom: var(--sp-5);
                  border-bottom: 0.5px solid var(--border); }
    /* Shared page title: one 22px blue heading for the whole app. Promoted out
       of onboarding/form.html's page-local `.ob-header h1` when these documents
       needed the same treatment: the D2 ratchet flagged the second copy, which
       is exactly its job. Use this class rather than restyling an <h1>. */
    .page-title { font-size: var(--fs-2xl); font-weight: 600; color: var(--blue);
                  margin: 0 0 var(--sp-2); }
    .legal-eff { font-size: var(--fs-sm); color: var(--text3); margin: 0; }
    .legal-body { font-size: var(--fs-base); line-height: 1.5; color: var(--text); }
    .legal-body h2 { font-size: var(--fs-lg); font-weight: 600; color: var(--blue);
                     margin: var(--sp-7) 0 var(--sp-4); }
    .legal-body p { margin: 0 0 var(--sp-4); }
    .legal-body ul { margin: 0 0 var(--sp-4); padding-left: var(--sp-6); }
    .legal-body li { margin-bottom: var(--sp-2); }
    .legal-body a { color: var(--blue); }
    .legal-crosslink { margin: var(--sp-7) 0 0; padding: var(--sp-5) 0 0;
                       border-top: 0.5px solid var(--border); font-size: var(--fs-sm); color: var(--text2); }
    .legal-crosslink a { color: var(--blue); }

    /* Authorized-use notice under the sign-in button. Quiet by design: it is a
       legal marker, not a warning the crew need to read every morning. */
    .auth-notice { margin-top: var(--sp-5); padding-top: var(--sp-4);
                   border-top: 0.5px solid var(--border); font-size: var(--fs-sm);
                   line-height: 1.5; color: var(--text3); text-align: center; }

    /* ══ ADMIN SURFACE ══════════════════════════════════════════════════════
       The vocabulary /admin/ and /admin/mail share. Everything here was
       PROMOTED verbatim out of admin/index.html's page-local <style> when the
       mail page needed the same components — the D2 ratchet's whole purpose is
       to force that move rather than let a second copy drift. Values are
       unchanged from the originals, so /admin/ renders identically; only the
       .badge block was dropped in favour of the shared one above (it was
       admin/index.html's single shared_dup, differing from it in nothing but
       inline-block vs inline-flex and 12px vs pill radius).

       Use these classes on any new admin page. Do not restyle them per page. */

    /* Page heading — the admin pages sit inside .page-shell with no gutter of
       their own, so the padding lives on each block rather than on a wrapper. */
    .page-head { padding: var(--sp-5) var(--sp-5) 0; }
    .page-h1   { font-size: var(--fs-xl); font-weight: 500; color: var(--blue); }
    .page-lede { font-size: var(--fs-base); color: var(--text3); margin-top: var(--sp-2); }

    /* Stat tiles. Four across on desktop, two on mobile. */
    .stat-grid  { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-4);
                  padding: var(--sp-5) var(--sp-5) 0; }
    .stat-card  { background: var(--bg); border-radius: var(--radius);
                  border: 0.5px solid var(--border); padding: var(--sp-4) var(--sp-5); }
    .stat-label { font-size: var(--fs-sm); color: var(--text3); font-weight: 500;
                  text-transform: uppercase; letter-spacing: .05em; margin-bottom: var(--sp-2); }
    .stat-val   { font-size: var(--fs-2xl); font-weight: 500; color: var(--blue); }
    .stat-sub   { font-size: var(--fs-sm); color: var(--text3); margin-top: var(--sp-1); }
    /* A count that is only interesting when it is non-zero — failures, expired
       invites. Red at a glance, neutral when the news is good, so an admin does
       not have to read five numbers to find the one that wants them. */
    .stat-val-bad  { color: var(--red); }
    .stat-val-calm { color: var(--text3); }

    /* Sectioned list blocks. */
    .section       { padding: var(--sp-5) var(--sp-5) 0; }
    .section-head  { display: flex; align-items: baseline; gap: var(--sp-3); margin-bottom: var(--sp-4); }
    .section-title { font-size: var(--fs-lg); font-weight: 500; color: var(--blue); }
    .section-count { font-size: var(--fs-base); color: var(--text3); }
    .section-pad-btm { padding-bottom: var(--sp-6); }
    /* Horizontal rule between sections. NOT named .divider: that class already
       exists scoped to .mobile-nav, and a second global meaning for one name is
       how a rule ends up applying where nobody expects it. */
    .section-rule  { border: none; border-top: 0.5px solid var(--border); margin: var(--sp-5); }

    /* Callouts. .warn-box is gold (something needs attention), .info-box blue
       (explanatory), .alert-box red (something is off right now). */
    .warn-box  { background: var(--gold-bg); border: 0.5px solid var(--gold-edge);
                 border-radius: var(--radius); padding: var(--sp-4);
                 display: flex; align-items: flex-start; gap: var(--sp-3);
                 margin: var(--sp-5) var(--sp-5) 0; }
    .warn-text { font-size: var(--fs-base); color: var(--gold); line-height: 1.5; }
    .info-box  { background: var(--blue-bg); border: 0.5px solid var(--border2);
                 border-radius: var(--radius); padding: var(--sp-4);
                 display: flex; align-items: flex-start; gap: var(--sp-3);
                 margin: var(--sp-5) var(--sp-5) 0; }
    .info-text { font-size: var(--fs-base); color: var(--blue); line-height: 1.55; }
    .alert-box  { background: var(--red-bg); border: 0.5px solid var(--red-edge);
                  border-radius: var(--radius); padding: var(--sp-4);
                  display: flex; align-items: flex-start; gap: var(--sp-3);
                  margin: var(--sp-5) var(--sp-5) 0; }
    .alert-text { font-size: var(--fs-base); color: var(--red); line-height: 1.55; }
    .warn-box-spaced { margin-top: var(--sp-5); }
    .icon-fixed      { flex-shrink: 0; margin-top: 1px; }  /* off-scale: 1px — optical nudge; --sp-1 is 2px, rounding would double it */

    /* Footnote rows under a section.

       .note-row is a DOT + ONE flex child. A bare run of prose containing
       inline <strong>/<code> is not one child — each inline element becomes its
       own flex item and lays out as a separate COLUMN. /admin/mail's notes
       rendered as three ragged columns that way, badly enough on a phone to be
       unreadable, and no overflow gate can see it because the columns still fit
       the viewport. Wrap any prose in .note-text, and use .note-row-block when
       it runs to more than one line so the dot sits on the first line rather
       than centred against the whole paragraph. */
    .note-row       { display: flex; align-items: center; gap: var(--sp-2);
                      font-size: var(--fs-sm); color: var(--text3); margin-top: var(--sp-3); }
    .note-row-block { align-items: flex-start; }
    .note-row-block .note-dot { margin-top: 6px; }  /* off-scale: 6px — optical, centres the dot on the first line */
    .note-text      { min-width: 0; line-height: 1.55; }
    .note-row-tight { margin-top: var(--sp-2); }
    .note-dot       { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }  /* off-scale: 5px dot */
    .note-dot-gray  { background: var(--text3); }
    .note-dot-blue  { background: var(--blue); }

    /* Pagination bar. */
    .pagebar      { display: flex; justify-content: space-between; align-items: center;
                    padding: var(--sp-4) var(--sp-5); border-top: 0.5px solid var(--border); }
    .pagebar-meta { font-size: var(--fs-sm); color: var(--text2); }
    .pagebar-btns { display: flex; gap: var(--sp-3); }
    .btn-page     { font-size: var(--fs-sm); padding: var(--sp-2) var(--sp-4); }

    /* The generic button this app never had. `.btn btn-page` was already in
       admin/index.html's markup with no .btn rule anywhere behind it, so the
       pager rendered as two bare text links; the mail page needs real buttons
       for the kill switch and the log filter, and one shared definition is the
       answer to both. The widget-scoped sets (.cp-btn, .pr-btn, .btn-signin)
       are left alone — they are deliberately sized for their own surfaces. */
    .btn { display: inline-flex; align-items: center; justify-content: center;
           gap: var(--sp-2); padding: var(--sp-3) var(--sp-4);
           border: 0.5px solid var(--border2); border-radius: var(--radius);
           background: var(--bg); color: var(--text); font-size: var(--fs-base);
           font-weight: 500; font-family: inherit; line-height: 1;
           text-decoration: none; cursor: pointer; white-space: nowrap; }
    .btn:hover        { background: var(--bg2); }
    .btn-primary      { background: var(--accent-bg); color: var(--accent-fg); border-color: var(--accent-bg); }
    .btn-primary:hover{ background: var(--accent-hover); }
    .btn-danger       { color: var(--red); border-color: var(--red-edge); }
    .btn-danger:hover { background: var(--red-bg); }
    .btn:disabled, .btn[aria-disabled="true"] { opacity: .5; cursor: default; }

    /* Badge variants the admin surface needs on top of the status set above.
       Same tint/deep pairing; the hairline is what separates a quiet grey chip
       from the page background. */
    .badge-inactive { background: var(--bg2); color: var(--text3); border: 0.5px solid var(--border); }
    .badge-pending  { background: var(--gold-bg); color: var(--gold); border: 0.5px solid var(--gold-edge); }
    .badge-bad      { background: var(--red-bg); color: var(--red); border: 0.5px solid var(--red-edge); }

    /* ══ MAIL OPERATIONS (/admin/mail) ══════════════════════════════════════
       Only what is genuinely specific to this page; everything above is shared.

       The email log is a stack of rows, not a table, on purpose: admin/index's
       table needs a whole duplicate card layout to survive a phone
       (.user-table/.user-cards), and a two-line log entry reflows on its own.
       It is also the shape osa-operations' /mail uses, so the two pages read
       the same. */
    .mail-switch      { display: flex; align-items: flex-start; justify-content: space-between;
                        gap: var(--sp-5); flex-wrap: wrap; }
    .mail-switch-copy { flex: 1 1 320px; font-size: var(--fs-base); color: var(--text); line-height: 1.55; }  /* off-scale: 320px flex basis */
    .mail-switch-form { display: flex; align-items: flex-end; gap: var(--sp-3); flex-wrap: wrap; }
    .mail-reason      { display: flex; flex-direction: column; gap: var(--sp-2); }
    .mail-reason label{ font-size: var(--fs-sm); color: var(--text3); font-weight: 500; }

    /* One input/select look for the page — the app has no shared form control
       outside .auth-card, and the filter row needs three. */
    .mail-input { min-width: 240px; padding: var(--sp-3); border: 0.5px solid var(--border2);
                  border-radius: var(--radius); background: var(--bg); color: var(--text);
                  font-size: var(--fs-base); font-family: inherit; }  /* off-scale: 240px */
    .mail-input:focus { outline: none; border-color: var(--blue); }

    .mail-filter { display: flex; gap: var(--sp-3); align-items: center; flex-wrap: wrap;
                   padding: 0 var(--sp-5) var(--sp-4); }
    .mail-filter .mail-input { flex: 1 1 220px; min-width: 0; }  /* off-scale: 220px */

    /* Paired with .card in the markup — the surface (background, radius,
       hairline) is the shared component; this adds only the clipping the rows
       need so their hover fill and separators stay inside the rounded corner. */
    .log-list  { overflow: hidden; }
    .log-row   { display: flex; align-items: flex-start; justify-content: space-between;
                 gap: var(--sp-4); padding: var(--sp-4) var(--sp-5);
                 border-bottom: 0.5px solid var(--border); }
    .log-row:last-child { border-bottom: none; }
    .log-row:hover      { background: var(--bg2); }
    .log-main  { min-width: 0; }
    .log-subj  { font-size: var(--fs-base); font-weight: 500; color: var(--text);
                 overflow-wrap: anywhere; }
    .log-meta  { font-size: var(--fs-sm); color: var(--text3); margin-top: var(--sp-1);
                 overflow-wrap: anywhere; }
    .log-err   { font-size: var(--fs-sm); color: var(--red); margin-top: var(--sp-2);
                 overflow-wrap: anywhere; }
    .log-empty { padding: var(--sp-6) var(--sp-5); text-align: center;
                 font-size: var(--fs-base); color: var(--text3); }

    /* consolidation: page-layout — the admin stat grid genuinely collapses
       4-col to 2-col, and the gutters tighten with the page. Lives here rather
       than in a template <style> so D4 has nothing left to flag. */
    @media (max-width: 768px) {
        .stat-grid    { grid-template-columns: 1fr 1fr; padding: var(--sp-4) var(--sp-4) 0; }
        .section      { padding: var(--sp-5) var(--sp-4) 0; }
        .page-head    { padding: var(--sp-5) var(--sp-4) 0; }
        .info-box, .warn-box, .alert-box { margin: var(--sp-4) var(--sp-4) 0; }
        .section-rule { margin: var(--sp-5) var(--sp-4); }
        .mail-filter  { padding: 0 var(--sp-4) var(--sp-4); }
        .log-row      { padding: var(--sp-4); }
    }
