/* Photon component CSS. Carved from Cosmyre app.css (master a14b275c, 2026-09-26). Tokens live in photon.tokens.css; a component's own layout lives in its .razor.css. */

/* ============================================================
   STANDARD CONTROLS — buttons, dropdowns, scrollbars, grids.
   See docs/rules/buttons-and-controls.md. 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 docs/rules/) ----
   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 docs/rules/) ----
   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;
}

/* ---- Settings list (Processing Settings is the reference) ----
   A settings form reads DOWN: one setting per row, never two side by side. Every row is
   label | control | hint, and the rows share ONE grid (each .setting-row is display: contents),
   so the label column is as wide as the longest label and every control — a select or a switch
   alike — starts on the same x. A per-row flex line cannot do that: each row would size its own
   label, and the controls would stagger.
   The hairline is drawn on the cells because a display: contents row has no box to carry one.
   A switch goes in the control column with its text in the label column (a <label for>), which
   is what lets it line up with the selects instead of hanging off its own caption. */
.setting-list {
    display: grid;
    grid-template-columns: max-content minmax(0, 26rem) max-content;
    column-gap: 1.5rem;
    align-items: center;
    max-width: 56rem;
}

.setting-row {
    display: contents;
}

/* Every cell is as tall as a row holding a select, so a switch row and a select row keep the
   same rhythm down the page; each cell centres its own content in that height. */
.setting-row > * {
    display: flex;
    align-items: center;
    min-height: 4.1rem;
    padding-block: .8rem;
    border-bottom: 1px solid var(--cosmyre-card-border);
}

.setting-row:last-child > * {
    border-bottom: 0;
}

.setting-label {
    margin: 0;
    font-weight: 500;
    white-space: nowrap;
}

/* A switch is a control here, not a caption carrier: no reserved gutter for a label. */
.setting-control .form-check.form-switch {
    display: flex;
    align-items: center;
    min-height: 0;
    margin: 0;
    padding-left: 0;
}

.setting-control .form-switch .form-check-input {
    float: none;
    margin: 0;
    width: 2.5em;
    height: 1.3em;
    cursor: pointer;
}

.setting-control .form-select {
    width: 100%;
}

/* Narrow: the label takes a line of its own, with its control and hint on the line below. The
   cells stay in document order (label, control, hint), so auto-placement needs no `order` —
   which could not be used anyway: every row's cells are siblings in the one grid. */
@media (max-width: 575.98px) {
    .setting-list {
        grid-template-columns: minmax(0, 1fr) max-content;
        column-gap: .75rem;
    }

    .setting-row > * {
        min-height: 0;
    }

    .setting-row > .setting-label {
        grid-column: 1 / -1;
        padding-bottom: .15rem;
        border-bottom: 0;
        white-space: normal;
    }

    .setting-row > .setting-control {
        grid-column: 1;
        padding-top: .15rem;
    }

    .setting-row > .setting-hint {
        grid-column: 2;
        padding-top: .15rem;
    }
}

/* ---- 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;
}

.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);
}

/* GRIDLINES — a per-browser view preference (Options > Gridlines) carried on <html> as
   data-gridlines, so every DataTable in the app answers to it with no page wiring at all.
   "light" is what the app has always rendered: Bootstrap's own 1px row rule and no vertical
   lines, which is why it is the default and why there is no rule for it here. */
html[data-gridlines="none"] .dnt-table > table > :not(caption) > * > * {
    border-bottom-width: 0;
}

html[data-gridlines="medium"] .dnt-table > table {
    --bs-table-border-color: var(--cosmyre-grid-mid);
}

html[data-gridlines="dark"] .dnt-table > table {
    --bs-table-border-color: var(--cosmyre-grid-strong);
}

/* Medium and dark are actual GRIDS rather than heavier row rules: the vertical line is what makes
   a wide row readable across. The last cell's right edge is left to the card's own border. */
html[data-gridlines="medium"] .dnt-table > table > :not(caption) > * > *:not(:last-child),
html[data-gridlines="dark"] .dnt-table > table > :not(caption) > * > *:not(:last-child) {
    border-right: 1px solid var(--bs-table-border-color);
}

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

/* ---- Collapsible cards (see docs/rules/) ----
   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 docs/rules/) ----
   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. */
.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 docs/rules/page-layout.md.
   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; }
}

/* ---- 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 docs/rules/page-layout.md. */
.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);
}

/* 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 docs/rules/tabs.md. */
.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;
}

/* The file glyphs' contents are knocked out of the family-coloured page in solid white (solid reads
   better at 16 px than the old 78%); a few details (the chart bars, the envelope flap, the zipper's
   slot) are drawn back on top in currentColor. These live here and not in a component's scoped
   stylesheet because the paths are PhIcon's, inside its own CSS isolation scope, which no caller's
   scoped stylesheet can reach. Cosmyre's Courier desktop app draws the same values
   (Cosmyre.Core/FileTypes/FileGlyphs.cs), and Cosmyre's FileTypeIconTests holds these rules to them. */
.ft .ft-knockout {
    fill: #fff;
}

.ft .ft-knockout-stroke {
    fill: none;
    stroke: #fff;
}

/* PhFileTypeIcon's older file-page glyph on its untyped paper page: the one case where a white
   rule would vanish. The family glyphs' bare file-other page has no rules, so this never reaches it. */
.ft.ft-other .ft-knockout {
    fill: #a29cae;
}

[data-bs-theme="dark"] .ft.ft-other .ft-knockout {
    fill: #8a8398;
}

/* The folded corner: the page's own colour in shadow, never a second hue (FileGlyphs.FoldOpacity). */
.ft .ft-fold {
    fill: rgba(0, 0, 0, .22);
}

/* The grid's sort control: two arrows that never move, with the sorted one INKED. Nothing
   appears or disappears as a reader cycles asc -> desc -> off, so the header never reflows.
   These rules live HERE and not in DataTable.razor.css because the paths are rendered by
   Icon.razor, inside its own CSS isolation scope, which no caller's scoped stylesheet can reach. */
.dnt-sort .sa-up,
.dnt-sort .sa-down {
    fill: none;
    stroke: currentColor;
    stroke-width: .9;
    stroke-linejoin: round;
}

.dnt-sort.sort-asc .sa-up,
.dnt-sort.sort-desc .sa-down {
    fill: currentColor;
    stroke: currentColor;
}

/* A nested rail's expander: the chevron that swaps the child's tabs for its parent's. It sits in
   the rail head, left of the bar. js/tabs.js only ever queries .ctab elements, so the keyline's
   measurements ignore it. */
.ctab-head-nested {
    display: flex;
    align-items: stretch;
}

/* The bar normally bleeds to the card's edges with a NEGATIVE left margin. Beside an expander
   that margin would drag the bar back over the chevron and — .ctab-bar being position: relative —
   paint above it and swallow its clicks, which is exactly what it did. The expander takes over
   the left bleed instead: it is the first child, so its own negative margin only reaches the
   card's edge, with nothing to cover. */
.ctab-head-nested > .ctab-bar,
.ctab-head-nested > .ctab-scroll {
    flex: 1 1 auto;
    min-width: 0;
    margin-left: 0;
}

.ctab-expander {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    margin-left: calc(var(--cosmyre-tab-bleed) * -1);
    padding: 0 .5rem 0 var(--cosmyre-tab-bleed);
    border: 0;
    background: none;
    color: var(--cosmyre-tab-icon);
    cursor: pointer;
}

.ctab-expander:hover,
.ctab-expander.on {
    color: var(--cosmyre-tab-accent);
}

/* ---- View preferences ------------------------------------------------------------------ */

/* Text size scales the ROOT, and the app is written in rem throughout, so one attribute moves
   the chrome, the grids and the document panes together. Medium is the browser's own default,
   which is why it sets nothing: a reader who has raised their browser's font size keeps that. */
html[data-font-size="small"] { font-size: 14px; }
html[data-font-size="large"] { font-size: 18px; }

/* The Options dialog. A native <dialog>, because the app header is statically rendered and a
   Blazor modal there would have no circuit; it borrows the modal tokens so it still reads as
   the house dialog. */
.opt-dialog {
    padding: 0;
    border: 0;
    border-radius: .75rem;
    background: transparent;
    color: var(--bs-body-color);
}

.opt-dialog::backdrop {
    background: rgba(0, 0, 0, .45);
}

.opt-body {
    min-width: 22rem;
    padding: 1rem 1.125rem 1.125rem;
    border-radius: .75rem;
    background: var(--cosmyre-card-bg, var(--bs-body-bg));
    border: 1px solid var(--cosmyre-card-border, var(--bs-border-color));
    box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, .35);
}

.opt-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: .875rem;
}

.opt-title {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    font-weight: 600;
}

.opt-close {
    border: 0;
    background: none;
    color: inherit;
    opacity: .6;
    cursor: pointer;
}

.opt-close:hover { opacity: 1; }

.opt-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.5rem;
    padding: .375rem 0;
}

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

.opt-note {
    margin: .25rem 0 0;
    font-size: .75rem;
    color: var(--bs-secondary-color);
}

.opt-foot {
    display: flex;
    justify-content: flex-end;
    margin-top: .875rem;
}


/* Mass Code's target: one switch with a label on each side. The two sides are ONE choice, and a
   slider between them says that more directly than two adjacent buttons, which read as two
   things you could press. The live side is inked; the other stays quiet. */
.doc-target {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    margin: 0;
    cursor: pointer;
    user-select: none;
    white-space: nowrap;
}

.doc-target-side {
    font-size: .8125rem;
    color: var(--bs-secondary-color);
}

.doc-target-side.on {
    color: var(--cosmyre-accent-text);
    font-weight: 600;
}

/* The input stays in the accessibility tree and takes the keyboard; the track and knob beside it
   are what is seen. */
.doc-target-input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

.doc-target-track {
    position: relative;
    flex: 0 0 auto;
    width: 2.25rem;
    height: 1.25rem;
    border-radius: 1rem;
    background: rgba(var(--cosmyre-accent-rgb), .2);
    transition: background .15s;
}

.doc-target-knob {
    position: absolute;
    top: .1875rem;
    left: .1875rem;
    width: .875rem;
    height: .875rem;
    border-radius: 50%;
    background: var(--cosmyre-accent);
    transition: transform .15s;
}

.doc-target-input:checked ~ .doc-target-track .doc-target-knob {
    transform: translateX(1rem);
}

.doc-target-input:focus-visible ~ .doc-target-track {
    outline: 2px solid var(--cosmyre-accent);
    outline-offset: 2px;
}

/* 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;
}

/* ---- 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
     file types    — PhFileTypeIcon colours its wrapper by family (Excel green, Word blue…); the
                     glyph must inherit that, or every file type reads as the same purple

   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,
.ft .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);
}

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

/* 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 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;
    }
}

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

/* Highlight the picked choice in the Options dialog's segmented controls (prefs.js sets
   .active on the current theme and the current text size). */
.theme-btn.active,
.fontsize-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);
}

/* ============================================================
   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 docs/rules/tabs.md.

   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%;
}

/* ---- 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; }
}
