html, body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

/* ============================================================
   Cosmyre page surface — THE DEFAULT PAGE LOOK.
   A page is a tinted, full-bleed SURFACE with its content floating
   on top in cards (see CLAUDE.md → "Standard page layout").

   Both the surface tint and the card look are driven by the tokens
   below, defined once on :root so light/dark (data-bs-theme) flips
   in a single place. This is the one source of truth for our default
   background color — never hardcode #F3EDF7 / #17141d again; reference
   var(--cosmyre-page-bg) (or the .page-surface class) instead.
   ============================================================ */
:root {
    color-scheme: light;
    --cosmyre-page-bg: #F3EDF7;                       /* default page background (light) */
    --cosmyre-card-bg: var(--bs-body-bg, #fff);
    --cosmyre-card-border: var(--bs-border-color, #e8e9ef);
    --cosmyre-card-radius: .65rem;
    --cosmyre-card-shadow: 0 .25rem .75rem rgba(20, 20, 50, .05);

    /* The sidebar's bottom strip (NavMenu's .sidebar-dock) and the viewport-fixed Navigator
       docked over it (CaseNavigator's .nav-dock) are both this tall, so the orb reads as the
       sidebar's own last row and the tree above never runs under it: .75rem pad + 3.4rem
       button (.4rem + 2.6rem orb + .4rem) + .75rem pad. Theme-independent. */
    --cosmyre-navdock-h: 4.9rem;

    /* ---- Accent family (see CLAUDE.md → "Standard buttons & controls") ----
       THE app accent. Light is the house violet; dark is the brand moon's own purple, and
       --cosmyre-accent-grad is the moon's crescent gradient (#784FF7 → #0549B9, sampled from
       cosmyre-mark.png — the same stops as #cosmyre-moon-grad in NavMenu). Fills (solid
       buttons, the tab keyline, selected list rows) paint the GRADIENT; text and hairlines
       use --cosmyre-accent-text / --cosmyre-accent, which stay readable at text weight.
       Never hardcode #845adf again — reference these tokens. */
    --cosmyre-accent: #845adf;
    --cosmyre-accent-rgb: 132, 90, 223;
    --cosmyre-accent-hover: #7549d4;
    --cosmyre-accent-fg: #fff;                        /* ink on an accent fill */
    --cosmyre-accent-text: #7443d6;                   /* accent at text weight (links, sort carets) */
    --cosmyre-accent-grad: linear-gradient(135deg, #8f63e6, #7443d6);

    /* Tertiary: matte tangerine. A muted, low-gloss orange for the third rank of emphasis
       (badges, tertiary buttons) — deliberately NOT Bootstrap warning yellow. */
    --cosmyre-tertiary: #c96f3e;
    --cosmyre-tertiary-rgb: 201, 111, 62;
    --cosmyre-tertiary-fg: #fff;
    --cosmyre-tertiary-hover: #b8622f;

    /* Accented controls (dropdown triggers + their panels — see "Cosmyre select"). */
    --cosmyre-ctl-bg: #f6f2fc;
    --cosmyre-ctl-border: #d9cbf3;
    --cosmyre-menu-bg: #f9f6fe;
    /* Grid header band: the accent composited onto the card so sticky headers stay opaque. */
    --cosmyre-thead-bg: color-mix(in srgb, var(--cosmyre-accent) 7%, var(--cosmyre-card-bg));

    /* Tab rail (see "Cosmyre tab bar" below). */
    --cosmyre-tab-color: #6f6880;                     /* resting label            */
    --cosmyre-tab-color-active: #17131f;              /* active + hovered label   */
    --cosmyre-tab-icon: #a9a3b8;                      /* resting icon             */
    --cosmyre-tab-accent: #845adf;                    /* keyline + active icon    */
    --cosmyre-tab-accent-rgb: 132, 90, 223;
    --cosmyre-tab-rule: #edeaf3;                      /* hairline the bar sits on */

    /* Throbber orbit (see "Throbber" below). Themed, unlike the rest of the orb: the mark is a
       saturated indigo that reads on either ground, but the star and its ring were lifted from
       the Navigator, which sits on a dark navy dock. On a white card that pale blue ring vanishes
       and the accent-coloured star is easy to miss — and the star IS the motion, so losing it
       loses the whole point of a throbber. Light gets a neon purple with a two-stage glow. */
    --cosmyre-throb-star: #b026ff;
    --cosmyre-throb-star-size: 5px;
    --cosmyre-throb-star-glow: 0 0 5px 2px rgba(176, 38, 255, .85),
                               0 0 14px 5px rgba(176, 38, 255, .40);
    --cosmyre-throb-ring: rgba(132, 90, 223, .45);
    --cosmyre-tab-bleed: 1.25rem;                     /* header card's side padding */

    /* ---- Modal design system (ModalShell + every builder that sits in it) ----
       The house palette for modals. It lives here, once, because a custom property
       declared inside a scoped .razor.css is scoped to that component's own elements,
       so sibling components could never share one — and because dark mode is then a
       matter of overriding the block below rather than hunting hexes through a
       thousand lines of layout CSS.
       NO HEX FOR MODAL UI MAY APPEAR OUTSIDE THESE TWO BLOCKS.
       Measurements (sizes, radii, padding) are NOT tokens — they live in
       ModalShell.razor.css, which is the one place that defines the dialog's shape. */
    --cosmyre-md-primary: #845adf;
    --cosmyre-md-primary-hover: #7549d4;
    --cosmyre-md-primary-tint: #f3eefc;
    --cosmyre-md-primary-shadow: rgba(132, 90, 223, .32);
    --cosmyre-md-ink: #17131f;
    --cosmyre-md-ink-2: #3d3748;
    --cosmyre-md-muted: #6f6880;                      /* operator text  */
    --cosmyre-md-muted-2: #8b8595;                    /* descriptions   */
    --cosmyre-md-muted-3: #a29cae;                    /* labels, hints  */
    --cosmyre-md-muted-4: #b3adbf;                    /* counts         */
    --cosmyre-md-muted-5: #c3bece;                    /* remove icons   */
    --cosmyre-md-grip: #cdc8d5;
    --cosmyre-md-hairline: #ece9f2;                   /* modal dividers   */
    --cosmyre-md-hairline-2: #efedf4;                 /* in-pane dividers */
    --cosmyre-md-control-border: #e4e1eb;
    --cosmyre-md-control-border-em: #e0dae9;
    --cosmyre-md-list-border: #e6e2ee;
    --cosmyre-md-row-border: #e8e4ef;
    --cosmyre-md-surface: #fff;                       /* cards, rows, rail */
    --cosmyre-md-canvas: #faf9fc;                     /* rules canvas      */
    --cosmyre-md-sunken: #fbfafd;                     /* list boxes        */
    --cosmyre-md-group-border: #e2dcee;
    --cosmyre-md-group-head: #f7f4fd;
    --cosmyre-md-group-rule: #ece7f7;
    --cosmyre-md-group-subrow: #fdfcfe;
    --cosmyre-md-band-rule: #c9b6f2;                  /* field-rule row band   */
    --cosmyre-md-band-ref: #e79aa4;                   /* saved-search row band */
    --cosmyre-md-ref-border: #f0dcdf;
    --cosmyre-md-ref-icon: #c4677a;
    --cosmyre-md-exclude-border: #eeced4;
    --cosmyre-md-exclude-bg: #fdf4f5;
    --cosmyre-md-exclude-fg: #b02a37;
    --cosmyre-md-and: #0f8a8a;                        /* bracket + pill, ALL  */
    --cosmyre-md-or: #d61f9c;                         /* bracket + pill, ANY  */
    --cosmyre-md-not: #b0561d;                        /* bracket + pill, NONE */
    --cosmyre-md-scrim: rgba(14, 10, 24, .52);

    /* Segmented control (view switch, category picker). */
    --cosmyre-md-segment-track: #f1eff6;
    --cosmyre-md-segment-border: #e7e3ee;

    /* "Any one of" group card — the OR-tinted counterpart of the group tokens above. */
    --cosmyre-md-orcard-border: #f2dcea;
    --cosmyre-md-orcard-head: #fdf5fa;
    --cosmyre-md-orcard-rule: #f7e6f0;
    --cosmyre-md-orcard-chip-border: #f2cee2;
    --cosmyre-md-orcard-chip-fg: #c2338a;

    /* Drop slots, shown only while a drag is in flight. Two variants so a slot says
       which stack it will drop into. */
    --cosmyre-md-slot-border: #cdbdf0;
    --cosmyre-md-slot-bg: #fbf9fe;
    --cosmyre-md-slot-fg: #a693d8;
    --cosmyre-md-slot-hot-border: #845adf;
    --cosmyre-md-slot-hot-bg: #f1eafc;
    --cosmyre-md-slot-hot-fg: #7549d4;
    --cosmyre-md-slot-or-border: #edc7de;
    --cosmyre-md-slot-or-bg: #fdf7fb;
    --cosmyre-md-slot-or-fg: #c58fba;
    --cosmyre-md-slot-or-hot-border: #d61f9c;
    --cosmyre-md-slot-or-hot-bg: #fdeaf5;
    --cosmyre-md-slot-or-hot-fg: #c2338a;

    /* Profile category identity (Individual / Business). */
    --cosmyre-md-cat-ind-fg: #2f7d70;
    --cosmyre-md-cat-ind-icon: #3f9184;
    --cosmyre-md-cat-ind-bg: #eaf4f1;
    --cosmyre-md-cat-ind-border: #cfe5df;
    --cosmyre-md-cat-bus-fg: #8a6a2c;
    --cosmyre-md-cat-bus-icon: #c08a3e;
    --cosmyre-md-cat-bus-bg: #f8f2e6;
    --cosmyre-md-cat-bus-border: #ebdcbe;

    /* Ambient tag dots. Named by hue, not by meaning, because two different
       taxonomies map onto them: a search field's Source (4 values) and a profile
       field's GroupName (7 values). Seven slots so every profile group has one —
       the handoff only coloured four, which left Protected Health Information,
       Access Information and Other PII Present with no identity. */
    --cosmyre-md-tag-teal: #4f9d8f;
    --cosmyre-md-tag-amber: #c08a3e;
    --cosmyre-md-tag-violet: #845adf;
    --cosmyre-md-tag-blue: #5a7fb5;
    --cosmyre-md-tag-rose: #c25e7a;
    --cosmyre-md-tag-cyan: #3f8fa8;
    --cosmyre-md-tag-slate: #7a7690;
    /* Workspace tokens (the Excel Extraction three-column coding surface). The tints are
       translucent overlays on purpose: they read correctly over both the light and the dark page
       background without a per-theme override, so the whole workspace inherits dark mode free.
       The two provenance colours and --cosmyre-conflict-text are the exceptions — solid, because
       they carry white text and have to hold their contrast on either theme. */
    --cosmyre-workspace-shadow: 0 .5rem 1.25rem rgba(20, 20, 50, .14);
    --cosmyre-pick-bg: rgba(99, 102, 241, .15);       /* selected list entry */
    --cosmyre-pick-hover: rgba(99, 102, 241, .07);

    /* Folder-tree connectors. Matte and translucent on purpose: structure the eye should be able
       to follow without it competing with the folder and search rows it joins. Alpha rather than
       a flat tint so it settles onto whichever surface the tree card is drawn on. */
    --cosmyre-tree-line: rgba(125, 110, 160, .45);
    /* Icon colour, app-wide. Every <Icon> outside the left nav renders in --cosmyre-icon; the
       carve-outs are listed on the .bi rule further down. --cosmyre-icon-edit marks the pencil,
       which reads as "edit" wherever it appears and is the one glyph that does not take the
       purple. Deep and matte on both counts — they sit with the folder-tree connector lines
       rather than competing with them. */
    --cosmyre-icon: #5f4b8b;
    --cosmyre-icon-edit: #a8447f;
    --cosmyre-icon-edit-hover: #87356a;
    --cosmyre-folder-icon: var(--cosmyre-icon);

    --cosmyre-map-dict: #5b21b6;                      /* value came from the header dictionary */
    --cosmyre-map-dict-tint: rgba(91, 33, 182, .08);
    --cosmyre-map-user: #20706c;                      /* value the reviewer picked by hand */
    --cosmyre-conflict-tint: rgba(255, 193, 7, .13);
    --cosmyre-conflict-border: #ffc107;
    --cosmyre-conflict-text: #997404;
    --cosmyre-mapped-text: #198754;                   /* the tick beside a cleanly mapped field */
    /* Height of the coding grid's frozen mapping-select row, and therefore the offset the
       frozen column-letter strip pins at. The two are coupled — see .xl-maprow-cell. */
    --xl-maprow-h: 2.6rem;

    /* ---- QC grid row states ----
       Solid, not translucent, unlike the coding tints above: these two answer "can this row be
       pulled?", and a wash that shifts with whatever is underneath is exactly the wrong reading.
       Both carry their own ink so they survive dark mode without a per-theme override. */
    --cosmyre-xl-blank: #4a4653;                      /* nothing in any cell     */
    --cosmyre-xl-blank-ink: #a9a3b8;
    --cosmyre-xl-rejected: #e879bd;                   /* has data, still dropped */
    --cosmyre-xl-rejected-ink: #2b1220;

    /* ---- QC block palette ----
       One tint per stacked table so a reviewer can see at a glance which block a row belongs to.
       Manilla leads because that is what the grid has always looked like — block 1 is unchanged
       from before multi-block existed, and the new colours only appear once a second block does.
       Light tints with dark ink for the same reason as above: readable on either theme. */
    /* A column the mapping pulls. Matte and light, NOT the solid --cosmyre-map-dict used on the
       coding grid's selects: there the fill sits behind a short label, here it sits behind rows of
       real data, and the solid violet read as a black stripe through the sheet. Muted enough to
       stay distinct from block 3's violet without competing with the data on top of it. */
    --cosmyre-xl-pulled: #c9b6e6;
    --cosmyre-xl-pulled-ink: #17131f;

    --cosmyre-xl-block-1: #f5e7c8;                    /* manilla   */
    --cosmyre-xl-block-2: #cfe2f5;                    /* light blue */
    --cosmyre-xl-block-3: #e2d6f7;                    /* violet     */
    --cosmyre-xl-block-4: #cfe9e4;                    /* teal       */
    --cosmyre-xl-block-5: #f7dcc4;                    /* amber      */
    --cosmyre-xl-block-6: #f7d3de;                    /* rose       */
    --cosmyre-xl-block-ink: #17131f;

    /* Border on a job-statistics tile. Matte and desaturated rather than the app's accent violet:
       these are hairlines around a dozen tiles at once, and the saturated purple turned a quiet
       panel into a grid of glowing boxes. */
    --cosmyre-xl-stat-border: #6b5f85;
}

/* Dark: swap the light lavender for a dark violet-tinted surface so cards pop. */
[data-bs-theme="dark"] {
    color-scheme: dark;
    --cosmyre-page-bg: #17141d;
    --cosmyre-tab-color: #9a94a6;
    --cosmyre-tab-color-active: #f4f1f8;
    --cosmyre-tab-icon: #7b7488;
    --cosmyre-tab-accent: #8b66f9;
    --cosmyre-tab-accent-rgb: 139, 102, 249;
    --cosmyre-tab-rule: #32373d;

    /* Dark accent IS the brand moon: flat #784FF7 for fills that can't take a gradient,
       the crescent's purple→blue for those that can. Text accents lift to #a88df8 so they
       hold contrast on the dark card (the moon purple alone is a fill colour, not an ink). */
    --cosmyre-accent: #784FF7;
    --cosmyre-accent-rgb: 120, 79, 247;
    --cosmyre-accent-hover: #8b66f9;
    --cosmyre-accent-fg: #fff;
    --cosmyre-accent-text: #a88df8;
    --cosmyre-accent-grad: linear-gradient(135deg, #784FF7, #0549B9);

    --cosmyre-tertiary: #e0925f;
    --cosmyre-tertiary-rgb: 224, 146, 95;
    --cosmyre-tertiary-fg: #241408;
    --cosmyre-tertiary-hover: #eaa678;

    --cosmyre-ctl-bg: #262039;
    --cosmyre-ctl-border: #43356e;
    --cosmyre-menu-bg: #221c33;

    /* Dark keeps the Navigator's own orbit, size included: this is the ground it was drawn for
       and it already reads there. Light needs the extra pixel and the wider halo, not dark. */
    --cosmyre-throb-star: #a888f6;
    --cosmyre-throb-star-size: 4px;
    --cosmyre-throb-star-glow: 0 0 6px 1px #a888f6;
    --cosmyre-throb-ring: rgba(150, 170, 255, .5);
    /* Lifted on the dark surface — the light value's alpha leaves it almost invisible there. */
    --cosmyre-tree-line: rgba(178, 165, 214, .42);
    --cosmyre-icon: #a294cc;
    --cosmyre-icon-edit: #e590c6;
    --cosmyre-icon-edit-hover: #f3b4dd;

    /* Search builder. The accents lift for contrast on a dark canvas; the surface
       tokens matter most, because the bracket and pill mix their colour AGAINST
       --cosmyre-md-surface — leave that white and the bracket becomes a glowing
       halo instead of a tint. */
    --cosmyre-md-primary: #a888f6;
    --cosmyre-md-primary-tint: #2a2440;
    --cosmyre-md-primary-shadow: rgba(168, 136, 246, .28);
    --cosmyre-md-ink: #f4f1f8;
    --cosmyre-md-ink-2: #cfc9da;
    --cosmyre-md-muted: #a49db2;
    --cosmyre-md-muted-2: #948da2;
    --cosmyre-md-muted-3: #8a8398;
    --cosmyre-md-muted-4: #7b7488;
    --cosmyre-md-muted-5: #6d6679;
    --cosmyre-md-grip: #5f5869;
    --cosmyre-md-hairline: #2e2937;
    --cosmyre-md-hairline-2: #2a2632;
    --cosmyre-md-control-border: #3a3446;
    --cosmyre-md-control-border-em: #423b50;
    --cosmyre-md-list-border: #332e3e;
    --cosmyre-md-row-border: #363040;
    --cosmyre-md-surface: #1f1b28;
    --cosmyre-md-canvas: #191622;
    --cosmyre-md-sunken: #221e2c;
    --cosmyre-md-group-border: #3a3350;
    --cosmyre-md-group-head: #262034;
    --cosmyre-md-group-rule: #322b45;
    --cosmyre-md-group-subrow: #241f2f;
    --cosmyre-md-band-rule: #7b5fc4;
    --cosmyre-md-band-ref: #b0616c;
    --cosmyre-md-ref-border: #4a3540;
    --cosmyre-md-ref-icon: #e08f9f;
    --cosmyre-md-exclude-border: #5c3540;
    --cosmyre-md-exclude-bg: #2e1f24;
    --cosmyre-md-exclude-fg: #f08d99;
    --cosmyre-md-and: #35c2bd;
    --cosmyre-md-or: #f472c0;
    --cosmyre-md-not: #e0904f;
    --cosmyre-md-scrim: rgba(4, 2, 8, .66);

    --cosmyre-md-primary-hover: #bda2f9;

    --cosmyre-md-segment-track: #232030;
    --cosmyre-md-segment-border: #322d3f;

    --cosmyre-md-orcard-border: #4a3346;
    --cosmyre-md-orcard-head: #2a1f2b;
    --cosmyre-md-orcard-rule: #3a2a38;
    --cosmyre-md-orcard-chip-border: #573b50;
    --cosmyre-md-orcard-chip-fg: #f18ac8;

    --cosmyre-md-slot-border: #4b3d68;
    --cosmyre-md-slot-bg: #241f33;
    --cosmyre-md-slot-fg: #9182bd;
    --cosmyre-md-slot-hot-border: #a888f6;
    --cosmyre-md-slot-hot-bg: #2e2547;
    --cosmyre-md-slot-hot-fg: #bda2f9;
    --cosmyre-md-slot-or-border: #573c4f;
    --cosmyre-md-slot-or-bg: #2c1f2a;
    --cosmyre-md-slot-or-fg: #b07fa0;
    --cosmyre-md-slot-or-hot-border: #f472c0;
    --cosmyre-md-slot-or-hot-bg: #3a2334;
    --cosmyre-md-slot-or-hot-fg: #f18ac8;

    --cosmyre-md-cat-ind-fg: #6dbcac;
    --cosmyre-md-cat-ind-icon: #6dbcac;
    --cosmyre-md-cat-ind-bg: #1d2e2b;
    --cosmyre-md-cat-ind-border: #2f4a45;
    --cosmyre-md-cat-bus-fg: #dda85c;
    --cosmyre-md-cat-bus-icon: #dda85c;
    --cosmyre-md-cat-bus-bg: #302719;
    --cosmyre-md-cat-bus-border: #4d3f28;

    --cosmyre-md-tag-teal: #6dbcac;
    --cosmyre-md-tag-amber: #dda85c;
    --cosmyre-md-tag-violet: #a888f6;
    --cosmyre-md-tag-blue: #7fa3d6;
    --cosmyre-md-tag-rose: #e2899f;
    --cosmyre-md-tag-cyan: #6bb8d2;
    --cosmyre-md-tag-slate: #a7a2bb;
    /* #997404 on a dark card fails contrast; lift it to the Bootstrap warning tint. */
    --cosmyre-conflict-text: #ffda6a;
    --cosmyre-mapped-text: #75b798;
}

/* ============================================================
   STANDARD CONTROLS — buttons, dropdowns, scrollbars, grids.
   See CLAUDE.md → "Standard buttons & controls". This block sits directly
   after the token blocks ON PURPOSE: anything page-specific written further
   down the file still wins a specificity tie against it.
   ============================================================ */

/* ---- Scrollbars: accent thumb everywhere, transparent track. The standard properties
   only — Chromium ≥121 and Firefox both honour them, and once scrollbar-color is set
   Chromium ignores ::-webkit-scrollbar anyway. color-scheme on the two token blocks is
   what keeps any unstyled corner (and native form controls) matching the theme. ---- */
* {
    scrollbar-color: rgba(var(--cosmyre-accent-rgb), .5) transparent;
}

/* ---- Buttons (see CLAUDE.md) ----
   Two ranks. A SINGLE-ACTION button (Add, Remove, Cancel, Edit, Refresh…) is .btn-quiet:
   icon + label, no fill, no border — the icon sits left in the app's icon purple, the
   label right. Everything else is SOLID: .btn-primary/.btn-accent paints the accent
   gradient; a button with a real colour semantic keeps its Bootstrap solid (danger,
   warning, success). Trash-can icon buttons (TrashButton) are outside both rules. */
.btn-quiet {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    background: transparent;
    border: 1px solid transparent;
    color: var(--bs-body-color);
    font-weight: 500;
}

.btn-quiet:hover,
.btn-quiet:focus-visible {
    background: rgba(var(--cosmyre-accent-rgb), .1);
    color: var(--cosmyre-accent-text);
}

.btn-quiet:disabled {
    background: transparent;
    color: var(--bs-secondary-color);
    opacity: .55;
}

/* The quiet button's glyph keeps the app icon purple at rest (beats the `.btn .bi`
   inherit carve-out below on specificity), and joins the label's accent on hover. */
.btn.btn-quiet .bi { color: var(--cosmyre-icon); }
.btn.btn-quiet:hover .bi,
.btn.btn-quiet:focus-visible .bi { color: inherit; }

/* Destructive single actions that are not the trash can: quiet, but in danger ink. */
.btn-quiet-danger { color: var(--bs-danger); }
.btn-quiet-danger:hover,
.btn-quiet-danger:focus-visible {
    background: rgba(220, 53, 69, .1);
    color: var(--bs-danger);
}
.btn.btn-quiet-danger .bi { color: inherit; }

/* Solid accent — THE default solid button. .btn-primary is restyled (not aliased) so the
   91 existing call sites pick it up without renaming; .btn-accent is the same thing under
   the house name. The fill is the accent GRADIENT (the moon's crescent in dark). */
.btn-primary,
.btn-accent {
    --bs-btn-color: var(--cosmyre-accent-fg);
    --bs-btn-bg: var(--cosmyre-accent);
    --bs-btn-border-color: transparent;
    --bs-btn-hover-color: var(--cosmyre-accent-fg);
    --bs-btn-hover-bg: var(--cosmyre-accent-hover);
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-color: var(--cosmyre-accent-fg);
    --bs-btn-active-bg: var(--cosmyre-accent-hover);
    --bs-btn-active-border-color: transparent;
    --bs-btn-disabled-color: var(--cosmyre-accent-fg);
    --bs-btn-disabled-bg: var(--cosmyre-accent);
    --bs-btn-disabled-border-color: transparent;
    background-image: var(--cosmyre-accent-grad);
}

/* The gradient hides Bootstrap's hover background-color swap, so hover/active feedback
   is a brightness step on the gradient itself. */
.btn-primary:hover, .btn-accent:hover { filter: brightness(1.08); }
.btn-primary:active, .btn-accent:active { filter: brightness(.94); }
.btn-primary:disabled, .btn-accent:disabled { filter: none; }

/* Solid tertiary — the matte tangerine rank. */
.btn-tertiary {
    --bs-btn-color: var(--cosmyre-tertiary-fg);
    --bs-btn-bg: var(--cosmyre-tertiary);
    --bs-btn-border-color: transparent;
    --bs-btn-hover-color: var(--cosmyre-tertiary-fg);
    --bs-btn-hover-bg: var(--cosmyre-tertiary-hover);
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-color: var(--cosmyre-tertiary-fg);
    --bs-btn-active-bg: var(--cosmyre-tertiary-hover);
    --bs-btn-active-border-color: transparent;
    --bs-btn-disabled-color: var(--cosmyre-tertiary-fg);
    --bs-btn-disabled-bg: var(--cosmyre-tertiary);
    --bs-btn-disabled-border-color: transparent;
}

/* Accent outline, for the stragglers that keep btn-outline-primary. */
.btn-outline-primary {
    --bs-btn-color: var(--cosmyre-accent-text);
    --bs-btn-border-color: var(--cosmyre-accent);
    --bs-btn-hover-color: var(--cosmyre-accent-fg);
    --bs-btn-hover-bg: var(--cosmyre-accent);
    --bs-btn-hover-border-color: var(--cosmyre-accent);
    --bs-btn-active-color: var(--cosmyre-accent-fg);
    --bs-btn-active-bg: var(--cosmyre-accent-hover);
    --bs-btn-active-border-color: var(--cosmyre-accent-hover);
    --bs-btn-disabled-color: var(--cosmyre-accent-text);
    --bs-btn-disabled-border-color: var(--cosmyre-accent);
}

/* ---- Dropdowns (see CLAUDE.md) ----
   The closed control: every .form-select carries the accent tint. The OPEN list is not
   the OS popup any more — js/select.js suppresses it and floats .csel-panel instead,
   with a filter box pinned at its top. The native <select> stays in the markup and stays
   the thing Blazor binds to; the panel only sets its value and dispatches `change`. */
.form-select {
    background-color: var(--cosmyre-ctl-bg);
    border-color: var(--cosmyre-ctl-border);
}

.form-select:focus {
    border-color: var(--cosmyre-accent);
    box-shadow: 0 0 0 .25rem rgba(var(--cosmyre-accent-rgb), .25);
}

/* The floating option panel (js/select.js appends it to <body>, positioned to the
   trigger; it never lives inside a Blazor-managed subtree). */
.csel-panel {
    position: fixed;
    z-index: 1085;                       /* over modals (ModalShell's frame sits at 1055) */
    display: flex;
    flex-direction: column;
    min-width: 10rem;
    max-height: 19rem;
    overflow: hidden;
    background: var(--cosmyre-menu-bg);
    border: 1px solid var(--cosmyre-ctl-border);
    border-radius: .5rem;
    box-shadow: 0 .5rem 1.25rem rgba(20, 20, 50, .25);
}

/* The filter option, pinned at the top of every dropdown. */
.csel-filter {
    flex: 0 0 auto;
    margin: .45rem .45rem .3rem;
    padding: .25rem .55rem;
    font-size: .8125rem;
    color: var(--bs-body-color);
    background: var(--cosmyre-card-bg);
    border: 1px solid var(--cosmyre-ctl-border);
    border-radius: .4rem;
}

.csel-filter:focus {
    outline: none;
    border-color: var(--cosmyre-accent);
    box-shadow: 0 0 0 .2rem rgba(var(--cosmyre-accent-rgb), .2);
}

.csel-list {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: .25rem .45rem .45rem;
}

.csel-opt {
    padding: .3rem .6rem;
    border-radius: .35rem;
    font-size: .845rem;
    color: var(--bs-body-color);
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.csel-opt:hover,
.csel-opt.csel-active {
    background: rgba(var(--cosmyre-accent-rgb), .12);
}

.csel-opt.csel-selected {
    background: var(--cosmyre-accent);
    background-image: var(--cosmyre-accent-grad);
    color: var(--cosmyre-accent-fg);
}

.csel-opt[aria-disabled="true"] {
    color: var(--bs-secondary-color);
    cursor: default;
    background: transparent;
}

.csel-group {
    padding: .45rem .6rem .15rem;
    font-size: .7rem;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
}

.csel-empty {
    padding: .4rem .6rem;
    font-size: .8125rem;
    font-style: italic;
    color: var(--bs-secondary-color);
}

/* ---- Info hint (Components/Shared/InfoHint.razor + js/tips.js) ----
   The icon a page's long description hangs on: buttons first, then this at the action
   row's end. The panel is a <body> child at fixed position (tips.js), themed like the
   select panel, so a card's overflow can never clip it. */
.info-hint {
    display: inline-flex;
    align-items: center;
    cursor: help;
    border-radius: .25rem;
}

.info-hint:focus-visible {
    outline: 2px solid var(--cosmyre-accent);
    outline-offset: 1px;
}

/* ---- Field strategies workspace (Components/Shared/SurvivorshipModal.razor) ----
   Two panes inside a modal body: the field list, and the clusters that speak for some of them.
   .md-body is overflow:hidden and not a flex column, so the workspace supplies its own height. */
.sv-workspace {
    height: 100%;
    display: grid;
    /* Three layers, as the Excel workspaces do it: --sv-trail (folded) beats --dv-trail
       (dragged, written by js/splitters.js) beats the default, and the minmax floor stops a drag
       crushing the pane to nothing. */
    grid-template-columns: minmax(0, 1fr) var(--sv-trail, minmax(12rem, var(--dv-trail, 20rem)));
    column-gap: 0;
    row-gap: 2px;
    align-items: stretch;
}

/* Folded, the trailing pane becomes the usual 2.75rem strip (app.css "Collapsible panes").
   Keyed on .dv-trail, the class the splitter already needs, so both modals that use this
   workspace -- field strategies, and the group modal's merged values -- fold the same way. */
.sv-workspace:has(> .dv-trail.pane-collapsed) { --sv-trail: 2.75rem; }

.sv-workspace > * { min-width: 0; }

/* Stacked, the two panes are taller than the dialog body, which is overflow:hidden — so without
   a scroller the trailing pane, its fold button and the splitter are simply cut off with no way
   to reach them. The three-pane workspaces cap their cards for the same reason. */
@media (max-width: 991.98px) {
    .sv-workspace {
        grid-template-columns: 1fr;
        overflow-y: auto;
    }

    .sv-workspace > .fill-card { max-height: 24rem; }
}

/* One field per line: name, what happens to it, which cluster owns it. */
.sv-group {
    font-size: .6875rem;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
    margin: .75rem 0 .25rem;
}

.sv-group:first-child { margin-top: 0; }

.sv-row {
    display: flex;
    align-items: center;
    gap: .4rem;
    padding: .25rem 0;
    border-bottom: 1px solid var(--cosmyre-ctl-border);
}

.sv-field {
    flex: 1 1 auto;
    min-width: 8rem;
    font-size: .8125rem;
}

.sv-follows {
    flex: 0 0 auto;
    font-size: .75rem;
    color: var(--bs-secondary-color);
    display: inline-flex;
    align-items: center;
    gap: .25rem;
}

.sv-strategy { flex: 0 0 12rem; }
.sv-opt { flex: 0 0 4rem; }
.sv-opt-wide { flex: 0 0 10rem; }
.sv-cluster { flex: 0 0 8rem; }

.sv-cluster-card {
    padding: .6rem;
    margin-bottom: .6rem;
    border: 1px solid var(--cosmyre-ctl-border);
    border-radius: .5rem;
}

.sv-chips {
    display: flex;
    flex-wrap: wrap;
    gap: .25rem;
}

.sv-chip {
    font-size: .6875rem;
    padding: .1rem .4rem;
    border-radius: .35rem;
    background: var(--cosmyre-thead-bg);
}

@media (max-width: 991.98px) {
    .sv-workspace { grid-template-columns: 1fr; }
}

/* ---- Merged values (Prime Profiles' group modal) ----
   One field per line rather than a grid: this is a read-back of a single record, and column
   headers over field/value pairs say nothing the labels already do. Stacked rather than columned
   so the pane stays readable when a splitter takes it narrow -- a grid would scroll sideways. */
.mv-row {
    padding: .4rem 0;
    border-bottom: 1px solid var(--bs-border-color);
}

.mv-row:last-child { border-bottom: 0; }

.mv-label {
    font-size: .6875rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
}

.mv-value {
    font-size: .875rem;
    line-height: 1.4;
    overflow-wrap: anywhere;    /* a long address or a delimited list must wrap, never widen the pane */
}

.mv-from {
    display: flex;
    align-items: center;
    gap: .25rem;
    font-size: .75rem;
    color: var(--bs-secondary-color);
}

.mv-pinned .mv-value { font-weight: 600; }

/* Quiet until wanted: a reviewer reads this pane far more often than they unpin from it. */
.mv-unpin { opacity: 0; padding: 0 .25rem; }

.mv-row:hover .mv-unpin,
.mv-unpin:focus-visible { opacity: 1; }

/* ---- Member value cells (Prime Profiles' group modal, the Review workspace) ----
   A member's value, with the control that makes it the prime profile's answer. The pin is its own
   button because the cell itself opens an editor on double-click; quiet until the pointer is over
   the cell, so a grid of twenty fields is not a wall of icons, but ALWAYS shown once pinned --
   that is the cell explaining why the prime reads the way it does. Focus-visible keeps it
   reachable by keyboard, where there is no hover to reveal it. */
.pick-cell {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
}

.pick-pin {
    flex: 0 0 auto;
    display: inline-flex;
    padding: 0;
    border: 0;
    border-radius: .25rem;
    background: transparent;
    color: var(--bs-secondary-color);
    opacity: 0;
    cursor: pointer;
}

.dnt-table td:hover .pick-pin,
.pick-pin:focus-visible,
.pick-pin-on { opacity: 1; }

.pick-pin-on,
.pick-pin:hover { color: var(--cosmyre-accent-text); }

/* ---- Rich hints (Components/Shared/RichHint.razor + StrategyDemo.razor) ----
   A hint whose panel holds a worked example rather than a sentence. The anchor behaves like
   .info-hint; the panel is the same <body>-child .tip-pop, widened, with its content cloned from
   a hidden source by js/tips.js. The source itself must never be visible or measurable. */
.rich-hint {
    display: inline-flex;
    align-items: center;
    color: var(--cosmyre-accent-text);
    cursor: help;
    line-height: 1;
}

.rich-hint:hover,
.rich-hint:focus-visible { color: var(--cosmyre-accent); }

.rich-hint-src { display: none !important; }

.tip-pop-rich {
    max-width: 26rem;
    padding: .75rem .85rem;
    /* Above .csel-panel, which shares 1085: an option's explanation that appeared UNDER the open
       dropdown would be worse than no explanation. */
    z-index: 1090;
}

/* ---- Strategy demo ---- */
.sd-title {
    font-size: .6875rem;
    font-weight: 700;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--cosmyre-accent-text);
    margin-bottom: .25rem;
}

.sd-lead {
    font-size: .8125rem;
    line-height: 1.45;
    margin-bottom: .5rem;
}

.sd-table {
    width: 100%;
    border-collapse: collapse;
    font-size: .78125rem;
}

.sd-table td {
    padding: .2rem .4rem;
    border-bottom: 1px solid var(--cosmyre-ctl-border);
}

.sd-who {
    color: var(--bs-secondary-color);
    white-space: nowrap;
    width: 1%;
}

.sd-val { font-family: var(--bs-font-monospace); }

.sd-mark {
    width: 1%;
    color: var(--bs-success);
    font-weight: 700;
}

/* The winning rows light in sequence, so the eye follows WHY before seeing the answer. --sd-step
   staggers each row; the loop is long enough to read and pauses at the end rather than snapping. */
.sd-win {
    animation: sd-pulse 4.5s ease-in-out infinite;
    animation-delay: calc(var(--sd-step, 0) * .45s);
}

.sd-win .sd-val { font-weight: 600; }

@keyframes sd-pulse {
    0%, 8% { background: transparent; }
    16%, 55% { background: color-mix(in srgb, var(--cosmyre-accent) 16%, transparent); }
    70%, 100% { background: transparent; }
}

.sd-result td {
    border-bottom: none;
    border-top: 2px solid var(--cosmyre-ctl-border);
    padding-top: .35rem;
    font-weight: 600;
}

.sd-result .sd-val { color: var(--cosmyre-accent-text); }

.sd-note {
    margin-top: .5rem;
    font-size: .75rem;
    line-height: 1.45;
    color: var(--bs-secondary-color);
}

/* Motion is decoration here: the example reads perfectly well still. */
@media (prefers-reduced-motion: reduce) {
    .sd-win {
        animation: none;
        background: color-mix(in srgb, var(--cosmyre-accent) 16%, transparent);
    }
}

.tip-pop {
    position: fixed;
    z-index: 1085;
    max-width: 34rem;
    padding: .6rem .8rem;
    font-size: .8125rem;
    line-height: 1.5;
    color: var(--bs-body-color);
    background: var(--cosmyre-menu-bg);
    border: 1px solid var(--cosmyre-ctl-border);
    border-radius: .5rem;
    box-shadow: 0 .5rem 1.25rem rgba(20, 20, 50, .25);
    pointer-events: none;
}

/* The one Bootstrap menu dropdown (the header user menu) joins the same theme. */
.dropdown-menu {
    --bs-dropdown-bg: var(--cosmyre-menu-bg);
    --bs-dropdown-border-color: var(--cosmyre-ctl-border);
    --bs-dropdown-link-hover-bg: rgba(var(--cosmyre-accent-rgb), .12);
    --bs-dropdown-link-active-color: var(--cosmyre-accent-fg);
    --bs-dropdown-link-active-bg: var(--cosmyre-accent);
}

/* ---- Data grids in theme: accent header band + accent row hover. The band token
   composites the accent over the card colour so DataTable's sticky headers stay opaque. */
.table {
    --bs-table-hover-bg: rgba(var(--cosmyre-accent-rgb), .06);
}

.dnt-table thead tr:first-child th {
    background-color: var(--cosmyre-thead-bg);
}

/* The active sort caret is the accent, not the icon purple. */
.dnt-sortable .bi { color: var(--cosmyre-accent-text); }

/* ---- Collapsible cards (see CLAUDE.md) ----
   An OUTER card (a standalone card on a scrolling page, a stacked pane) collapses to its
   header row. The toggle is a .card-collapse-btn carrying data-collapse-key, placed in
   the card's FIRST child (its header row); js/cards.js toggles .card-collapsed on the
   nearest card and persists the state per key. The contract is structural on purpose:
   everything after the first child hides. Side-by-side panes don't collapse — they
   resize (see splitters below). */
.card-collapse-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 1.6rem;
    height: 1.6rem;
    padding: 0;
    border: 0;
    border-radius: .35rem;
    background: transparent;
    color: var(--bs-secondary-color);
    cursor: pointer;
}

.card-collapse-btn:hover {
    background: rgba(var(--cosmyre-accent-rgb), .1);
    color: var(--cosmyre-accent-text);
}

.card-collapse-btn .bi { transition: transform .2s ease; }

.card-collapsed > .card-collapse-btn .bi,
.card-collapsed > :first-child .card-collapse-btn .bi { transform: rotate(-90deg); }

.card-collapsed > :not(:first-child) { display: none !important; }

/* A collapsed fill card stops flexing and shrinks to its header. */
.card-collapsed {
    flex: 0 0 auto !important;
    min-height: 0 !important;
}

/* Belt and braces for the rule stated above: a pane in a side-by-side ROW must not collapse.
   The contract breaks twice over there — `:not(:first-child)` hides nothing when the pane's only
   child is a .card-body, and `flex: 0 0 auto` sizes WIDTH in a row container, pinning the pane to
   max-content and blowing out its neighbour. The state persists per key, so it survives reload.
   Hiding the affordance is what keeps a pane conversion from silently reintroducing this. */
.list-page-row .card-collapse-btn,
.snap-workspace .card-collapse-btn,
.search-workspace .card-collapse-btn,
.term-workspace .card-collapse-btn,
.cargo-panes .card-collapse-btn,
.xl-workspace .card-collapse-btn,
.xl-workspace-qc .card-collapse-btn,
.pr-workspace .card-collapse-btn,
.xl-jobs-grid .card-collapse-btn,
.usel-panes .card-collapse-btn {
    display: none;
}

/* ---- Pane splitters (see CLAUDE.md) ----
   Multi-pane workspaces are resizable. A .pane-split element with data-splitter="name"
   rides the trailing edge of the pane it sizes (the pane also carries class dv-<name>);
   js/splitters.js drags it, auto-initialised via data-split-root="<prefix>" on the pane
   CONTAINER. Sizes persist per user in localStorage; double-click resets.

   The splitter is an absolutely-positioned OVERLAY on the pane's edge, never a grid/flex
   track of its own: tracks would shift every sibling index (breaking nth-child rules and
   the narrow-width templates), while an overlay leaves the layout untouched. It works
   because panes butt together with no gutter, and every pane class (.fill-card,
   .list-page-list, .xl-card) is already position: relative for the throbber. The pane's
   overflow: hidden clips anything outside it, so the handle sits fully INSIDE the edge.
   Invisible at rest; glows accent under the pointer. */
.pane-split {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 0;
    width: 7px;
    z-index: 6;                     /* over the pane's sticky heads and throbber overlay */
    padding: 0;
    border: 0;
    background: transparent;
    cursor: col-resize;
    touch-action: none;
}

/* Start-edge variant: for a RIGHT-hand pane, the handle rides its LEFT edge (pair it
   with data-invert so dragging away from the pane shrinks it). */
.pane-split-start {
    right: auto;
    left: 0;
}

/* Vertical variant: rides the pane's bottom edge, resizes height (data-axis="y"). */
.pane-split-y {
    top: auto;
    left: 0;
    right: 0;
    bottom: 0;
    width: auto;
    height: 7px;
    cursor: row-resize;
}

.pane-split:hover,
.dv-dragging .pane-split {
    background: rgba(var(--cosmyre-accent-rgb), .35);
}

/* ---- Collapsible panes (three-column workspaces) ----
   A SIDE pane of the Excel workspaces carries a .pane-collapse-btn[data-pane-key] in its head row
   (.pane-head); js/cards.js toggles .pane-collapsed on the pane. Collapsed, the pane is a narrow
   vertical strip: its grid track shrinks (the :has() rules below set --xl-lead / --xl-trail, which
   the workspaces' grid-template-columns read), everything but the head hides, and the head turns
   on its side so its title reads upward with the chevron on top. This is NOT .card-collapsed: that
   folds a STACKED card to its header bar and pins flex: 0 0 auto, which in a row means width — the
   wrong shape for a pane, and why .card-collapse-btn is hidden inside pane containers above.
   Only side panes carry the button; the middle pane is the work surface, not a reference. */
.xl-workspace:has(> .dv-queue.pane-collapsed),
.xl-workspace-qc:has(> .dv-queue.pane-collapsed) {
    --xl-lead: 2.75rem;
}

.xl-workspace:has(> .dv-panel.pane-collapsed),
.xl-workspace-qc:has(> .dv-editor.pane-collapsed) {
    --xl-trail: 2.75rem;
}

.snap-workspace:has(> .dv-main.pane-collapsed) {
    --snap-lead: 2.75rem;
}

.pane-collapse-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    margin-left: auto;
    width: 1.5rem;
    height: 1.5rem;
    padding: 0;
    border: 0;
    border-radius: .35rem;
    background: transparent;
    color: var(--bs-secondary-color);
    cursor: pointer;
}

.pane-collapse-btn:hover {
    background: var(--bs-secondary-bg);
    color: var(--bs-body-color);
}

.pane-collapsed {
    padding: .5rem .25rem !important;   /* the pane's own p-3 would eat the whole 2.75rem */
    overflow: hidden;
}

/* Body, filter, footer, and the pane's own splitter all go — a 2.75rem strip has nothing to drag. */
.pane-collapsed > :not(.pane-head) {
    display: none !important;
}

/* The head becomes the strip. writing-mode alone reads downward; the 180° turn makes the title read
   upward and, with the children in DOM order title · count · hint · button, puts the BUTTON at the
   top. The chevron turns with it, so the queue's chevron-left reads as chevron-right ("expand")
   once collapsed — no icon swap. InfoHint tooltips are body children positioned from the icon's
   box, so they still open upright beside the strip. */
.pane-collapsed > .pane-head {
    /* !important because the pane heads carry their own flex: 0 0 auto — three inline on the Excel
       pages, two from scoped sheets (which load after app.css and win the tie). Collapsed, the head
       IS the strip and has to take the height, or its bottom border lands partway down it. */
    flex: 1 1 auto !important;
    min-height: 0;
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    /* Centred along the strip, and !important for the same reason as flex above: the expanded
       heads set their own justify-content (Dashboard's .side-card-head is space-between, which
       collapsed would shove the label to one end and the chevron to the other) from scoped sheets
       that load after this one and win the tie. Without it the label sat at the bottom on the
       Dashboard and the top on Snapshot — same rule, two different inherited values. */
    justify-content: center !important;
    gap: .75rem;
    padding: 0 !important;
    margin: 0 !important;
}

.pane-collapsed > .pane-head .pane-collapse-btn {
    margin-left: 0;
}

@media (max-width: 991.98px) {
    /* Every pane grid stacks to one column here; a drag handle between stacked cards
       resizes nothing real, so it goes away with the layout it belonged to. */
    .pane-split { display: none; }
}

/* Default page wrapper: a tinted surface that bleeds past .content's
   padding-top (1.1rem) and the article's px-4 (1.5rem) out to the viewport
   edges, and fills at least the viewport height below the 3.75rem app header.
   Wrap a page's whole body in this, then lay content out in .surface-card cards. */
.page-surface {
    background: var(--cosmyre-page-bg);
    margin: -1.1rem -1.5rem 0;
    padding: 1.1rem 1.5rem 2rem;
    min-height: calc(100vh - 3.75rem);
}

/* The standard card that floats on .page-surface (Bootstrap's .card also works —
   this token-driven class just matches the dashboard/snapshot card styling). */
.surface-card {
    background: var(--cosmyre-card-bg);
    border: 1px solid var(--cosmyre-card-border);
    border-radius: var(--cosmyre-card-radius);
    box-shadow: var(--cosmyre-card-shadow);
}

/* ---- Record-list page (the Documents / Cases pattern) ----
   A full-height page surface holding a pinned header card above a list card that
   fills the remaining height; the DataTable scrolls INSIDE the list card (sticky
   header, both axes) so the scrollbars sit at the card's edges. Use the list card
   with <DataTable FillHeight="true">. See CLAUDE.md → "Standard page layout".
   These are global (not scoped) so a plain descendant selector reaches into the
   DataTable child component without ::deep. */
.list-page {
    display: flex;
    flex-direction: column;
    height: calc(100vh - 3.75rem);
    margin: -1.1rem -1.5rem 0;
    padding: 1.1rem 1.5rem;
    background: var(--cosmyre-page-bg);
}

/* ---- Card panes -------------------------------------------------------------------------
   Cards that share a page butt together across and are parted by a hairline down. Two axes, not
   one gap: every pane grid below collapses to a single column at a narrow breakpoint, and written
   this way the same declaration stays right on both sides of it - side by side they touch, stacked
   they get the 2px. The rail (.section-card-detached) is the one seam that closes completely,
   because it is a header for what follows rather than a pane beside it. */

/* Page-name / controls card, pinned above the list card. */
.list-page-head {
    flex: 0 0 auto;
    margin-bottom: 2px;
    border-radius: .75rem;
    box-shadow: 0 .25rem .75rem rgba(20, 20, 50, .06);
}

/* Muted count pill for the header card, and for a section card's meta row. Two names, one rule,
   aliased the way .xl-page aliases .page-fill — so they can never drift apart. */
.list-page-count,
.section-card-count {
    background: var(--bs-secondary-bg, #e9ecef);
    color: var(--bs-secondary-color, #6b7280);
    font-weight: 600;
}

/* The list card floats and fills the rest of the height. */
.list-page-list {
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;                 /* clip the table to the card's rounded corners */
    display: flex;
    flex-direction: column;
    border-radius: .75rem;
    box-shadow: 0 .35rem 1rem rgba(20, 20, 50, .09);
}

/* The DataTable's scroll area grows to fill the card; height:auto overrides the
   component's height:100% so flex controls the size and inner scrolling works. */
.list-page-list .dnt-table.dnt-fill {
    flex: 1 1 auto;
    min-height: 0;
    height: auto;
}

/* The pager (next sibling of the scroll area) stays pinned at the card's bottom. */
.list-page-list .dnt-table.dnt-fill + div {
    flex: 0 0 auto;
    padding: .5rem .75rem;
    border-top: 1px solid var(--bs-border-color);
    background: var(--cosmyre-card-bg);
}

/* Nudge the first column in so it isn't flush to the card's edge. */
.list-page-list table th:first-child,
.list-page-list table td:first-child {
    padding-left: 1rem;
}

/* Two list cards sharing the remaining height under a pinned header (e.g. the Grouping
   page's Groups | Members panes). Each pane is a .list-page-list, so its table scrolls
   inside the card — the page itself never scrolls. */
.list-page-row {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    /* Panes touch across, hairline down: see "Card panes" in app.css. */
    column-gap: 0;
    row-gap: 2px;
}

.list-page-row > .list-page-list {
    flex: 1 1 0;
    min-width: 0;
    /* When a pane's pinned content (an expanded bulk-coding block, a long header) leaves
       too little room, the pane scrolls itself — the page never grows. */
    overflow-y: auto;
}

/* The fill table keeps a usable minimum height inside a row pane instead of being
   squeezed to nothing by the pinned content above it. */
.list-page-row .dnt-table.dnt-fill {
    min-height: 10rem;
}

@media (max-width: 991.98px) {
    .list-page-row {
        flex-direction: column;
    }
}

/* Two list cards stacked under a pinned header (e.g. the Workers page's Fleet | Job queue
   panes). Same contract as .list-page-row: each pane is a .list-page-list whose table
   scrolls inside the card, so the page itself never scrolls. */
.list-page-col {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    /* Stacked panes: a hairline, not nothing - see "Card panes" in app.css. */
    gap: 2px;
}

.list-page-col > .list-page-list {
    flex: 1 1 0;
    min-height: 0;
}

/* The fill table keeps a usable minimum height inside a stacked pane instead of being
   squeezed to nothing by the card header above it. */
.list-page-col .dnt-table.dnt-fill {
    min-height: 8rem;
}

/* Splitter-driven pane sizing (see "Pane splitters" in the STANDARD CONTROLS block).
   The LEADING pane opts in with .dv-lead; the fallback is the even split it had anyway.
   These sit AFTER the panes' own flex rules on purpose — same specificity, so order is
   what lets the driven size win. Grid workspaces instead consume their --dv-* var in
   grid-template-columns. */
.list-page-row > .dv-lead { flex: 0 0 var(--dv-lead, 50%); }
.list-page-col > .dv-lead { flex: 0 0 var(--dv-lead, 50%); }

@media (max-width: 991.98px) {
    /* Stacked to one column, a driven size resizes nothing real — back to the even split. */
    .list-page-row > .dv-lead,
    .list-page-col > .dv-lead { flex: 1 1 0; }
}

/* ---- Coding workspace (Excel Extraction) ----------------------------------------------
   A three-column working surface — group queue | coding area | mapped-headers panel —
   rather than the stacked-card record-list pattern above. align-items: stretch is what
   makes all three cards share a height regardless of content. These are global (not
   scoped) so the column tints below can reach into the CsvCodingGrid child component
   without ::deep, the same reason .list-page-list reaches into DataTable. */
/* The page itself never scrolls: it is exactly the viewport below the 3.75rem app header, and
   each card scrolls its own body. Same contract as .list-page, which CLAUDE.md allows a page to
   substitute for .page-surface as long as the background stays on the token. */
/* ---- Fill-and-scroll: the house rule for card containers ---------------------------------
   A page that holds LISTS is exactly the viewport below the 3.75rem app header and never scrolls
   itself; each card fits the height available and scrolls its own body. .page-fill is the wrapper,
   .fill-card the card, .fill-card-body the one part inside it that scrolls.

   .xl-page / .xl-card / .xl-card-body are the Excel workspaces' original names, kept as aliases
   and defined HERE in the same rule so the two can never drift apart.

   This applies to EVERY page in a tab group, forms included — there is no long-form exception any
   more. A form does not need one: the section card's action row is already flex: 0 0 auto, so a
   Save button in the Actions slot pins above the scroll instead of riding away below it, which is
   strictly better than the page scroll it replaced. What a form DOES need is its error alert in
   the Notices slot, for the same reason — an alert left in the body can be carried out of sight.
   See CLAUDE.md → "Standard page layout". */
.page-fill,
.xl-page {
    display: flex;
    flex-direction: column;
    height: calc(100vh - 3.75rem);
    margin: -1.1rem -1.5rem 0;
    padding: 1.1rem 1.5rem;
    background: var(--cosmyre-page-bg);
}

.xl-workspace {
    flex: 1 1 auto;
    min-height: 0;                    /* lets the grid shrink so its children can scroll */
    display: grid;
    /* The queue and panel columns are splitter-driven (.pane-split overlays, see
       "Pane splitters" above); the fr defaults are the untouched layout. ALWAYS three columns
       down to the phone breakpoint — the fold that used to drop the panel under the pair below
       1400px made the tab unreviewable on a laptop. The rem floors are what let that fold go:
       without them a 15fr queue collapses to a sliver on a narrow window; with them the side
       columns stop shrinking at a usable width and the coding area gives up the difference. */
    /* The side tracks are read through --xl-lead / --xl-trail so a COLLAPSED pane can shrink its
       own track (the :has() rules under "Pane splitters" set the var to the strip width) without
       this declaration changing — which is what lets the ≤992px stack rule's plain
       grid-template-columns: 1fr still win outright. */
    grid-template-columns:
        var(--xl-lead, minmax(12rem, var(--dv-queue, 15fr)))
        minmax(0, 63fr)
        var(--xl-trail, minmax(14rem, var(--dv-panel, 22fr)));
    /* Panes touch across, hairline down: see "Card panes" in app.css. */
    column-gap: 0;
    row-gap: 2px;
    align-items: stretch;
}

/* Case Snapshot: team activity | review pipeline. Same contract as .xl-workspace. The lead pane
   takes 42% (splitter-driven) and the pipeline takes whatever is left — narrower than the 58%
   every other workspace gives its lead, because this one holds a LIST (a roster read down the
   page, which needs little width) while the pipeline beside it carries set bars, the review flow
   and two charts. Importance picks the side (CLAUDE.md → pane order); content picks the width.
   Review progress used to be a third track — a fixed vertical sliver on the right — and is now
   the bar in the section card's SubHead, which is why there are only two tracks. */
.snap-workspace {
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    /* First track via --snap-lead for the same reason .xl-workspace reads --xl-lead: a collapsed
       Last 7 Days pane shrinks its track through the var, never through this declaration. */
    grid-template-columns: var(--snap-lead, var(--dv-main, 42%)) minmax(0, auto);
    column-gap: 0;
    row-gap: 2px;
    align-items: stretch;
}

/* min-width: 0 lets a grid child shrink below its content width, which is what keeps the
   coding area's horizontal scrollbar inside its own card instead of stretching the page;
   min-height: 0 does the same vertically so the card's body scrolls instead of the page.
   overflow: hidden clips the body to the card's rounded corners. */
/* min-width: 0 lets a grid/flex child shrink below its content width, which is what keeps a wide
   table's scrollbar inside its own card instead of stretching the page; min-height: 0 does the
   same vertically so the body scrolls rather than the page. overflow: hidden clips the body to
   the card's rounded corners. */
.fill-card,
.xl-card {
    min-width: 0;
    min-height: 0;
    overflow: hidden;
    box-shadow: var(--cosmyre-workspace-shadow);
}

/* The scrolling body of a workspace card. */
/* The one part of a fill card that scrolls. Everything else in the card — heads, filters,
   footers, action rows — stays flex: 0 0 auto and therefore stays pinned. */
.fill-card-body,
.xl-card-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
}

/* The one part of a DETACHED page that scrolls. A detached page is more than one card, so there is
   no single card body to own the scroll — this is that body, sitting under the rail and holding the
   page's run of sibling cards. Same three declarations as .fill-card-body above, kept beside it so
   the two can never drift; the name says page because there is no card around it. Anything that
   must stay put goes OUTSIDE it — the rail and the Notices strip already are.

   scrollbar-gutter, which .fill-card-body does not need: these are the pages that carry ApexCharts,
   and a chart measures its container's width once at render. An inner scrollbar appearing later
   (content grew past the fold) is NOT a window resize, so the chart's own resize listener never
   fires and it would sit ~15px too wide. Reserving the gutter means the width never changes.

   Deliberately NOT given position: relative. An overlay throbber inside a scroller sizes to the
   content box and slides away with the rows; a throbber here belongs to one of the cards, and every
   card class is already relative. Equally deliberately no transform/filter/contain — those would
   make this a containing block for fixed descendants and break every modal rendered inside it. */
.page-fill-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* overflow-x is NOT left unspecified: a `visible` paired with a non-visible value computes to
       `auto`, which would make this a scroller on both axes — and Bootstrap's .row hangs 0.75rem
       past each edge on its negative gutter margin, so every detached page holding a .row would
       carry a spurious horizontal bar (and, with the gutter below, a dead strip along the foot).
       .section-card-body does not need this: it sets overflow: hidden first, so a page opting in
       with BodyClass="overflow-y-auto" overrides only the y and keeps x clipped. */
    overflow-x: clip;
    scrollbar-gutter: stable;
}

/* ---- Section card: the ONE card a tab-group page renders ---------------------------------
   Every page under a Services/TabRegistry.cs group is a single card of three stacked rows: the
   route tab bar, an action row (meta left, buttons right), then the page. The old shape - a
   header card repeating the same title and subtitle on every tab of a group, above a data card
   - is gone. The bar already names where you are, so the title band was a second answer to a
   question nobody asked twice, hand-written on every page and therefore agreeing on none.

   Rendered ONLY by Components/Shared/SectionCard.razor. A page never writes .section-card-*
   itself, the same way a page never writes .ctab-* itself.

   Row 1 has no rules here on purpose: it is .ctab-head.ctab-head-bare, defined with the rest of
   the rail below, so the bar in a card and the bar CaseProcessingDetail already rendered are
   the same thing and cannot drift. See CLAUDE.md -> "Standard tabs". */
.section-card {
    display: flex;              /* .fill-card deliberately does not set this; the rows need it */
    flex-direction: column;
    flex: 1 1 auto;             /* fills .page-fill; inert inside .page-surface's block flow */
    min-height: 0;
}

/* Only the body flexes. The rows above it stay pinned - the same contract .fill-card-body has
   with its heads, filters and footers. */
.section-card-tabs,
.section-card-actions,
.section-card-subhead,
.section-card-notices {
    flex: 0 0 auto;
}

/* A graphic that belongs with the meta row's counts, directly under them and inside the card
   (SectionCard's SubHead slot). Only the rows' shared side bleed lives here — what goes in it
   brings its own height and its own inset, because the one page using it wants the strip to
   stop short of the card's rounded corners rather than run edge to edge. */
.section-card-subhead {
    padding: 0 var(--cosmyre-tab-bleed);
}

/* The action row flows from the LEFT: buttons first (ending with the page's InfoHint), meta
   after — directly under the rail. The bar's 3px keyline lives on its own bottom edge, so it
   lands ON this border and reads as the same rail it did in the app header. Side padding is
   the bar's own --cosmyre-tab-bleed, so all three rows share a left edge. */
.section-card-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .75rem 1.5rem;
    padding: .625rem var(--cosmyre-tab-bleed);
    border-top: 1px solid var(--cosmyre-tab-rule);
}

/* Counts, pills, "OCR: Standard". Quiet by default so a page does not have to remember
   text-muted small on every span. min-width: 0 lets it shrink rather than shove the buttons off
   the card - the meta is the side that may wrap. */
.section-card-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .5rem .75rem;
    min-width: 0;
    font-size: .8125rem;
    color: var(--bs-secondary-color);
}

/* A row whose halves both rendered nothing this frame — buttons still waiting on data — would
   otherwise show as an empty banded strip under the rail, and then jump when the data lands.
   SectionCard writes both halves with no surrounding whitespace so :empty can actually see them;
   a page that supplies neither slot never renders the row in the first place. */
.section-card-actions:has(> .section-card-meta:empty):has(> .section-card-buttons:empty) {
    display: none;
}

/* Leads the row; the left edge is exactly where these hug now. */
.section-card-buttons {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .5rem;
}

/* A page whose action row needs TWO lines — a count/description line over a controls line —
   puts them in one .section-card-rows inside its Actions slot (CaseProfiles). Only then does
   the buttons container grow to the full row, so an inner `ms-auto` has the whole width to push
   against; every other page's content-sized buttons + trailing meta are untouched. Gated on
   :has() rather than a SectionCard parameter because it is markup opting in, not the shell. */
.section-card-buttons:has(> .section-card-rows) {
    flex: 1 1 auto;
    min-width: 0;
}

.section-card-rows {
    display: flex;
    flex-direction: column;
    gap: .5rem;
    width: 100%;
}

/* SectionCard always renders the meta div, even for a page that supplies no Meta. Next to a
   full-width .section-card-rows that empty div still consumes the row's 1.5rem column gap, so an
   inner ms-auto group stops short of the right edge instead of reaching it. */
.section-card-actions:has(> .section-card-buttons > .section-card-rows) > .section-card-meta:empty {
    display: none;
}

.section-card-rows > * {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .5rem;
}

/* Alerts and inline errors, between the action row and the body so the body's scrollbar can
   never carry an error out of sight. Zero VERTICAL padding and a flex column, so a Notices slot
   that renders nothing this pass contributes no height at all (whitespace between block children
   is not a flex item) and no page needs to wrap the whole slot in a conditional. Each notice
   carries its own mb-2. */
.section-card-notices {
    display: flex;
    flex-direction: column;
    padding: 0 var(--cosmyre-tab-bleed);
}

/* Row 3. overflow: hidden, NOT auto: what usually fills it is a DataTable, which scrolls itself
   on both axes under a sticky header - an auto-scrolling wrapper would put a second scrollbar on
   the card and unstick that header. Content that is not a table opts in with
   BodyClass="overflow-y-auto" — the y-only spelling on purpose, because Bootstrap's .overflow-auto
   is both axes and would add a horizontal bar the moment a control sets its own min-width. */
.section-card-body {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* Detached: the card is rows 1 and 2 only, and the page's own cards follow it under the same
   wrapper. For a page that is genuinely more than one card - the Excel workspaces' three
   columns, Workers' Fleet | Job queue pair, the term reports' picker + results - where folding
   everything into one card would mean cards nested inside a card.

   The 1rem here is the fallback, for a page whose next block is running text (the standing
   explainers on Extractions and Review Flow) — a paragraph jammed against a card edge is not the
   same thing as a panel continuing it. When a CARD or a workspace grid follows instead, the rules
   below close that gap to nothing: the rail is then the head of the thing beneath it, not a card
   floating above it, and a gap there read as two unrelated cards that happened to stack. */
.section-card-detached {
    flex: 0 0 auto;
    margin-bottom: 1rem;
}

/* Closing the gap has to be driven from the rail, so it keys on what FOLLOWS via :has(). The last
   two reach THROUGH .page-fill-body (the detached page's own scroller — app.css "fill-and-scroll")
   to whatever its first child actually is, so a detached page keeps exactly the seam it had before
   gaining a scroller: closed when that first child is a card, still open when it's a .row of tiles
   or a <Throbber> (neither matches :first-child here, same as today). */
.section-card-detached:has(+ .surface-card),
.section-card-detached:has(+ .card),
.section-card-detached:has(+ .search-workspace),
.section-card-detached:has(+ .term-workspace),
.section-card-detached:has(+ .xl-workspace),
.section-card-detached:has(+ .xl-workspace-qc),
.section-card-detached:has(+ .pr-workspace),
.section-card-detached:has(+ .xl-jobs-grid),
.section-card-detached:has(+ .cargo-panes),
.section-card-detached:has(+ .snap-workspace),
.section-card-detached:has(+ .list-page-row),
.section-card-detached:has(+ .list-page-col),
.section-card-detached:has(+ .page-fill-body > .surface-card:first-child),
.section-card-detached:has(+ .page-fill-body > .card:first-child) {
    margin-bottom: 0;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
    /* The card below brings its own top border, so keeping this one too would draw the seam at
       2px — twice every other hairline in the app. Dropping it also lets the active tab's 3px
       keyline sit right on the join instead of one pixel shy of it. */
    border-bottom-width: 0;
}

/* The other half of the seam: whatever sits directly under the rail loses its top rounding, so the
   two read as one surface rather than two rounded cards colliding. What counts as "directly under"
   depends on the container — a side-by-side grid puts every child against the seam, a stacked
   column only its first, a single card is itself.

   Bootstrap's .row is deliberately absent from both this and the gap rule above. It holds a wrapping
   field of small tiles which never touch the rail anyway: .row's own negative y-gutter margin pulls
   it up by 1rem and .row > * pushes each tile back down by the same, so the tiles keep a 1rem gap
   whatever the rail does. Closing the gap there would only square a rail that nothing sits against,
   and squaring whichever tiles happened to land in the first line would be arbitrary. */
.section-card-detached + .surface-card,
.section-card-detached + .card,
.section-card-detached + .search-workspace > *,
.section-card-detached + .term-workspace > *,
.section-card-detached + .xl-workspace > *,
.section-card-detached + .xl-workspace-qc > *,
.section-card-detached + .pr-workspace > *,
.section-card-detached + .xl-jobs-grid > *,
.section-card-detached + .cargo-panes > *,
.section-card-detached + .snap-workspace > *,
.section-card-detached + .list-page-row > *,
.section-card-detached + .list-page-col > *:first-child,
.section-card-detached + .page-fill-body > .surface-card:first-child,
.section-card-detached + .page-fill-body > .card:first-child {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}

/* Nudge the first column in so it isn't flush to the card's edge. Scoped through > .dnt-table,
   unlike .list-page-list's bare descendant selector, so a table nested deeper in the body (a
   pane, a detail block) keeps its own padding. */
.section-card-body > .dnt-table table th:first-child,
.section-card-body > .dnt-table table td:first-child {
    padding-left: 1rem;
}

/* A bar long enough to scroll inside a section card (Case Settings has ten tabs) fades on the
   RIGHT edge only. .ctab-scroll's default is a two-layer mask whose second layer also fades the
   first 26px; that layering exists to keep a card bar's TOP HAIRLINE crisp, and .ctab-head-bare
   has no hairline - so all a left fade does here is permanently dim the first tab's label.
   Single layer, right edge only, exactly as .ctab-top does and for the same reason.
   overflow-y: hidden because overflow-x: auto computes overflow-y to a non-visible value, which
   would otherwise clip the keyline's glow into a ragged edge. */
.section-card-tabs > .ctab-scroll {
    overflow-y: hidden;
    -webkit-mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 30px), transparent 100%);
    -webkit-mask-size: 100% 100%;
    -webkit-mask-position: 0 0;
    mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 30px), transparent 100%);
    mask-size: 100% 100%;
    mask-position: 0 0;
}

/* The term-report tabs' two-column workspace: a picker or control column on the left, the results
   on the right. Defined here rather than in each page's scoped stylesheet because both report tabs
   use it and scoped CSS cannot be shared — two copies would drift. */
.term-workspace {
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    /* The picker column is splitter-driven (see "Pane splitters"); default unchanged. */
    grid-template-columns: var(--dv-picker, minmax(16rem, 34fr)) minmax(0, 66fr);
    /* Panes touch across, hairline down: see "Card panes" in app.css. */
    column-gap: 0;
    row-gap: 2px;
    align-items: stretch;
}

/* The custom tab's picker holds short names only, so it gives more room to the editor. */
.term-workspace-custom {
    grid-template-columns: var(--dv-picker, minmax(14rem, 25fr)) minmax(0, 75fr);
}

/* Right-hand column of the custom tab: the editor keeps its natural height, the results card takes
   whatever is left. Stacked, so the hairline gap — see "Card panes" above; it used to be an mb-3
   on the editor card, which left this one pair on the old 1rem gutter after everything else moved. */
.term-detail {
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    row-gap: 2px;
}

/* The editor and the results split the column's height evenly and each scrolls its own middle
   row. flex-basis 0 rather than auto, so the split is driven by the share and not by how much
   content each happens to hold. */
.term-detail > .surface-card {
    flex: 1 1 0;
    min-height: 0;
}

/* A flex child that should take the room left over and TRUNCATE, rather than push its siblings
   onto a second line. Both halves are load-bearing and Bootstrap ships neither:
     flex-basis: 0 — a wrapping container decides to wrap from each child's BASE size, so a long
                     heading forces the wrap before any shrinking is even considered. .flex-fill
                     (flex: 1 1 auto) does NOT do this and wraps.
     min-width: 0  — a flex item defaults to min-width:auto and refuses to shrink below its
                     content, so .text-truncate on a child never engages without it. */
.flex-truncate {
    flex: 1 1 0;
    min-width: 0;
}

/* ---- Icon colour ---------------------------------------------------------------------------

   Every <Icon> renders deep matte purple. This has to live here, globally: the svg is inside
   Icon's own CSS isolation scope, so a caller's scoped stylesheet can never colour it — a rule
   written there silently does nothing, which has caught us more than once.

   Setting colour on the SVG ITSELF is what makes this stick, and it is also why the carve-outs
   below are needed: an icon that used to take its colour from a parent (a red danger span, a
   solid button) no longer inherits anything, so each of those puts the inheritance back. The list
   is deliberately short, and everything on it colours the icon to MEAN something:

     left nav      — the sidebar and case navigator own their own palette
     tab rail      — .ctab-icon glides between the --cosmyre-tab-* active/inactive states
     buttons       — a solid .btn paints its own foreground; purple on btn-primary is unreadable
     status text   — danger / warning / success / info, and the Excel conflict + mapped marks

   The pencil is the one glyph that opts out of the purple rather than out of the rule: it reads
   as "edit" everywhere it appears, so it takes --cosmyre-icon-edit instead. It sits at the same
   specificity as .bi and simply comes later, so the carve-outs above still beat it — a pencil on
   a btn-primary stays white. */
.bi {
    color: var(--cosmyre-icon);
}

.sidebar-nav .bi,
.sidebar-brand .bi,
.sidebar-menu-toggle .bi,
.nav-dock .bi,
.ctab-icon .bi,
.btn .bi,
.text-danger .bi,
.text-warning .bi,
.text-success .bi,
.text-info .bi,
.xl-conflict-text .bi,
.xl-mapped-text .bi {
    color: inherit;
}

.bi-pencil {
    color: var(--cosmyre-icon-edit);
}

/* These two buttons are bare glyphs, so their hover feedback WAS the icon changing colour. The
   icon now carries its own colour, which means the button's :hover no longer reaches it — these
   put that feedback back. Global for the same isolation-scope reason as everything above. */
.icon-action-btn:hover .bi,
.icon-action-btn:focus-visible .bi {
    color: var(--bs-body-color);
}

/* The pencil keeps its own family on hover rather than collapsing into the body colour: on the
   mass-code button it is the mark that distinguishes the action from a plain globe, and on the
   bare edit button it is the only thing that can carry the hover at all. */
.icon-action-btn:hover .bi-pencil,
.icon-action-btn:focus-visible .bi-pencil {
    color: var(--cosmyre-icon-edit-hover);
}

/* Folder glyphs in the saved-search tree. Global rather than scoped because <Icon> renders inside
   its own isolation scope, so neither the page's nor the node's scoped stylesheet can colour it.
   Icon fills with currentColor, which is why setting colour alone is enough. Deep and matte on
   purpose — it belongs to the same family as the connector lines rather than competing with them. */
.text-folder {
    color: var(--cosmyre-icon);
}

/* A results panel that has been handed its card's height: status strip pinned, grid scrolling,
   pager pinned — the same three rows every other fill card uses. */
.term-results-fill {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

/* Pinned action row at the top of a fill card, matching the search tree's. */
.card-actions {
    padding: 0 .15rem .25rem;
}

@media (max-width: 991.98px) {
    /* Stacked, splitting one fixed page height between two cards is worse than scrolling — the
       same reasoning as the form exception above. */
    .term-workspace,
    .term-workspace-custom {
        grid-template-columns: 1fr;
    }
}

/* The column reference at the right of a mapped-headers row. Its own fixed slot rather than the
   tail of a variable-length label, so the letters line up down the card instead of landing
   wherever the raw header text happens to end. min-width, not width: a multi-column reference
   ("#1 B, #2 D") still gets to grow. */
.xl-col-ref {
    flex: none;
    min-width: 2.75rem;
    text-align: center;
    font-size: .72rem;
    color: var(--bs-secondary-color);
}

/* QC's workspace: queue | sheet preview | block editor, ALWAYS three columns down to the phone
   breakpoint. Standalone rather than a modifier on .xl-workspace so the two can be tuned apart.
   The side columns are proportional with a floor rather than fixed rem: fixed widths were why
   this used to fold the editor under the pair below 1600px, which made the tab unreviewable on a
   laptop. Now they shrink with the window until they hit their floors, and the preview — the one
   column whose usefulness scales with width — takes the rest. Splitter sizes (px) still win over
   the defaults; the floors only stop a drag or a narrow window from crushing a form column. */
.xl-workspace-qc {
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    /* Side tracks via --xl-lead / --xl-trail for the same reason as .xl-workspace: a collapsed
       pane shrinks its track through the var, never through this declaration. */
    grid-template-columns:
        var(--xl-lead, minmax(13rem, var(--dv-queue, 20%)))
        minmax(0, 1fr)
        var(--xl-trail, minmax(15rem, var(--dv-editor, 22%)));
    /* Panes touch across, hairline down: see "Card panes" in app.css. */
    column-gap: 0;
    row-gap: 2px;
    align-items: stretch;
}

@media (max-width: 991.98px) {
    .xl-workspace-qc {
        grid-template-columns: 1fr;
    }

    .xl-workspace-qc > .xl-card {
        max-height: 32rem;
    }
}

/* The review workspace's document frame. Its own classes rather than DocumentViewer's, whose
   .dv-viewport / .native-frame are scoped to that page's stylesheet and cannot be reached here. */
.pr-doc-viewport {
    flex: 1 1 auto;
    min-height: 0;
    position: relative;
}

.pr-doc-frame {
    width: 100%;
    height: 100%;
    border: 1px solid var(--bs-border-color);
    border-radius: 4px;
    background: #fff;
}

/* Profile review: queue | members | document. The same three-column contract as QC above —
   --pr-lead / --pr-trail for a folded pane, --dv-queue / --dv-docs for a dragged one, minmax()
   floors so neither drag crushes the members in the middle. The document track has the widest
   default of the three because a page of a document needs the room whenever it is open at all;
   it starts folded (data-pane-default on its button), so usually that track is 2.75rem anyway. */
.pr-workspace {
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    grid-template-columns:
        var(--pr-lead, minmax(13rem, var(--dv-queue, 20%)))
        minmax(0, 1fr)
        var(--pr-trail, minmax(15rem, var(--dv-docs, 30%)));
    column-gap: 0;
    row-gap: 2px;
    align-items: stretch;
}

.pr-workspace:has(> .dv-queue.pane-collapsed) {
    --pr-lead: 2.75rem;
}

.pr-workspace:has(> .dv-docs.pane-collapsed) {
    --pr-trail: 2.75rem;
}

@media (max-width: 991.98px) {
    .pr-workspace {
        grid-template-columns: 1fr;
    }

    .pr-workspace > .xl-card {
        max-height: 32rem;
    }
}

/* The throbber a server-paged DataTable renders while fetching is position:absolute, so the card
   holding it has to be its containing block — otherwise it escapes to the viewport. Declared on
   the card and not on .dnt-table itself: that element scrolls, and an overlay inside a scroller
   sizes to the CONTENT box, so it would stretch to the full table width and slide away with the
   rows instead of covering what you are looking at. */
.list-page-list,
.fill-card,
.xl-card,
.section-card-body {
    position: relative;
}

/* A DataTable filling a workspace card — same contract as .list-page-list, so the table scrolls
   inside the card and the pager stays pinned at its foot. .section-card-body is listed here in
   its own right, and not left to inherit through .fill-card, because a Fill="false" section card
   (a form page) has no .fill-card and still hosts tables. */
.fill-card .dnt-table.dnt-fill,
.xl-card .dnt-table.dnt-fill,
.section-card-body .dnt-table.dnt-fill {
    flex: 1 1 auto;
    min-height: 0;
    height: auto;
}

.fill-card .dnt-table.dnt-fill + div,
.xl-card .dnt-table.dnt-fill + div,
.section-card-body .dnt-table.dnt-fill + div {
    flex: 0 0 auto;
    padding: .5rem .75rem;
    border-top: 1px solid var(--bs-border-color);
    background: var(--cosmyre-card-bg);
}

@media (max-width: 991.98px) {
    /* Stacked, a viewport-height page would squash every card, so let the page scroll again. */
    .xl-page,
    .snap-page {
        height: auto;
        min-height: calc(100vh - 3.75rem);
        padding-bottom: 2rem;
    }

    .xl-workspace,
    .snap-workspace {
        grid-template-columns: 1fr;
    }

    .xl-card,
    .snap-workspace > .fill-card {
        max-height: 32rem;
    }

    /* Stacked to one column, a vertical strip makes no sense: a collapsed pane is just its header
       bar, like every stacked card. The grid is 1fr above, so the --xl-* track vars are moot here. */
    .pane-collapsed > .pane-head {
        flex: 0 0 auto;
        writing-mode: horizontal-tb;
        transform: none;
        justify-content: flex-start;
    }

    .pane-collapsed > .pane-head .pane-collapse-btn {
        margin-left: auto;
    }
}

/* One entry in the group queue. A button, not an anchor: an <a href="#"> resolves against
   <base href="/"> and Blazor's enhanced navigation then routes to the home page. */
.xl-group-entry {
    display: block;
    width: 100%;
    text-align: left;
    border: 0;
    background: transparent;
    font: inherit;
    padding: .6rem 1rem;
    cursor: pointer;
    color: inherit;
    text-decoration: none;
}

.xl-group-entry:hover {
    background: var(--cosmyre-pick-hover);
    color: inherit;
}

.xl-group-entry.xl-selected {
    background: var(--cosmyre-pick-bg);
}

/* Soft page-bg-coloured rule between entries, inset from both edges — deliberately not a
   border, so it reads as a seam in the card rather than a table gridline. */
.xl-group-divider {
    height: 1px;
    background: var(--cosmyre-page-bg);
    margin: 0 5px;
}

.xl-mono {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* Duplicate-field mode, right-aligned on the header-row strip. .form-select is width:100% by
   default, which would stretch it across the whole strip inside a flex row — size it to its
   longest option instead and let it shrink on a narrow card rather than push the row to wrap. */
.xl-dup-select {
    width: auto;
    max-width: 21rem;
    min-width: 0;
}

/* Provenance of a column's mapped field, encoded on the select itself: solid fill and white
   text, so a mapped column is unmistakable at a glance. background-color, never the background
   shorthand — the shorthand wipes .form-select's caret image along with it. */
.xl-map-dict { background-color: var(--cosmyre-map-dict); }
.xl-map-user { background-color: var(--cosmyre-map-user); }

.xl-map-dict,
.xl-map-user {
    color: #fff;
    border-color: transparent;
    /* Bootstrap's caret is a dark chevron; repaint it white or it vanishes on these fills. */
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
}

/* The dropdown list itself is drawn by the OS, which does not inherit the closed control's
   white text — give the options the page's own colours back or the list is white on white. */
.xl-map-dict option,
.xl-map-user option {
    color: var(--bs-body-color);
    background-color: var(--bs-body-bg);
}

/* The coding header's two rows sit flush with each other and with the first data row: no
   bottom border under the selects, and the letter strip carries the full grid border so it
   reads as the top edge of the data rather than a caption floating above it.
   Qualified with th on purpose — Bootstrap's own `.table > :not(caption) > * > *` sets the
   padding, background and border-bottom at (0,1,1), which a bare class would lose to. */
/* Both header rows are FROZEN: they stay put while the grid scrolls under them, so a reviewer
   200 rows into a group can still see which field each column maps to.
   Sticky cells, not a sticky <tr> — a sticky table ROW is not honoured across engines.
   The two offsets are coupled: the select row pins at 0 and the letter strip pins directly
   below it, which is the only reason the select row carries an explicit height. If either
   changes, --xl-maprow-h has to change with it or the strip will float or overlap. */
.xl-grid-scroll thead th {
    position: sticky;
    z-index: 2;
    /* Opaque base. The column tints are translucent by design, and a translucent frozen cell
       lets the scrolling rows read straight through it. */
    background-color: var(--cosmyre-card-bg);
}

th.xl-maprow-cell {
    top: 0;
    height: var(--xl-maprow-h);
    padding: .3rem .3rem .35rem;
    border-bottom: 0;
    vertical-align: top;
}

th.xl-letter-cell {
    top: var(--xl-maprow-h);
    padding: .1rem .35rem;
    background-color: var(--bs-tertiary-bg);
    color: var(--bs-secondary-color);
    font-size: .72rem;
    font-weight: 600;
    text-align: center;
    /* The border is an inset shadow rather than a real border because the table collapses its
       borders (Bootstrap's reboot), and a collapsed border belongs to the TABLE — it scrolls
       away and leaves the frozen strip unboxed. A shadow is painted with the cell. */
    border: 0;
    box-shadow: inset 1px 0 0 var(--bs-border-color), inset -1px 0 0 var(--bs-border-color),
                inset 0 1px 0 var(--bs-border-color), inset 0 -1px 0 var(--bs-border-color);
}

/* Conflict border wins over the transparent one the provenance fills set. */
.xl-map-conflict { border-color: var(--cosmyre-conflict-border); }

/* The tint runs down the whole column — letter strip, header cell and every data cell — so the
   eye can follow a mapping down the grid.
   The long selector is load-bearing. Bootstrap paints every cell with
   `.table > :not(caption) > * > * { background-color: var(--bs-table-bg) }`, and --bs-table-bg
   is var(--bs-body-bg) — OPAQUE, not transparent. At (0,1,1) that beat the bare `.xl-col-mapped`
   class this used to be, so the tint was being painted over and never actually appeared. Match
   Bootstrap's structure to outrank it; do not simplify these back to a single class. */
.table > :not(caption) > * > .xl-col-mapped { background-color: var(--cosmyre-map-dict-tint); }
.table > :not(caption) > * > .xl-col-conflict { background-color: var(--cosmyre-conflict-tint); }

/* ---- QC grid cell states ----
   Same long selector, same reason: Bootstrap paints every cell with an opaque --bs-table-bg at
   (0,1,1), so a bare class loses and the colour never appears. Do not shorten these.
   Ink is set alongside every background because these are fixed colours rather than translucent
   washes — inheriting the theme's text colour would put light ink on a light tint in dark mode. */
.table > :not(caption) > * > .xl-row-blank {
    background-color: var(--cosmyre-xl-blank);
    color: var(--cosmyre-xl-blank-ink);
}

.table > :not(caption) > * > .xl-row-rejected {
    background-color: var(--cosmyre-xl-rejected);
    color: var(--cosmyre-xl-rejected-ink);
}

/* A column the mapping actually pulls, inside a block. */
.table > :not(caption) > * > .xl-cell-pulled {
    background-color: var(--cosmyre-xl-pulled);
    color: var(--cosmyre-xl-pulled-ink);
}

.table > :not(caption) > * > .xl-block-1 { background-color: var(--cosmyre-xl-block-1); color: var(--cosmyre-xl-block-ink); }
.table > :not(caption) > * > .xl-block-2 { background-color: var(--cosmyre-xl-block-2); color: var(--cosmyre-xl-block-ink); }
.table > :not(caption) > * > .xl-block-3 { background-color: var(--cosmyre-xl-block-3); color: var(--cosmyre-xl-block-ink); }
.table > :not(caption) > * > .xl-block-4 { background-color: var(--cosmyre-xl-block-4); color: var(--cosmyre-xl-block-ink); }
.table > :not(caption) > * > .xl-block-5 { background-color: var(--cosmyre-xl-block-5); color: var(--cosmyre-xl-block-ink); }
.table > :not(caption) > * > .xl-block-6 { background-color: var(--cosmyre-xl-block-6); color: var(--cosmyre-xl-block-ink); }

/* One block: its controls and, when open, its column mapping. Both live in here so the highlight
   covers everything that belongs to the block and the container grows with the mapping rather
   than the mapping floating below it as a separate card. */
.xl-block-row {
    padding: .5rem;
    margin-bottom: .5rem;
    border: 1px solid transparent;
    border-radius: .5rem;
}

/* A block's four rows: identity + remove, extent, header, then mapping. Stacked rather than one
   wrapping strip because in a 24rem card a single flex row wraps at whatever point the browser
   picks, which put "Header row" beside "Stop row" on one width and alone on the next. */
.xl-block-head {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin-bottom: .4rem;
}

/* The remove button sits hard right of the block's name. */
.xl-block-head > .btn {
    margin-left: auto;
    line-height: 1;
    padding: .05rem .4rem;
}

/* Start and Stop share a row and split it evenly, so the two ends of a range read as a pair
   rather than as two unrelated fields that happen to be adjacent. */
.xl-block-fields {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: .5rem;
    margin-bottom: .4rem;
}

/* Header row is one decision, so it takes the left half and leaves the right empty rather than
   stretching a row-number box to the full card width. */
.xl-block-fields-single {
    grid-template-columns: 1fr 1fr;
}

.xl-block-fields-single > :first-child {
    grid-column: 1;
}

.xl-block-fields input {
    width: 100%;
}

.xl-block-actions {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-wrap: wrap;
}

/* The Job Queue's two job lists, side by side.

   align-items: stretch (the grid default, stated rather than assumed) so both cards share the
   row's height. They are a matched pair, and one card ending short of the other reads as a
   rendering fault rather than as "this list happens to be empty" — which is the whole thing an
   empty state is trying to say.

   min-width: 0 on the children because a DataTable scrolls horizontally inside its card —
   without it a wide table would widen its grid track and push the other card off instead of
   scrolling within its own. */
.xl-jobs-grid {
    flex: 1 1 auto;
    min-height: 0;          /* lets the row shrink so the cards scroll, not the page */
    display: grid;
    /* The left list is splitter-driven (see "Pane splitters"); default is the even split. */
    grid-template-columns: var(--dv-lead, 1fr) minmax(0, 1fr);
    /* Panes touch across, hairline down: see "Card panes" in app.css. */
    column-gap: 0;
    row-gap: 2px;
    align-items: stretch;
}

.xl-jobs-grid > * {
    min-width: 0;
}

@media (max-width: 1199.98px) {
    /* Two job tables stop being readable side by side well before the viewport runs out.
       Stacked, they would each get half of an already-fixed page height, so the page goes
       back to scrolling and the cards size to their content — the form exception in reverse. */
    .xl-jobs-grid {
        grid-template-columns: 1fr;
        flex: 0 0 auto;
    }

    .page-fill:has(.xl-jobs-grid) {
        height: auto;
        min-height: calc(100vh - 3.75rem);
        padding-bottom: 2rem;
    }

    .xl-jobs-grid > .fill-card {
        max-height: 32rem;
    }
}

/* ---- Icon affordance on the Header Mappings list ----------------------------------------
   One square button above the table, opening the add-headers modal. Deliberately an icon rather
   than a labelled button — the dictionary is grown mostly by coding groups, so this is an
   occasional tool and should not compete with the list it sits on. */
.xl-icon-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 2rem;
    height: 2rem;
    padding: 0;
    border: 1px solid var(--bs-border-color);
    border-radius: .375rem;
    background: var(--cosmyre-card-bg);
    cursor: pointer;
    line-height: 1;
}

.xl-icon-btn:hover {
    border-color: currentColor;
}

.xl-icon-bulk { color: var(--bs-body-color); }

/* The bulk button is a page glyph with a small plus in its bottom-right corner — "several of
   these at once", without needing a second word of chrome. */
.xl-icon-badge {
    position: absolute;
    right: -.15rem;
    bottom: -.2rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: .85rem;
    height: .85rem;
    border-radius: 50%;
    background: #198754;
    color: #fff;
    font-size: .7rem;
    font-weight: 700;
    line-height: 1;
    /* Rides on the card, so the ring reads as a cut-out rather than a floating dot. */
    box-shadow: 0 0 0 1.5px var(--cosmyre-card-bg);
}

/* The block a grid click currently edits. Quiet on purpose — the swatch already says which block
   is which, so this only needs to say "clicks land here". */
.xl-block-active {
    background: var(--cosmyre-pick-hover);
    border-color: var(--cosmyre-md-control-border, var(--bs-border-color));
    box-shadow: inset 2px 0 0 var(--cosmyre-tab-accent);
}

/* The column editor, nested inside its block. Separated by a rule rather than its own border —
   it is part of the block, not a card sitting on top of one. */
.xl-block-cols {
    margin-top: .5rem;
    padding-top: .5rem;
    border-top: 1px solid var(--bs-border-color);
}

/* The raw header text beside a column letter in the per-block mapping editor. Small and quiet:
   the letter is the identifier, this is the evidence that the letter means what you think. */
.xl-colhead {
    font-size: .7rem;
    font-weight: 400;
    color: var(--bs-secondary-color);
}

/* Job-statistics modal. The content is inset from the dialog edge so the tiles are not flush
   against it, and each category is its own band separated by a rule. */
/* Fills the dialog body and scrolls itself. ModalShell's .md-body is overflow:hidden and expects
   the caller to supply its own scrolling region — the other modals do the same. Absolute inset
   rather than height:100% because .md-body's height comes from a flex parent, and a percentage
   against that is not reliably resolvable. */
.xl-stats {
    position: absolute;
    inset: 0;
    overflow-y: auto;
    padding: 1rem 1.25rem 1rem .75rem;
}

.xl-stat-group {
    padding-bottom: .85rem;
    border-bottom: 1px solid var(--bs-border-color);
}

/* No rule on the last band — the dialog's own footer already draws one directly beneath it, and
   two hairlines a few pixels apart read as a mistake. */
.xl-stat-group:last-child {
    padding-bottom: 0;
    border-bottom: 0;
}

.xl-stat-head {
    margin-bottom: .5rem;
    font-size: .75rem;
    font-weight: 600;
    letter-spacing: .03em;
    color: var(--bs-secondary-color);
}

/* A single figure: the number leads, its label sits under it. Fixed width so a row of them forms
   a grid rather than a ragged line of differently-sized tiles. */
.xl-stat {
    min-width: 8.5rem;
    padding: .6rem .75rem;
    border: 1px solid var(--cosmyre-xl-stat-border);
    border-radius: .5rem;
    background: var(--cosmyre-card-bg);
    box-shadow: 0 1px 3px rgba(20, 20, 50, .2);
}

.xl-stat-n {
    font-size: 1.35rem;
    font-weight: 600;
    line-height: 1.1;
}

.xl-stat-l {
    font-size: .75rem;
    color: var(--bs-secondary-color);
}

/* Block swatch for the range editor's list, so the number and the colour agree. */
.xl-block-swatch {
    display: inline-block;
    width: .85rem;
    height: .85rem;
    border-radius: .2rem;
    border: 1px solid var(--bs-border-color);
    vertical-align: -.12rem;
    margin-right: .35rem;
}

/* On the two FROZEN header rows the same tint has to composite over an opaque base instead of
   replacing it — a translucent frozen cell shows the scrolling rows through itself. Painting
   the tint as a background-image over an opaque background-color keeps one source of truth for
   the colour while still hiding what passes underneath. */
.table > :not(caption) > * > th.xl-col-mapped {
    background-color: var(--cosmyre-card-bg);
    background-image: linear-gradient(var(--cosmyre-map-dict-tint), var(--cosmyre-map-dict-tint));
}

.table > :not(caption) > * > th.xl-col-conflict {
    background-color: var(--cosmyre-card-bg);
    background-image: linear-gradient(var(--cosmyre-conflict-tint), var(--cosmyre-conflict-tint));
}

.xl-conflict-text {
    color: var(--cosmyre-conflict-text);
    font-weight: 600;
}

.xl-mapped-text {
    color: var(--cosmyre-mapped-text);
}

.xl-swatch {
    display: inline-block;
    width: .8rem;
    height: .8rem;
    border-radius: .2rem;
    border: 1px solid var(--bs-border-color);
    vertical-align: -.1rem;
    margin-right: .3rem;
}

a, .btn-link {
    color: var(--cosmyre-accent-text);
}

a:hover, .btn-link:hover {
    color: var(--cosmyre-accent-hover);
}

/* Inline icons (Icon.razor) baseline alignment. */
.bi {
    vertical-align: -.125em;
    flex-shrink: 0;
}

/* When the hamburger collapses the sidebar to the icon rail, shrink the docked Navigator to
   match (width: 3.6rem) and minimize it to just its orb (hide the label block + caret) so it
   tracks the left pane. Mirrors the sidebar collapse conditions in MainLayout.razor.css; only
   applies on the desktop rail (>=641px). Global (not scoped) so it can span the MainLayout
   checkbox and the sibling CaseNavigator's classes. */
@media (min-width: 641px) {
    .page:not(.rail-default) > .sidebar-toggle-state:checked ~ .nav-dock,
    .page.rail-default > .sidebar-toggle-state:not(:checked) ~ .nav-dock {
        /* .75rem inline padding would leave too little room for the 3.4rem orb button in the
           3.6rem rail — drop it and centre the button in the strip instead. */
        width: 3.6rem;
        padding-inline: 0;
        justify-content: center;
    }

    .page:not(.rail-default) > .sidebar-toggle-state:checked ~ .nav-dock .nav-orb-label,
    .page.rail-default > .sidebar-toggle-state:not(:checked) ~ .nav-dock .nav-orb-label,
    .page:not(.rail-default) > .sidebar-toggle-state:checked ~ .nav-dock .nav-orb-caret,
    .page.rail-default > .sidebar-toggle-state:not(:checked) ~ .nav-dock .nav-orb-caret {
        display: none;
    }

    .page:not(.rail-default) > .sidebar-toggle-state:checked ~ .nav-dock .nav-orb-btn,
    .page.rail-default > .sidebar-toggle-state:not(:checked) ~ .nav-dock .nav-orb-btn {
        width: auto;
        padding: .4rem;
        gap: 0;
    }
}

/* Below 641px .sidebar is display:none until the hamburger opens it as a full-screen overlay
   (see MainLayout.razor.css); the docked Navigator mirrors that so it is reachable only through
   the open menu, same as every other sidebar item, rather than floating over page content on
   its own. Global for the same reason as the block above.
   :not(:checked), not a bare .nav-dock rule: CaseNavigator.razor.css's scoped .nav-dock carries
   Blazor's scope attribute, which already beats a plain class selector on specificity alone —
   the extra pseudo-class is what lets this override win. No "checked -> flex" rule is needed:
   the scoped stylesheet's own display:flex is the default and applies whenever this doesn't. */
@media (max-width: 640px) {
    .sidebar-toggle-state:not(:checked) ~ .nav-dock {
        display: none;
    }
}

/* ---- Header user/settings dropdown ---- */
.header-user-btn {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    padding: .25rem .5rem;
    border: 1px solid var(--bs-border-color, #e2e2e2);
    border-radius: 999px;
    background: var(--bs-body-bg, #fff);
}

.header-user-btn .user-avatar {
    display: inline-flex;
    font-size: 1.4rem;
    color: var(--bs-secondary-color, #6c757d);
}

.header-user-btn .user-name {
    font-size: .9rem;
    max-width: 12rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Highlight the active theme choice (set by theme.js). */
.theme-btn.active {
    background-color: var(--bs-secondary-bg, #e9ecef);
    border-color: var(--cosmyre-accent);
    color: var(--bs-emphasis-color, inherit);
}

/* Accent focus ring, themed — the old rule baked a white inner ring that glowed in dark. */
.btn:focus-visible, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
    box-shadow: 0 0 0 0.25rem rgba(var(--cosmyre-accent-rgb), .3);
}

.form-check-input:checked {
    background-color: var(--cosmyre-accent);
    border-color: var(--cosmyre-accent);
}

.content {
    padding-top: 1.1rem;
}

h1:focus {
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: #e50000;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}

/* ---- Case Snapshot dashboard (Ynex-style report cards) ----
   The page wrapper is SectionCard's .page-surface now; .snapshot-page is retired. */
.snap-card {
    border: 1px solid var(--bs-border-color, #e8e9ef);
    border-radius: .65rem;
    box-shadow: 0 .25rem .75rem rgba(20, 20, 50, .05);
}

.snap-card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: transparent;
    border-bottom: 1px solid var(--bs-border-color, #eef0f5);
    cursor: move;
}

.snap-grip {
    color: var(--bs-secondary-color, #adb5bd);
    font-size: 1.1rem;
}

.snap-stat .snap-stat-label {
    font-size: .8rem;
    color: var(--bs-secondary-color, #6b7280);
    margin-bottom: .15rem;
}

.snap-stat .snap-stat-value {
    font-size: 1.85rem;
    font-weight: 700;
    line-height: 1.1;
}

.snap-stat .snap-stat-sub {
    font-size: .75rem;
    margin-top: .15rem;
}

.snap-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 3rem;
    height: 3rem;
    border-radius: .65rem;
    font-size: 1.4rem;
}

.snap-icon-primary { background: rgba(132, 90, 223, .15); color: #845adf; }
.snap-icon-success { background: rgba(38, 191, 148, .15); color: #1aa179; }
.snap-icon-warning { background: rgba(245, 184, 73, .18); color: #d8961f; }
.snap-icon-info    { background: rgba(35, 183, 229, .15); color: #1799c0; }

.snap-avatar {
    width: 2.1rem;
    height: 2.1rem;
    border-radius: 50%;
    object-fit: cover;
    background: var(--bs-secondary-bg, #e9ecef);
}

/* Review Sets — Pace & ETA card on the Snapshot: one block per set. */
.snap-set + .snap-set {
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--bs-border-color, #eef0f5);
}

.snap-set-bar {
    height: .6rem;
    margin: .35rem 0 .3rem;
}

.snap-set-stats {
    row-gap: .1rem;
}

/* Review Flow card on the Snapshot: one row per assignment. */
.snap-flow + .snap-flow {
    margin-top: .65rem;
    padding-top: .65rem;
    border-top: 1px solid var(--bs-border-color, #eef0f5);
}

/* Large avatar on the Mission Settings reviewer detail page header. */
.reviewer-avatar {
    width: 3.5rem;
    height: 3.5rem;
    border-radius: 50%;
    object-fit: cover;
    background: var(--bs-secondary-bg, #e9ecef);
}

/* The same avatar in a section card's meta row, where 3.5rem would set the row's height. */
.reviewer-avatar-sm {
    width: 1.5rem;
    height: 1.5rem;
}

.snap-team th {
    font-size: .78rem;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--bs-secondary-color, #6b7280);
}

/* Drag feedback (SortableJS). */
.sortable-ghost { opacity: .4; }

/* ---- Display-columns dual list (saved-search builder, native HTML5 drag/drop) ---- */
.dnd-list {
    height: 240px;
    overflow-y: auto;
    background: var(--bs-tertiary-bg, #f8f9fa);
}

.dnd-item {
    cursor: grab;
    user-select: none;
}

.dnd-item:active { cursor: grabbing; }

.dnd-item.dnd-dragging { opacity: .4; }

/* Builder variant: the list grows with its content instead of the dual-list's fixed height. */
.dnd-list.dnd-fill {
    height: auto;
    min-height: 60px;
}

/* ---- Coding sidebar sections (document viewer + template builder preview). Global —
   the markup spans components (CodingFieldInput, CodingTemplatePreview), so scoped CSS
   can't reach it. Accent tints are inline styles from CodingSectionUi (template-chosen). ---- */
.dv-section-head {
    display: flex;
    align-items: center;
    gap: .4rem;
    width: 100%;
    border: none;
    border-bottom: 1px solid var(--bs-border-color);
    background-color: var(--bs-body-bg);
    color: var(--bs-body-color);
    padding: .5rem .75rem;
    font-weight: 600;
    font-size: calc(.9rem * var(--dv-text-scale, 1));
    text-align: left;
    position: sticky;
    top: 0;
    z-index: 2;
}

.dv-section-body {
    padding: .5rem .75rem;
}

/* A section header hosts its toggle plus optional action buttons (e.g. Save Coding). */
.dv-section-toggle {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    gap: .4rem;
    background: none;
    border: none;
    color: inherit;
    font: inherit;
    padding: 0;
    text-align: left;
    cursor: pointer;
}

.dv-section-action {
    flex: 0 0 auto;
}

/* Coding fields: name on the left, its input on the right (mirrors the profile form). */
.dv-code-field {
    display: flex;
    align-items: center;
    gap: .4rem;
    margin-bottom: .35rem;
}

.dv-code-field > label {
    flex: 0 0 44%;
    margin: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.dv-code-field > :not(label) {
    flex: 1 1 auto;
    min-width: 0;
}

/* The builder preview scrolls like the viewer's side card. */
.dv-preview-scroll {
    max-height: 640px;
    overflow-y: auto;
}

/* ---- Template builder: section accent color picker (current-color dot + popover) ---- */
.ct-swatch {
    width: 1.15rem;
    height: 1.15rem;
    border-radius: 50%;
    border: 2px solid transparent;
    padding: 0;
    cursor: pointer;
    flex: 0 0 auto;
}

.ct-swatch.selected {
    border-color: var(--bs-body-color);
}

.ct-color-pick {
    position: relative;
    flex: 0 0 auto;
    display: flex;
    align-items: center;
}

.ct-swatch-current {
    border-color: var(--bs-border-color, #dee2e6);
}

/* Transparent full-screen layer under the popover: clicking anywhere else closes it. */
.ct-swatch-backdrop {
    position: fixed;
    inset: 0;
    z-index: 10;
}

.ct-swatch-pop {
    position: absolute;
    top: calc(100% + .3rem);
    right: 0;
    z-index: 11;
    display: flex;
    align-items: center;
    gap: .35rem;
    padding: .45rem .55rem;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: .5rem;
    box-shadow: 0 .25rem .75rem rgba(20, 20, 50, .18);
}

.sortable-chosen .snap-card { box-shadow: 0 .5rem 1.5rem rgba(20, 20, 50, .15); }

/* ============================================================
   MODAL PRIMITIVES
   The parts a rule builder is made of: an operator bracket, a rule row, a field
   chip, a drop slot, a tag dot, a segmented control.

   These are GLOBAL rather than scoped because they span components — the search
   builder, the acceptance builder and the dedupe builder all draw the same row and
   the same bracket, and a custom property or class in one component's .razor.css
   is unreachable from another's. Dialog chrome (frame, header, footer) is NOT here:
   it belongs to ModalShell and is scoped to it.

   Colours are the --cosmyre-md-* tokens at the top of this file. Never hardcode.
   ============================================================ */

/* ---- operator bracket ----
   A 3px stroke open on the right, curled top and bottom so it reads as ENCLOSING the
   rules it joins, with the operator word on a pill centred on the stroke. The stroke
   colour is mixed against --cosmyre-md-surface, NOT white: mixed against white it
   becomes a glowing halo on a dark canvas.

   The bracket and pill are absolutely positioned in the gutter that .md-stack's
   padding-left reserves. Whatever adds rules must be a SIBLING of the stack, never a
   child, or the bracket overshoots and the pill sits off-centre. */
.md-stack {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding-left: 52px;
}

.md-bracket {
    position: absolute;
    left: 24px;
    top: 4px;
    bottom: 4px;
    width: 16px;
    border: 3px solid;
    border-right: 0;
    border-radius: 17px 0 0 17px;
    pointer-events: none;
}

.md-pill {
    position: absolute;
    /* Derived from the stroke centre (24px + 3px/2 - 22px) so the pill stays centred
       if the stroke width ever changes. */
    left: 3.5px;
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 21px;
    border-radius: 999px;
    background: var(--cosmyre-md-surface);
    border: 1px solid;
    font-size: 10.5px;
    font-weight: 700;
    line-height: 1;
    letter-spacing: .09em;
    pointer-events: none;
}

.md-and { color: var(--cosmyre-md-and); }
.md-or { color: var(--cosmyre-md-or); }
.md-not { color: var(--cosmyre-md-not); }

.md-bracket.md-and { border-color: color-mix(in srgb, var(--cosmyre-md-and) 40%, var(--cosmyre-md-surface)); }
.md-bracket.md-or { border-color: color-mix(in srgb, var(--cosmyre-md-or) 40%, var(--cosmyre-md-surface)); }
.md-bracket.md-not { border-color: color-mix(in srgb, var(--cosmyre-md-not) 40%, var(--cosmyre-md-surface)); }

.md-pill.md-and { border-color: color-mix(in srgb, var(--cosmyre-md-and) 45%, var(--cosmyre-md-surface)); }
.md-pill.md-or { border-color: color-mix(in srgb, var(--cosmyre-md-or) 45%, var(--cosmyre-md-surface)); }
.md-pill.md-not { border-color: color-mix(in srgb, var(--cosmyre-md-not) 45%, var(--cosmyre-md-surface)); }

/* ---- rule row ---- */
.md-row {
    position: relative;
    overflow: hidden;
    display: flex;
    align-items: center;
    gap: 10px;
    background: var(--cosmyre-md-surface);
    border: 1px solid var(--cosmyre-md-row-border);
    border-radius: 11px;
    padding: 11px 12px 11px 15px;
    box-shadow: 0 1px 2px rgba(23, 19, 31, .04);
    animation: md-in .36s cubic-bezier(.22, .9, .28, 1) both;
}

/* The 3px band is what tells row kinds apart at a glance, and what carries a field's
   tag colour. A group card gets none — its bracket already marks it. */
.md-band {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
    background: var(--cosmyre-md-band-rule);
}

.md-row-sub {
    background: var(--cosmyre-md-group-subrow);
    border-radius: 10px;
    padding: 9px 11px 9px 14px;
    gap: 9px;
}

.md-grip {
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
    font-size: 13px;
    color: var(--cosmyre-md-grip);
    cursor: grab;
    user-select: none;
}

.md-grip:active { cursor: grabbing; }

.md-iconbtn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    flex: 0 0 auto;
    padding: 0;
    border: 0;
    border-radius: 7px;
    background: transparent;
    color: var(--cosmyre-md-muted-5);
    font-size: 11px;
    cursor: pointer;
}

.md-iconbtn:hover {
    background: var(--cosmyre-md-sunken);
    color: var(--cosmyre-md-ink-2);
}

.md-iconbtn-sm { width: 24px; height: 24px; font-size: 10px; }

/* ---- section eyebrow (rail headings, strip headings) ---- */
.md-eyebrow {
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: .11em;
    text-transform: uppercase;
    color: var(--cosmyre-md-muted-3);
}

/* ---- ambient tag dot ---- */
.md-dot {
    width: 7px;
    height: 7px;
    flex: 0 0 auto;
    border-radius: 50%;
}

/* ---- field chip ---- */
.md-chip {
    display: flex;
    align-items: center;
    gap: 9px;
    flex: 0 0 auto;
    padding: 8px 10px;
    border: 1px solid var(--cosmyre-md-list-border);
    border-radius: 9px;
    background: var(--cosmyre-md-surface);
    font-size: 13px;
    color: var(--cosmyre-md-ink-2);
    cursor: grab;
    user-select: none;
    transition: border-color .18s, color .18s;
}

.md-chip:hover {
    border-color: var(--cosmyre-md-band-rule);
    color: var(--cosmyre-md-ink);
}

.md-chip:active { cursor: grabbing; }

/* A field already in the rule stays in the list, dimmed with a tick, rather than
   disappearing — a list that reflows under the cursor is hard to work in. */
.md-chip-used {
    border-color: var(--cosmyre-md-group-border);
    background: var(--cosmyre-md-sunken);
    color: var(--cosmyre-md-muted-3);
    cursor: default;
}

.md-chip-used:hover { border-color: var(--cosmyre-md-group-border); color: var(--cosmyre-md-muted-3); }

.md-chip-label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.md-chip-check { color: var(--cosmyre-md-primary); font-size: 11px; flex: 0 0 auto; }

.md-dragging { opacity: .4; }

/* ---- drop slot ----
   Hidden until a drag is in flight, so the canvas stays quiet at rest. .md-slot-on is
   what the dragging state adds; .md-slot-hot is the one currently under the pointer. */
.md-slot {
    display: none;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 12px;
    border-radius: 11px;
    border: 1px dashed var(--cosmyre-md-slot-border);
    background: var(--cosmyre-md-slot-bg);
    color: var(--cosmyre-md-slot-fg);
    font-size: 13px;
    transition: border-color .16s, background .16s, color .16s;
}

.md-slot-on { display: flex; animation: md-in .2s ease both; }

.md-slot-hot {
    border-color: var(--cosmyre-md-slot-hot-border);
    background: var(--cosmyre-md-slot-hot-bg);
    color: var(--cosmyre-md-slot-hot-fg);
}

.md-slot-or {
    border-color: var(--cosmyre-md-slot-or-border);
    background: var(--cosmyre-md-slot-or-bg);
    color: var(--cosmyre-md-slot-or-fg);
}

.md-slot-or.md-slot-hot {
    border-color: var(--cosmyre-md-slot-or-hot-border);
    background: var(--cosmyre-md-slot-or-hot-bg);
    color: var(--cosmyre-md-slot-or-hot-fg);
}

/* ---- dashed "add" affordance ---- */
.md-add {
    display: flex;
    align-items: center;
    gap: 9px;
    width: 100%;
    padding: 11px 12px;
    border: 1px dashed var(--cosmyre-md-control-border-em);
    border-radius: 11px;
    background: transparent;
    color: var(--cosmyre-md-muted-3);
    font-size: 13.5px;
    text-align: left;
    cursor: pointer;
}

.md-add:hover {
    border-color: var(--cosmyre-md-primary);
    color: var(--cosmyre-md-ink-2);
}

/* ---- segmented control (view switch, category picker) ---- */
.md-segment {
    display: flex;
    gap: 3px;
    padding: 3px;
    border-radius: 11px;
    background: var(--cosmyre-md-segment-track);
    border: 1px solid var(--cosmyre-md-segment-border);
}

.md-segment button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    padding: 6px 13px;
    border: 0;
    border-radius: 8px;
    background: transparent;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--cosmyre-md-muted);
    cursor: pointer;
}

.md-segment .md-seg-on {
    color: var(--cosmyre-md-ink);
    background: var(--cosmyre-md-surface);
    box-shadow: 0 1px 2px rgba(23, 19, 31, .09);
}

/* ---- summary tokens ----
   An "any one of" run reads as a continuously tinted stretch rather than parentheses:
   every token from the first member to the last carries the tint, and only the ends
   are rounded, so the run looks like one object. The asymmetric padding is what keeps
   the tint unbroken between adjacent tokens. */
.md-tok-field {
    padding: 2px 6px;
    color: var(--cosmyre-md-ink-2);
    white-space: nowrap;
}

.md-tok-and {
    padding: 2px 6px;
    color: var(--cosmyre-md-and);
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: .09em;
}

.md-tok-or {
    padding: 3px 6px;
    color: var(--cosmyre-md-or);
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: .09em;
}

.md-tok-in { background: var(--cosmyre-md-orcard-head); padding: 3px 4px; }
.md-tok-first { padding-left: 8px; border-radius: 7px 0 0 7px; }
.md-tok-last { padding-right: 8px; border-radius: 0 7px 7px 0; }
.md-tok-only { padding: 3px 8px; border-radius: 7px; }

.md-summary-empty {
    color: var(--cosmyre-md-muted-4);
    font-style: italic;
}

/* ---- "any one of" group card ----
   The nested-group card, tinted to the OR colour. Its bracket lives INSIDE the body so
   the rule does not indent further and the two bracket lanes never collide. */
.md-orcard {
    position: relative;
    background: var(--cosmyre-md-surface);
    border: 1px solid var(--cosmyre-md-orcard-border);
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 1px 2px rgba(23, 19, 31, .04);
    animation: md-in .36s cubic-bezier(.22, .9, .28, 1) both;
}

.md-orcard-head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 12px;
    background: var(--cosmyre-md-orcard-head);
    border-bottom: 1px solid var(--cosmyre-md-orcard-rule);
}

.md-orcard-label { font-size: 13px; color: var(--cosmyre-md-muted); }
.md-orcard-hint { font-size: 13px; color: var(--cosmyre-md-muted-3); }

.md-orcard-chip {
    padding: 4px 9px;
    border-radius: 7px;
    background: var(--cosmyre-md-surface);
    border: 1px solid var(--cosmyre-md-orcard-chip-border);
    font-size: 12.5px;
    font-weight: 700;
    color: var(--cosmyre-md-orcard-chip-fg);
}

.md-orcard-actions { display: flex; align-items: center; gap: 2px; margin-left: auto; }

.md-orcard-body { position: relative; padding: 12px 14px 14px; }

/* ---- shared entry animation ---- */
@keyframes md-in {
    from { opacity: 0; transform: translateY(-4px); }
    to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .md-row,
    .md-orcard,
    .md-slot-on { animation: none; }
    .md-chip,
    .md-slot { transition: none; }
}

/* ---- Standard-list horizontal scroll for hand-rolled tables (master-detail lists
   that can't use DataTable). Wide tables grow and scroll instead of squishing. ---- */
.dnt-scroll {
    overflow-x: auto;
}

.dnt-scroll > table {
    width: max-content;
    min-width: 100%;
}
/* ============================================================
   Cosmyre tab bar — "Underline rail". THE DEFAULT SUB-NAVIGATION.
   Every tab bar in the app is <TabBar> (Components/Shared/TabBar.razor)
   rendering this markup; there is no other tab treatment. Behaviour —
   keyline placement, hover ghost, near-edge auto-scroll — is in
   wwwroot/js/tabs.js. See CLAUDE.md → "Standard tabs".

   Anatomy: the bar is the LAST child of the page-header card, bleeding
   past the card's side padding so its hairline spans edge to edge; the
   card has no bottom padding, so the 3px violet keyline under the active
   tab rides the card's bottom edge.

   Global (not scoped): tabs.js reaches into these elements and every
   caller shares them.
   ============================================================ */

/* A page-header card whose LAST child is a page-owned bar — one that swaps card content
   via OnSelect rather than navigating, so it has no URL and cannot live in the app header
   (CaseProcessingDetail). The zero bottom padding is what lets the keyline sit on the
   card's edge. Route sub-navigation does NOT use this: it lives in .ctab-top. */
.ctab-head {
    padding: 1.125rem var(--cosmyre-tab-bleed) 0;
}

/* Title block on the left, optional meta on the right. The gap down to the bar's
   hairline comes from the bar's own margin-top. */
.ctab-head-row,
.page-head-row {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    flex-wrap: wrap;
    gap: 1.5rem;
}

/* Bottom-aligned is right for a title block next to a line of meta, but a row whose
   right side is buttons wants its own alignment. app.css loads after Bootstrap, so
   the utility classes need re-stating here to beat the row's own align-items. */
.ctab-head-row.align-items-start,
.page-head-row.align-items-start { align-items: flex-start; }
.ctab-head-row.align-items-center,
.page-head-row.align-items-center { align-items: center; }

/* The ordinary page-header card: a title, and often meta or buttons beside it. Symmetric
   padding — unlike .ctab-head above, which zeroes the bottom for a bar that rides the
   card's edge. Use on the card itself in place of `.p-3` / `.card-body`'s own padding. */
.page-head {
    padding: 1.125rem var(--cosmyre-tab-bleed);
}

.ctab-bar {
    position: relative;
    display: flex;
    gap: 1.625rem;
    border-top: 1px solid var(--cosmyre-tab-rule);
}

/* Bleed to the header card's edges. Only inside .ctab-head, where the side
   padding is known — a bar used anywhere else stays inside its container. */
.ctab-head > .ctab-bar,
.ctab-head > .ctab-scroll {
    margin: .875rem calc(var(--cosmyre-tab-bleed) * -1) 0;
}

.ctab-head > .ctab-bar,
.ctab-head > .ctab-scroll > .ctab-bar {
    padding-inline: var(--cosmyre-tab-bleed);
}

/* The bar used as a card's own header — an in-page bar that swaps what the card below
   it shows, with no title above it. No top padding, no title gap, and no hairline:
   the card's top edge is already the line it sits under. */
.ctab-head-bare {
    padding: 0 var(--cosmyre-tab-bleed);
}

.ctab-head-bare > .ctab-bar,
.ctab-head-bare > .ctab-scroll {
    margin-top: 0;
}

.ctab-head-bare > .ctab-bar {
    border-top: 0;
}

/* ---- Header variant: THE bar, mounted in MainLayout's sticky app header ----
   Route sub-navigation lives here rather than in each page's header card, so it sits at
   the same Y on every page (card heights vary with their title block). No top hairline —
   the header's own border-bottom is the line the keyline rides.

   min-width:0 is load-bearing: .ctab-top and .app-header-right are flex siblings, so the
   bar shrinks and scrolls instead of ever running under the chat launcher or user menu. */
.ctab-top {
    display: flex;
    align-items: stretch;
    align-self: stretch;
    flex: 1 1 auto;
    min-width: 0;
}

/* Separates window chrome (hamburger, case bubble, Back) from navigation. */
.ctab-top::before {
    content: "";
    flex: 0 0 auto;
    align-self: center;
    width: 1px;
    height: 1.5rem;
    margin-right: 1rem;
    background: var(--bs-border-color, #e2e2e2);
}

.ctab-top > .ctab-scroll,
.ctab-top > .ctab-bar {
    flex: 1 1 auto;
    min-width: 0;
}

.ctab-top .ctab-bar {
    height: 100%;
    align-items: stretch;      /* tabs fill the 3.75rem band; .ctab centres its own contents */
    gap: 1.25rem;              /* tighter than the card bar's 1.625rem — the header is narrower */
    border-top: 0;
}

/* The bar sits centred in the space between the case label and the right-hand controls
   rather than hugging the case label with a wide gap after it.
   min-width:100% gives the centring something to act on — inside the scroller the bar is
   content-width by default, so justify-content would have no slack to distribute.
   `safe` is load-bearing: once the tabs are wider than the header (Case Settings has nine),
   plain `center` overflows equally in both directions and the first tab scrolls off the
   left edge with no way to reach it. `safe` drops back to flex-start exactly then. */
.ctab-top > .ctab-scroll > .ctab-bar {
    min-width: 100%;
    justify-content: safe center;
}

.ctab-top .ctab {
    padding-block: 0;
}

/* Flush to the bottom of the header band, directly above its border. NOT a negative
   bottom: the scroller below must clip vertically (overflow-x:auto forces overflow-y
   to a non-visible value), so anything hanging past the bar's edge is cut off — at
   bottom:-1px only half the keyline survived and it read as missing entirely. */
.ctab-top .ctab-ind,
.ctab-top .ctab-ghost {
    bottom: 0;
}

/* Right-edge fade only: the card bar's two-layer mask exists to keep a top hairline crisp
   and there is none here, and a left fade would permanently dim the first tab. mask-size
   and mask-position MUST be re-stated — inheriting the base rule's `100% 1px` first layer
   against a single-layer image renders the whole bar as a 1px sliver. overflow-y:hidden
   because `auto` on one axis computes `auto` on the other, which clips the keyline's glow. */
.ctab-top > .ctab-scroll {
    overflow-y: hidden;
    -webkit-mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 30px), transparent 100%);
    -webkit-mask-size: 100% 100%;
    -webkit-mask-position: 0 0;
    mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 30px), transparent 100%);
    mask-size: 100% 100%;
    mask-position: 0 0;
}

/* Too narrow for both header clusters and a bar. Sub-navigation falls back to the
   sidebar's nested menu, which carries the same items. */
@media (max-width: 900px) {
    .ctab-top { display: none; }
}

.ctab {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    padding: .8125rem .125rem .875rem;
    font-size: .845rem;
    font-weight: 600;
    letter-spacing: -.003em;
    line-height: 1;
    color: var(--cosmyre-tab-color);
    text-decoration: none;
    white-space: nowrap;
    background: none;
    border: 0;
    cursor: pointer;
    transition: color .34s ease;
}

.ctab:hover,
.ctab:focus-visible,
.ctab.active {
    color: var(--cosmyre-tab-color-active);
    text-decoration: none;
}

.ctab-icon {
    display: inline-flex;
    flex-shrink: 0;
    font-size: .9375rem;
    color: var(--cosmyre-tab-icon);
    transition: color .34s ease;
}

.ctab:hover .ctab-icon,
.ctab:focus-visible .ctab-icon,
.ctab.active .ctab-icon {
    color: var(--cosmyre-tab-accent);
}

.ctab:focus-visible {
    outline: 2px solid var(--cosmyre-tab-accent);
    outline-offset: -4px;
    border-radius: 4px;
}

/* Keyline (committed) and its hover preview. Width and X come from js/tabs.js,
   which measures the target tab — the keyline is exactly as wide as the tab. */
.ctab-ind,
.ctab-ghost {
    position: absolute;
    left: 0;
    bottom: 0;
    width: 0;
    height: 3px;
    border-radius: 3px;
    /* The keyline is a fill, so it takes the accent gradient — in dark that is the brand
       moon's crescent. The glow keeps riding the flat accent-rgb. */
    background: var(--cosmyre-tab-accent);
    background-image: var(--cosmyre-accent-grad);
    box-shadow: 0 0 10px rgba(var(--cosmyre-tab-accent-rgb), .6),
                0 0 2px rgba(var(--cosmyre-tab-accent-rgb), .9);
    pointer-events: none;
}

.ctab-ind {
    transition: transform .52s cubic-bezier(.22, .9, .28, 1),
                width .52s cubic-bezier(.22, .9, .28, 1);
}

.ctab-ghost {
    opacity: 0;
    transition: opacity .34s ease,
                transform .34s ease,
                width .34s ease;
}

/* No motion until the first measurement lands, so the keyline never flies in
   from x=0 when you arrive on a page (each tab is its own route). */
.ctab-bar:not(.ctab-ready) .ctab-ind {
    transition: none;
}

@media (prefers-reduced-motion: reduce) {
    .ctab-ind,
    .ctab-ghost { transition: none; }
}

/* ---- Long bars: horizontal scroll with fading edges ----
   <TabBar Scroll="true"> for bars that can overflow (Case Settings has nine).
   The bar never wraps to a second line. The mask has two layers so only the
   TABS fade at the edges: layer 1 keeps the top 1px — the hairline — fully
   opaque edge to edge, layer 2 fades everything below it. */
.ctab-scroll {
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-mask-image: linear-gradient(#000, #000),
                        linear-gradient(90deg, transparent 0, #000 26px, #000 calc(100% - 26px), transparent 100%);
    -webkit-mask-size: 100% 1px, 100% calc(100% - 1px);
    -webkit-mask-position: top left, bottom left;
    -webkit-mask-repeat: no-repeat;
    mask-image: linear-gradient(#000, #000),
                linear-gradient(90deg, transparent 0, #000 26px, #000 calc(100% - 26px), transparent 100%);
    mask-size: 100% 1px, 100% calc(100% - 1px);
    mask-position: top left, bottom left;
    mask-repeat: no-repeat;
}

.ctab-scroll::-webkit-scrollbar { display: none; }

.ctab-scroll > .ctab-bar {
    width: max-content;
    min-width: 100%;
}

/* ---- Profile validation rule tints: the offending cell carries the rule's severity.
   Semi-transparent backgrounds so they read on both light and dark themes. ---- */
td.cell-viol-error { background: rgba(220, 53, 69, .16); box-shadow: inset 2px 0 0 rgba(220, 53, 69, .7); }
td.cell-viol-warning { background: rgba(255, 193, 7, .18); box-shadow: inset 2px 0 0 rgba(255, 193, 7, .8); }
td.cell-viol-info { background: rgba(13, 202, 240, .14); box-shadow: inset 2px 0 0 rgba(13, 202, 240, .7); }


/* ---- Throbber ----------------------------------------------------------------------------
   The app's one loading indicator, rendered by Components/Shared/Throbber.razor: the Cosmyre
   mark with a dot in orbit and a ring pulsing out of it.

   The shape is lifted from the Navigator orb on purpose - it is the same object, standing still
   and spinning. Those rules live in CaseNavigator.razor.css, which is SCOPED and so unreachable
   from another component; these are the global twins. Prefixed throb* so the keyframes cannot
   collide with the nav* ones already in that scope.

   The TIMING is deliberately NOT the Navigator's. That orb sits in the dock all day, so a 4s
   orbit reads as a slow drift. A throbber is on screen for a few hundred milliseconds while a
   page swaps, and in that window a 4s orbit turns 30 degrees - the star looks pinned, which is
   the one thing a throbber must not look like. Worse, it restarts from the same angle every
   time, because the wrapper is display:none between navigations, so the star is not even in a
   new place. The orbit therefore runs at 1.1s and the ring at 1.5s: a 300ms glimpse is a third
   of a revolution, which reads as motion. Do not "restore" these to the Navigator's numbers.
   The star also has to clear the mark's drop-shadow, which is a separate trap and the one that
   actually hid it: see the note above throbOrbitDot for the three radii that must stay in
   order. */
.throb {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .75rem;
    padding: 2rem 1rem;
}

/* Floating over a table that is re-fetching, rather than taking its own space: the rows stay
   where they are and nothing jumps when the page lands. The tint is the card's own background at
   70%, so what is underneath reads as suspended rather than hidden. */
.throb-overlay {
    position: absolute;
    inset: 0;
    z-index: 4;                    /* over .dnt-fill's sticky header, which sits at 3 */
    background: color-mix(in srgb, var(--cosmyre-card-bg) 70%, transparent);
    backdrop-filter: blur(1px);
    pointer-events: none;          /* a spinner should never eat a click meant for the page */
}

.throb-orb {
    /* The orbit radius lives here, not in the keyframes, because it has to stay in a fixed
       relation to this box and to the mark's halo - see the note above throbOrbitDot. Custom
       properties inherit, so .throb-dot picks it up. */
    --cosmyre-throb-orbit: 23px;
    position: relative;
    width: 3.4rem;
    height: 3.4rem;
    flex: 0 0 3.4rem;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.throb-ring {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: 1.5px solid var(--cosmyre-throb-ring);
    animation: throbPulseRing 1.5s ease-out infinite;
}

.throb-mark {
    width: 1.7rem;
    height: 1.7rem;
    position: relative;
    z-index: 1;
    /* Kept tight on purpose. At 6px this glow reached r20 and the star orbits that same band,
       so the star spent its whole lap inside an indigo cloud of its own colour and read as part
       of the mark. 3px keeps the halo inside r17, well clear of the orbit. */
    filter: drop-shadow(0 0 3px rgba(120, 110, 240, .45));
}

/* The star in orbit — the part that actually says "working". Themed via --cosmyre-throb-star so
   it carries on a white card as well as it does on the Navigator's dark dock. */
.throb-dot {
    position: absolute;
    top: 50%;
    left: 50%;
    width: var(--cosmyre-throb-star-size);
    height: var(--cosmyre-throb-star-size);
    margin: calc(var(--cosmyre-throb-star-size) / -2);
    border-radius: 50%;
    background: var(--cosmyre-throb-star);
    box-shadow: var(--cosmyre-throb-star-glow);
    animation: throbOrbitDot 1.1s linear infinite;
}

.throb-label {
    font-size: .8125rem;
    color: var(--bs-secondary-color);
}

/* Reduced motion, deliberately PARTIAL - only the ring gives way. The two animations here are
   not equal risks and should not be switched off together:

     - the ring expands from 30px to ~98px, four times a second's worth of sweep across a good
       chunk of the card. That is the large-area movement the preference exists to suppress, so
       it fades in place instead.
     - the star is a 5px dot on a 46px path. It is small, local, and it is the ONLY part of this
       that distinguishes "working" from "stopped", which makes it functional rather than
       decorative. It keeps orbiting.

   Two earlier versions of this block got that balance wrong in the same direction. The first hid
   the star outright (animation: none; opacity: 0); the second parked it and cross-faded it. Both
   left a reduced-motion machine looking at a static logo, and the first also silently swallowed
   three rounds of tuning to the star's colour, speed and orbit - none of which can show up on
   something set to zero opacity. If this block ever grows a .throb-dot rule again, that is the
   history it is repeating. */
@media (prefers-reduced-motion: reduce) {
    .throb-ring {
        animation: throbFade 1.6s ease-in-out infinite;
    }
}

@keyframes throbPulseRing {
    0% { transform: scale(.55); opacity: .75; }
    100% { transform: scale(1.8); opacity: 0; }
}

/* The star's lap. The radius is --cosmyre-throb-orbit off .throb-orb, and the three numbers it
   sits between are load-bearing: the mark's halo must end inside it, and the orb's own radius
   must end outside it. Today that is halo r17 < orbit r23 (star spans r20.5-r25.5) < orb r27.2.
   Shrink the orb or widen the drop-shadow without moving this and the star disappears into the
   mark again - which is exactly what it did before, and it looks for all the world like the
   animation is broken rather than like a contrast problem. */
@keyframes throbOrbitDot {
    from { transform: rotate(0) translateX(var(--cosmyre-throb-orbit)) rotate(0); }
    to { transform: rotate(360deg) translateX(var(--cosmyre-throb-orbit)) rotate(-360deg); }
}

@keyframes throbFade {
    0%, 100% { opacity: .25; }
    50% { opacity: .75; }
}

