:root {
    --bg: #eef4fa;
    --panel: #ffffff;
    --ink: #102a43;
    --ink-medium: #324d6b;
    --ink-soft: #486581;
    --rule: #d9e6f3;
    --accent: #6f95bb;
    --accent-soft: #e3eef7;
    --accent-hover: #557fa9;
    /* Inline-link text. Tuned for >=4.5:1 contrast on white so link
       runs in grid cells (Surname, drill targets) read cleanly. The
       paler --accent stays for borders, focus rings and chip fills. */
    --link: #3d6691;
    --link-hover: #2c4f76;
    --error-bg: #fdecea;
    --error-border: #f5c6c0;
    --error-text: #8a1f1f;
    --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    --display: 'Fraunces', Georgia, 'Times New Roman', serif;
    --faint: #829ab1;

    /* R/A/G semantic palette - the canonical out-of-tolerance red,
       amber-warning, and in-tolerance green. Used by every balance
       cell, pill, badge, lozenge, top-mover delta and audit chip.
       Single source of truth so a future palette tweak is one block.
       (BalanceCss.For returns the .balance-red / -amber / -green
       class names against the Â±11.5 h tolerance band.) */
    --rag-red:                 #a85252;  /* text colour */
    --rag-amber:               #a47236;  /* text colour */
    --rag-green:               #3f7a4a;  /* text colour */
    --rag-red-strong:          #b85959;  /* solid pill / lozenge fill */
    --rag-green-strong:        #4a8b56;  /* solid pill / lozenge fill */
    --rag-red-soft-bg:         #f6eded;  /* badge / pill background */
    --rag-red-soft-bg-hover:   #fbeded;  /* hover variant */
    --rag-red-soft-border:     #e1c8c8;
    --rag-green-soft-bg:       #eef6f1;
    --rag-green-soft-border:   #c8e1cf;
    --rag-amber-soft-bg:       #f7f1e7;
    --rag-amber-soft-border:   #e3d2b3;

    /* Activity-type palette, used everywhere a duty bucket / NERG sub-type
       is shown (chip, daily strip, 52-week chart). Maps to the table in
       sql/schema/ob_shifts.md â†’ "Activity-type derivation". Pastel tier -
       low saturation, mid lightness, designed to read as a coherent system
       without overwhelming the eye. Keep these in sync with the C# palette
       in BalancesService.GetWeeklyHoursAsync / GetUnitWeeklyHoursAsync. */
    --duty-local:      #8aafce;  /* Local substantive */
    --duty-bank:       #dfb480;  /* Bank */
    --duty-agency:     #c89d6e;  /* Agency */
    --duty-workingday: #a8c4dc;  /* WorkingDay = Management Time, paler blue */
    --duty-annual:     #e9d075;  /* AnnualLeave, soft yellow */
    --duty-sickness:   #d4928a;  /* Sickness, soft red */
    --duty-study:      #8cbf9f;  /* StudyLeave, soft green */
    --duty-parenting:  #dcaebd;  /* Parenting, soft pink */
    --duty-other:      #bd97a3;  /* OtherLeave / Unknown, muted mauve */

    /* Inline UI arrows - disclosure carets (â–¾/â–¸), drill chevrons (â€º/â€¹),
       column-header sort indicators (â–²/â–¼), and the account-menu caret.
       Sized + weighted consistently so every arrow-like glyph in the app
       reads as the same family. Standalone glyph-button content (e.g.
       the prev/next chevrons on the Top movers scroller, which fill a
       full button) is NOT bound to this - those have their own size. */
    --ui-arrow-size: 25px;
    --ui-arrow-weight: 700;

    /* Mini-icon sizing. SVG icons reference a 24x24 viewBox and
       inherit currentColor from the surrounding text, so the only
       thing the call site has to set is the display size (and
       optionally an override colour on hover / active). */
    --icon-size: 12px;
    --icon-size-sm: 9px;
    --icon-size-lg: 15px;

    /* Height of the bottom-fixed activity strip - referenced by
       body { padding-bottom } + every scroll container's max-
       height calc so the sticky <tfoot> totals row sits above it.
       Matches the rendered strip: 21 px lozenge + 0.45 rem Ã— 2
       vertical padding + 1 px border-top on .activity-panel. */
    --activity-strip-h: 36px;
}

/* â”€â”€ All-units override toggle â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Pill button now lives at the bottom of the Units search dropdown
   (#unit-results), below the picker and Apply button. Visible only
   when ob_sentinel.CanChooseAllLocation = 1 for the current Trust.
   OFF state is a quiet outline pill; ON state highlights with the
   accent so the manager can see at a glance the override is active. */

.unit-results-override {
    border-top: 1px solid var(--rule);
    padding: 0.75rem 0.9rem;
    background: var(--bg);
    display: flex;
}
.unit-results-override .all-units-toggle {
    width: 100%;
    justify-content: center;
}
.all-units-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0 0.85rem;
    height: 31px;
    border-radius: 2px;
    border: 1px solid var(--rule);
    background: var(--panel);
    color: var(--ink-soft);
    font-size: 9px;
    font-weight: 500;
    cursor: pointer;
    white-space: nowrap;
    transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.all-units-toggle:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--ink);
}
.all-units-toggle:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.all-units-toggle.is-on {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--ink);
}
.all-units-dot {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--accent);
    display: inline-block;
}
@media (prefers-reduced-motion: reduce) {
    .all-units-toggle { transition: none; }
}

/* â”€â”€ Trust multi-select (header) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Replaces the old single-select Trust dropdown when the user has
   access to more than one Trust. <details>/<summary> disclosure
   pattern; each ob_sentinel-allowed Trust is a checkbox; toggling
   posts to /switch-trusts which redirects via HX-Redirect. */
.trust-multi { position: relative; }
.trust-multi-summary {
    list-style: none;
    cursor: pointer;
    padding: 0.4rem 0.75rem;
    border-radius: 2px;
    border: 1px solid var(--rule);
    background: var(--panel);
    color: var(--ink);
    font-size: 11px;
    line-height: 1.2;
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    user-select: none;
    transition: background 140ms ease, border-color 140ms ease;
    /* Fixed width so the comma-joined Trust list doesn't push the
       Loading lozenge sideways as Trusts are added/removed. The
       label inside ellipses if it overflows; the popover shows
       the full set on click. */
    width: 22ch;
    max-width: 22ch;
}
.trust-multi-summary .trust-multi-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    flex: 1 1 auto;
    min-width: 0;
}
.trust-multi-summary::-webkit-details-marker { display: none; }
.trust-multi-summary:hover { background: var(--accent-soft); border-color: var(--accent); }
.trust-multi-summary:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.trust-multi[open] .trust-multi-summary {
    background: var(--accent-soft);
    border-color: var(--accent);
}
.trust-multi-label { font-weight: 500; }
.trust-multi-caret {
    font-size: var(--ui-arrow-size);
    font-weight: var(--ui-arrow-weight);
    color: var(--ink-soft);
    line-height: 1;
    transition: transform 140ms ease;
}
.trust-multi[open] .trust-multi-caret { transform: rotate(180deg); }
/* Popover is a flex column - actions row stays at the top, the
   long list of Trust rows below scrolls independently. Keeps the
   Apply / Clear icon buttons in view however far the user scrolls. */
.trust-multi-popover {
    position: absolute;
    right: 0;
    top: calc(100% + 3px);
    min-width: 214px;
    max-height: 241px;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.4rem 0;
    z-index: 100;
    margin: 0;
}
.trust-multi-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.9rem;
    border-bottom: 1px solid var(--rule);
    margin-bottom: 0.25rem;
    font-size: 9px;
    flex: 0 0 auto;  /* keep at top, never compress */
}
.trust-multi-actions-hint {
    flex: 1 1 auto;
    font-size: 8px;
    color: var(--ink-soft);
    font-style: italic;
}
.trust-multi-rows {
    display: flex;
    flex-direction: column;
    /* Only this section scrolls - actions above stay anchored. */
    flex: 1 1 auto;
    overflow-y: auto;
    min-height: 0;
}
.trust-multi-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.45rem 0.9rem;
    cursor: pointer;
    font-size: 10px;
    color: var(--ink);
}
.trust-multi-row:hover { background: var(--accent-soft); }
.trust-multi-row input[type=checkbox] {
    accent-color: var(--accent);
    width: 11px;
    height: 11px;
}
/* Hard cap visual - when 3 Trusts are selected, the remaining
   unchecked rows go semi-transparent + non-interactive so the user
   sees the limit without having to read the hint. Already-ticked
   rows stay enabled so the user can swap their selection. */
.trust-multi-row.is-disabled {
    opacity: 0.4;
    cursor: not-allowed;
}
.trust-multi-row.is-disabled input[type=checkbox] { pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
    .trust-multi-summary, .trust-multi-caret { transition: none; }
}

/* Trust column on the multi-Trust Staff grid. Only emitted when
   Model.ShowTrustColumn is true. Slight ink-soft tint to distinguish
   it from the per-row data. */
.balance-grid td.trust-cell {
    font-weight: 500;
    color: var(--ink);
    white-space: nowrap;
}

/* â”€â”€ Account menu (header top-right) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Native <details>/<summary> disclosure popover. Trust selector stays
   on the strip; this captures user email, name, the optional Admin
   link, and Sign out. Click-outside closes via a small JS hook in
   _Layout.cshtml. */
.user-menu {
    position: relative;
}
.user-menu-trigger {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    list-style: none;
    cursor: pointer;
    padding: 0.4rem 0.75rem;
    border-radius: 2px;
    border: 1px solid var(--rule);
    background: var(--panel);
    color: var(--ink);     /* user's email/name is identity - primary text */
    font-size: 11px;
    line-height: 1.2;
    user-select: none;
    transition: background 140ms ease, border-color 140ms ease;
}
.user-menu-trigger::-webkit-details-marker { display: none; }
.user-menu-trigger:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
}
.user-menu-trigger:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.user-menu[open] .user-menu-trigger {
    background: var(--accent-soft);
    border-color: var(--accent);
}
.user-menu-trigger .user-email {
    font-weight: 500;
}
.user-menu-caret {
    font-size: var(--ui-arrow-size);
    font-weight: var(--ui-arrow-weight);
    color: var(--ink-soft);
    line-height: 1;
    transition: transform 140ms ease;
}
.user-menu[open] .user-menu-caret { transform: rotate(180deg); }
.user-menu-popover {
    position: absolute;
    right: 0;
    top: calc(100% + 3px);
    min-width: 214px;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.35rem 0;
    z-index: 100;
}
.user-menu-name {
    padding: 0.55rem 0.9rem 0.4rem;
    font-size: 11px;
    font-weight: 600;
    color: var(--ink);
    border-bottom: 1px solid var(--rule);
    margin-bottom: 0.25rem;
}
.user-menu-item {
    display: block;
    width: 100%;
    text-align: left;
    background: transparent;
    border: 0;
    padding: 0.55rem 0.9rem;
    font-size: 11px;
    color: var(--ink);
    text-decoration: none;
    cursor: pointer;
}
.user-menu-item:hover { background: var(--accent-soft); }
.user-menu-item:focus { outline: none; background: var(--accent-soft); }
.user-menu-form { margin: 0; padding: 0; }
.user-menu-signout { color: var(--ink-soft); border-top: 1px solid var(--rule); margin-top: 0.25rem; padding-top: 0.55rem; }
@media (prefers-reduced-motion: reduce) {
    .user-menu-caret { transition: none; }
}

/* â”€â”€ Admin: users & access â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
.admin-page {
    padding: 1.25rem 1.5rem;
    display: flex;
    flex-direction: column;
    gap: 1rem;
}
/* Close the gap between the top tab bar and the Administration sub-tab
   strip - .admin-page is flex with a 1rem gap, fine for body sections
   but the sub-tab strip is designed to merge into the active tab (same
   visual language as .work-page). Negative pull cancels the flex gap
   above the sub-tabs; the gap to the next sibling (.audit-header /
   .admin-header) stays. */
.admin-page > .tab-row + .subtab-row,
.admin-workload-page > .tab-row + .subtab-row {
    margin-top: -1rem;
}
/* Default banner shape - info-blue family. Used on every admin
   page above the form / grid. Two semantic variants live further
   down: .admin-banner-success (green), .admin-banner-error (red).
   Originally defined twice in this file at conflicting positions;
   merged here so the rule is one block. */
.admin-banner {
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    border-radius: 2px;
    padding: 0.7rem 1rem;
    margin: 0 0 1rem;
    color: var(--ink);
    font-size: 10px;
}
.admin-form {
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 1rem 1.25rem 1.25rem;
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
}
.admin-form-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}
.admin-form-header h2 {
    margin: 0;
    font-size: 1.1rem;
    font-weight: 600;
}
.admin-form-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(174px, 1fr));
    gap: 0.85rem 1rem;
}
.admin-field {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    /* Bumped from 13 px to 15 px to match the Staff grid toolbar
       label sizing - the admin pages were rendering noticeably
       tinier and the field labels were the worst offender. */
    font-size: 10px;
    color: var(--ink-soft);
}
.admin-field > span,
.admin-field > legend {
    font-weight: 600;
    font-size: 9px;
    color: var(--ink-soft);
}
.admin-field input[type=text],
.admin-field input[type=email],
.admin-field input[type=search],
.admin-field select {
    height: 31px;
    padding: 0 0.85rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    font-size: 11px;
    color: var(--ink);
    background: var(--panel);
    transition: border-color 140ms ease, box-shadow 140ms ease;
}
.admin-field input:hover,
.admin-field select:hover { border-color: var(--accent); }
.admin-field input:focus-visible,
.admin-field select:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.admin-field-hint {
    font-size: 8px;
    color: var(--faint);
}
.admin-flags-field {
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.5rem 0.75rem;
    display: grid;
    grid-template-columns: 1fr;
    gap: 0.75rem;
}
/* Two-column layout at wide viewports so the four sub-sections sit
   2x2 rather than stacked. Below the breakpoint they fall back to
   the original single column. */
@media (min-width: 900px) {
    .admin-flags-field {
        grid-template-columns: 1fr 1fr;
        column-gap: 1.5rem;
    }
}
.admin-flags-section {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}
.admin-flags-section-title {
    font-size: 8px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ink-soft);
    margin: 0 0 0.1rem;
    border-bottom: 1px solid var(--rule);
    padding-bottom: 0.2rem;
}
.admin-flags-section-body {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}
.admin-checkbox {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    font-size: 9px;
    color: var(--ink);
    cursor: pointer;
    line-height: 1.45;
}
.admin-checkbox input {
    accent-color: var(--accent);
    margin-top: 0.1rem;
    flex-shrink: 0;
}

.admin-units {
    border-top: 1px solid var(--rule);
    padding-top: 0.85rem;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}
.admin-units-header h3 {
    margin: 0;
    font-size: 10px;
    font-weight: 600;
    color: var(--ink);
}
.admin-unit-search { position: relative; }
.admin-unit-search input[type=search] {
    width: 100%;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    font-size: 9px;
}
.admin-unit-search input[type=search]:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.admin-unit-results:empty { display: none; }
.admin-unit-results {
    margin-top: 0.4rem;
    max-height: 188px;
    overflow-y: auto;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    padding: 0.5rem 0.75rem;
}
.admin-unit-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    min-height: 17px;
}
.admin-chip { background: var(--accent-soft); border: 1px solid var(--accent); }
.admin-chip-empty { font-size: 9px; color: var(--ink-soft); }

.admin-form-actions {
    display: flex;
    gap: 0.6rem;
    align-items: center;
}

.admin-list { display: flex; flex-direction: column; gap: 0.5rem; }
.admin-grid td { vertical-align: middle; }
.admin-person-first td { border-top: 1px solid var(--rule); }
.admin-row-inactive td { color: var(--faint); }
.admin-trust-id { color: var(--faint); font-size: 8px; margin-left: 0.25rem; }
.admin-flags { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.admin-flag {
    background: var(--bg);
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.1rem 0.4rem;
    font-size: 7px;
    color: var(--ink-soft);
}
.admin-flag-strong {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--ink);
}
.admin-pill {
    display: inline-block;
    padding: 0.15rem 0.55rem;
    border-radius: 7px;
    font-size: 8px;
    font-weight: 600;
}
.admin-pill-active   { background: var(--rag-green-soft-bg); color: var(--rag-green); border: 1px solid var(--rag-green-soft-border); }
.admin-pill-inactive { background: var(--rag-red-soft-bg); color: var(--rag-red); border: 1px solid var(--rag-red-soft-border); }
.admin-actions-col { white-space: nowrap; }
.admin-inline-form { display: inline; margin: 0; padding: 0; }

/* Status lozenges - colour-coded by RAG. Pastel only so the grid still
   reads at a glance; always pair colour with a textual label / arrow
   glyph for colour-blind users. Originated on login-audit rows; now
   reused anywhere a small inline status badge is needed (triangulation
   over/under-Finance flags on the Approvals summary, etc.) */
.audit-badge {
    display: inline-block;
    padding: 0.1rem 0.5rem;
    border-radius: 7px;
    font-size: 8px;
    font-weight: 600;
    border: 1px solid var(--rule);
    background: var(--bg);
    color: var(--ink-soft);
}
.audit-badge-ok    { background: var(--rag-green-soft-bg); border-color: var(--rag-green-soft-border); color: var(--rag-green); }
.audit-badge-err   { background: var(--rag-red-soft-bg); border-color: var(--rag-red-soft-border); color: var(--rag-red); }
.audit-badge-warn  { background: var(--rag-amber-soft-bg); border-color: var(--rag-amber-soft-border); color: var(--rag-amber); }
.audit-badge-muted { background: var(--bg);  border-color: var(--rule); color: var(--ink-soft); }
.audit-badge-info  { background: var(--accent-soft); border-color: var(--accent); color: var(--ink); }

/* Approvals per-unit summary: tight inline lozenge sitting next to the
   Estab / Demand / Actual WTE figure that flags whether the source is
   over (red â–²) or under (green â–¼) the Finance baseline. Extends the
   audit-badge family with a slightly tighter footprint so the badge
   doesn't push the WTE number off-balance in a dense grid row. */
.tri-fin-badge {
    margin-left: 0.3rem;
    padding: 0.05rem 0.35rem;
    vertical-align: 0.05rem;
    white-space: nowrap;
}
.tri-fin-arrow {
    font-size: 7px;
    margin-left: 0.1rem;
    line-height: 1;
}
.tri-fin-badge .tri-fin-arrow {
    margin-left: 0;
    margin-right: 0.15rem;
}
/* The .tri-fin-pill variant lives on the Budgets matrix - three of
   them sit side by side in the "vs Finance" column, each carrying a
   one-letter source prefix (E/D/A) plus an over/under arrow. Slightly
   tighter padding than .tri-fin-badge because they pack three to a
   cell rather than sitting solo next to a WTE figure. */
.tri-fin-pill {
    display: inline-block;
    padding: 0.05rem 0.3rem;
    margin: 0 0.1rem;
    min-width: 22px;
    text-align: center;
    font-weight: 700;
    letter-spacing: 0.02em;
}
.tri-fin-pill-blank {
    background: var(--bg);
    border-color: var(--rule);
    color: var(--ink-soft);
    opacity: 0.55;
}
.audit-ip { font-family: var(--mono); font-size: 9px; color: var(--ink-soft); }
.audit-ua { font-size: 8px; color: var(--ink-soft); max-width: 214px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* â”€â”€ Activity bar (always-visible compact strip) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   One thin row at the bottom of every signed-in page. Lozenge on
   the left, most-recent action inline next to it, Clear on the
   right. Clicking anywhere on the strip expands the bar UPWARDS
   to show the previous 4 rows above the strip (5 rows total).
   Esc or any click outside collapses it back. */
.activity-panel {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1499;
    background: var(--panel);
    border-top: 1px solid var(--rule);
    display: flex;
    flex-direction: column;
    transition: max-height 180ms ease;
}
.activity-history {
    overflow-y: auto;
    max-height: 0;
    transition: max-height 180ms ease;
    border-bottom: 1px solid var(--rule);
}
/* Expanded panel reserves up to 40% of the viewport for history
   rows and always shows a vertical scrollbar so the user can
   scroll back through everything that's been logged - the strip
   below only ever shows the latest, the rest live here. */
.activity-panel.is-expanded .activity-history {
    max-height: 40vh;
    overflow-y: scroll;
}
.activity-strip {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    padding: 0.45rem 0.95rem;
    cursor: pointer;
    min-height: 29px;
    user-select: none;
}
.activity-lozenge {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.25rem 0.85rem;
    height: 21px;
    border-radius: 669px;
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    color: var(--ink);
    font-weight: 600;
    font-size: 9px;
    flex: 0 0 auto;
}
.activity-lozenge-count {
    display: inline-block;
    min-width: 12px;
    padding: 0 4px;
    background: var(--accent);
    color: white;
    border-radius: 669px;
    font-size: 8px;
    font-weight: 700;
    line-height: 12px;
    text-align: center;
}
.activity-lozenge-arrow {
    font-size: 16px;
    font-weight: var(--ui-arrow-weight);
    color: var(--accent-hover);
    line-height: 1;
    margin-left: 0.2rem;
}
.activity-panel.is-expanded .activity-lozenge-arrow { transform: rotate(180deg); }
.activity-latest {
    flex: 1 1 auto;
    display: grid;
    grid-template-columns: 47px minmax(94px, 147px) 1fr 47px;
    gap: 0.85rem;
    align-items: center;
    font-size: 9px;
    color: var(--ink);
    overflow: hidden;
    min-width: 0;
}
.activity-latest-empty {
    grid-column: 1 / -1;
    color: var(--ink-soft);
    font-style: italic;
}
.activity-latest .activity-time {
    font-family: var(--mono);
    color: var(--ink-soft);
    font-size: 9px;
    font-variant-numeric: tabular-nums;
}
.activity-latest .activity-level {
    font-weight: 600;
    color: var(--ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.activity-latest .activity-msg {
    color: var(--ink-soft);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.activity-latest .activity-duration {
    font-family: var(--mono);
    color: var(--ink-soft);
    font-size: 9px;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.activity-strip-actions {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    flex: 0 0 auto;
}
.activity-strip-action {
    background: transparent;
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0 0.85rem;
    height: 21px;
    color: var(--ink-soft);
    font: inherit;
    font-size: 9px;
    font-weight: 600;
    cursor: pointer;
}
.activity-strip-action:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--ink);
}
/* History list above the strip - one row per entry, same grid
   shape as the latest-row inline so columns line up. */
.activity-history-row {
    display: grid;
    grid-template-columns: 64px 54px minmax(94px, 147px) 1fr 47px 19px;
    gap: 0.85rem;
    align-items: center;
    padding: 0.4rem 1rem;
    border-bottom: 1px solid var(--rule);
    font-size: 9px;
    color: var(--ink);
}
.activity-history-row:last-child { border-bottom: none; }
.activity-history-row .activity-time {
    color: var(--ink-soft);
    font-family: var(--mono);
    font-size: 9px;
    font-variant-numeric: tabular-nums;
}
.activity-history-row .activity-pad { visibility: hidden; }
.activity-history-row .activity-level {
    font-weight: 600;
    color: var(--ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.activity-history-row .activity-msg {
    color: var(--ink-soft);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.activity-history-row .activity-duration {
    color: var(--ink-soft);
    font-family: var(--mono);
    font-size: 9px;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.activity-panel-header {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.75rem 1.25rem;
    border-bottom: 1px solid var(--rule);
    background: var(--bg);
}
.activity-panel-title { font-weight: 600; color: var(--ink); font-size: 13px; }
.activity-panel-sub   { font-size: 11px; color: var(--ink-soft); flex: 1; }
.activity-panel-action {
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.55rem 1.1rem;
    font: inherit;
    font-size: 11px;
    color: var(--ink);
    cursor: pointer;
    min-height: 28px;
    transition: background 120ms ease, border-color 120ms ease;
}
.activity-panel-action:hover { background: var(--accent-soft); border-color: var(--accent); }
.activity-panel-action:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.activity-panel-close {
    background: transparent;
    border: 1px solid transparent;
    color: var(--ink-soft);
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
    width: 28px;
    height: 28px;
    border-radius: 2px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.activity-panel-close:hover { background: var(--accent-soft); color: var(--ink); border-color: var(--rule); }
.activity-panel-close:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

.activity-list {
    overflow-y: auto;
    flex: 1 1 auto;
    font-size: 11px;
    color: var(--ink);
}
.activity-empty {
    padding: 1.25rem;
    color: var(--ink-soft);
    font-style: italic;
    font-size: 12px;
}
/* Columns: time Â· action title Â· detail Â· duration Â· replay.
   Body text uses --ink (primary), only the secondary metadata
   (time + duration) drops to --ink-soft. The .activity-history-row
   selector above carries the grid layout for the actually-used
   row variant; the original .activity-row was removed once the
   history strip became the only consumer. */
.activity-time {
    color: var(--ink-soft);
    font-family: var(--mono);
    font-size: 10px;
    font-variant-numeric: tabular-nums;
}
.activity-level {
    font-weight: 600;
    color: var(--ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.activity-msg {
    color: var(--ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.activity-duration {
    color: var(--ink-soft);
    font-family: var(--mono);
    font-variant-numeric: tabular-nums;
    text-align: right;
    font-size: 10px;
}
.activity-error .activity-level { color: var(--rag-red); }
.activity-warn  .activity-level { color: var(--rag-amber); }
.activity-info  .activity-level { color: var(--ink); }

/* Replay control - only emitted on rows that map to a re-runnable
   GET (currently /balances/grid). Clicking it re-syncs the toolbar
   inputs and re-fires the same request. */
.activity-replay {
    background: transparent;
    border: 1px solid var(--rule);
    border-radius: 50%;
    width: 24px;
    height: 24px;
    color: var(--ink-soft);
    cursor: pointer;
    font-size: 13px;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background 120ms ease, border-color 120ms ease, color 120ms ease, transform 200ms ease;
}
.activity-replay:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--ink);
    transform: rotate(90deg);
}
.activity-replay:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.activity-replay-placeholder { display: inline-block; width: 24px; height: 24px; }
@media (prefers-reduced-motion: reduce) {
    .activity-replay { transition: none; }
    .activity-replay:hover { transform: none; }
}
@media (prefers-reduced-motion: reduce) {
    .activity-panel { animation: none; }
}

/* Screen-reader-only utility - visible labels for headings/buttons that
   are graphically obvious to sighted users (icons, empty drill columns)
   but otherwise opaque to assistive tech. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* â”€â”€ Staff details page â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

.staff-page {
    padding: 1.25rem 1.5rem;
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.staff-header {
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 1.25rem 1.5rem;
}

.staff-header-top {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
}

.staff-identity { min-width: 0; }

.staff-back {
    color: var(--accent);
    text-decoration: none;
    font-size: 9px;
}

.staff-back:hover { text-decoration: underline; }

.staff-name {
    /* Inherit the body Inter sans face. Was var(--display) (Fraunces
       serif) which read as 'strange font' next to the sans-serif
       grids and stat panels everywhere else in the app. */
    font-weight: 600;
    font-size: 19px;
    margin: 0.35rem 0 0.15rem 0;
    color: var(--ink);
    letter-spacing: -0.01em;
}

.staff-subtitle {
    margin: 0;
    color: var(--ink-soft);
    font-size: 10px;
}

.staff-subtitle .sep { margin: 0 0.5rem; color: var(--faint); }

.stat-strip {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 0.75rem;
    margin-top: 1.1rem;
    padding-top: 1.1rem;
    border-top: 1px solid var(--rule);
}

.stat {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.stat-label {
    color: var(--ink-soft);
    font-size: 9px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-weight: 600;
}

.stat-value {
    color: var(--ink);
    font-size: 15px;
    font-weight: 600;
}

.stat-value.num {
    font-family: var(--mono);
    font-variant-numeric: tabular-nums;
}

.stat-meta {
    color: var(--ink-soft);
    font-size: 9px;
}

.stat-value.balance-red { color: var(--rag-red); }
.stat-value.balance-green { color: var(--rag-green); }

/* Reconciliation lozenges in the Units grid. Default to grey ("pending");
   the wired-up version flips to green on approval. */
.lozenge-col { text-align: center; width: 80px; }

/* Sign-off button on the Units grid - shaped like the Staff grid's
   Generate button so the two read as the same kind of row-level
   action. Two states: pending (accent outlined, clickable) and
   signed (filled green, opens audit). Replaces the old round
   lozenge. */
.signoff-button {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    background: transparent;
    border: 1px solid var(--accent);
    color: var(--accent);
    cursor: pointer;
    font: inherit;
    font-size: 9px;
    padding: 0.35rem 0.7rem;
    border-radius: 2px;
    white-space: nowrap;
    transition: background 140ms ease, color 140ms ease, border-color 140ms ease, box-shadow 140ms ease;
}
.signoff-button:hover {
    background: var(--accent);
    color: #fff;
}
.signoff-button:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.signoff-button-signed {
    /* Pastel-blue "signed" state - same visual family as the
       identity column headers. Reads as "done + audit trail
       available" rather than as a status colour. */
    border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 15%, var(--panel));
    color: var(--accent);
    font-weight: 600;
}
.signoff-button-signed:hover {
    background: var(--accent);
    color: #fff;
}
.signoff-button.just-signed { animation: lozenge-just-signed 600ms cubic-bezier(0.2, 0.8, 0.2, 1); }

/* Optimistic-flip state - applied by JS the instant a sign-off
   POST goes out (one-click lozenge OR modal Submit). Looks the
   same as a real signed lozenge so the user sees confirmation
   without waiting for the server response. The grid swap that
   comes back via balances-refresh replaces this wholesale; on
   failure the class is removed and the lozenge reverts to its
   pending state. */
.signoff-button.is-optimistically-signed {
    border-color: var(--rag-green-strong);
    color: var(--rag-green-strong);
    /* Subtle pulse confirms the click registered. Override the
       generic .is-button-busy dim so the flash reads clearly. */
    animation: lozenge-just-signed 500ms cubic-bezier(0.2, 0.8, 0.2, 1);
    opacity: 1;
}
.signoff-button.is-optimistically-signed.is-button-busy {
    opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
    .signoff-button.is-optimistically-signed { animation: none; }
}

.lozenge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 15px;
    height: 15px;
    border-radius: 50%;
    font-size: 7px;
    font-weight: 700;
    color: white;
    line-height: 1;
}

/* Pending = needs the ward manager's attention (red); approved = signed
   off for the current year (green). Rejected reserved for future use. */
.lozenge-pending { background: var(--rag-red-strong); }
.lozenge-approved { background: var(--rag-green-strong); }
.lozenge-rejected { background: var(--rag-red-strong); }

.lozenge-button {
    border: none;
    cursor: pointer;
    padding: 0;
    transition: filter 140ms ease, transform 140ms ease, box-shadow 140ms ease;
}

.lozenge-button:hover {
    filter: brightness(1.1);
}
.lozenge-button:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--accent-soft);
    border-radius: 50%;
}

/* Brief pulse the first time a pending lozenge transitions to signed
   via the quick-action POST - visual confirmation the click landed. */
.lozenge.just-signed { animation: lozenge-just-signed 600ms cubic-bezier(0.2, 0.8, 0.2, 1); }
@keyframes lozenge-just-signed {
    0%   { transform: scale(0.85); box-shadow: 0 0 0 0 rgba(30, 91, 39, 0.55); }
    45%  { transform: scale(1.18); box-shadow: 0 0 0 5px rgba(30, 91, 39, 0); }
    100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(30, 91, 39, 0); }
}

/* Sign-off modal */
.signoff-form {
    position: relative;
    padding: 1.4rem 1.5rem 1.2rem;
    min-width: min(375px, 92vw);
    max-width: 482px;
}

.signoff-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    margin-bottom: 1rem;
}

.signoff-eyebrow {
    display: block;
    color: var(--ink-soft);
    font-size: 9px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.signoff-title {
    margin: 0.15rem 0 0 0;
    font-size: 15px;
    font-weight: 600;
}

.signoff-status { margin-bottom: 1rem; }

.signoff-current {
    padding: 0.75rem 1rem;
    border-radius: 2px;
    border: 1px solid var(--rule);
    font-size: 10px;
}

.signoff-current-signed   { background: var(--rag-green-soft-bg); border-color: var(--rag-green-soft-border); color: var(--rag-green); }
.signoff-current-pending  { background: var(--bg); }

/* Sign-off preview - shows the manager what the per-unit totals look
   like before they commit. Identical aggregates land in the snapshot. */
.signoff-preview {
    margin: 1rem 0;
    padding: 0.85rem 1rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--bg);
}
.signoff-preview-title {
    font-size: 9px;
    color: var(--ink);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-weight: 600;
    margin-bottom: 0.5rem;
}
.signoff-preview-stats {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.5rem 0.75rem;
}
.signoff-preview-stat {
    display: flex;
    flex-direction: column;
}
.signoff-preview-label {
    font-size: 9px;
    color: var(--ink-soft);
    font-weight: 500;
}
.signoff-preview-value {
    font-size: 15px;
    font-weight: 600;
}
.signoff-preview-meta {
    font-size: 9px;
    color: var(--ink-soft);
}
.signoff-preview-hint {
    margin: 0.6rem 0 0;
    font-size: 8px;
    color: var(--ink-soft);
    font-style: italic;
}

.signoff-note {
    margin-top: 0.4rem;
    font-style: italic;
    color: var(--ink);
}

.signoff-form-row {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    margin-bottom: 0.75rem;
}

.signoff-form-row label {
    font-size: 9px;
    color: var(--ink-soft);
    font-weight: 500;
}

.signoff-note-input {
    width: 100%;
    padding: 0.6rem 0.8rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    font: inherit;
    background: var(--panel);
    resize: vertical;
}

/* Attestation block - the wording the Ward Manager accepts when
   they sign off. Sits just above the action buttons and keeps the
   prose visible without dominating the modal. The text uses
   white-space: pre-line so the source string's newlines + leading
   ' - ' bullets render naturally. */
.signoff-attestation {
    margin: 0.75rem 0 0.55rem;
    padding: 0.6rem 0.8rem;
    border: 1px solid var(--accent);
    border-radius: 2px;
    background: var(--accent-soft);
}
.signoff-attestation-text {
    font-size: 9px;
    line-height: 1.45;
    color: var(--ink);
    white-space: pre-line;
    margin-bottom: 0.6rem;
}
.signoff-attestation-ack {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    cursor: pointer;
    font-size: 9px;
    font-weight: 600;
    color: var(--ink);
}
.signoff-attestation-ack input[type=checkbox] {
    width: 12px;
    height: 12px;
    accent-color: var(--accent);
    cursor: pointer;
}
/* Type-name e-signature - the second factor on top of the tick.
   Visually subordinate to the attestation text but unmistakeable
   when its match state flips, so the manager knows when they can
   submit. .is-ok = name typed and matches; .is-bad = typed but
   doesn't match yet. */
.signoff-typed-name-label {
    font-size: 10px;
    font-weight: 600;
    color: var(--ink);
}
/* .signoff-typed-name-input height / border / focus-ring now inherits
   from .filter-input (sign-off modal writes both classes). The bespoke
   override below kept only for callers without .filter-input alongside
   - belt-and-braces in case a future surface drops the canonical class. */
.signoff-typed-name-input {
    font-size: 11px;
}
.signoff-typed-name-hint {
    font-size: 10px;
    color: var(--ink-soft);
}
.signoff-typed-name-hint.is-ok {
    color: var(--rag-green-strong);
    font-weight: 600;
}
.signoff-typed-name-hint.is-ok::before { content: "\2713 "; }
.signoff-typed-name-hint.is-bad {
    color: var(--rag-red-strong);
}
.signoff-typed-name-hint.is-bad::before { content: "\2717 "; }
/* Disabled primary button reads as inert - so the manager has a
   clear visual cue that the box must be ticked first. */
.primary-button[disabled] {
    background: var(--rule);
    border-color: var(--rule);
    color: var(--ink-soft);
    cursor: not-allowed;
}

.signoff-actions {
    display: flex;
    justify-content: flex-end;
    gap: 0.75rem;
    margin-top: 1rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--rule);
}

.signoff-history { margin-top: 1.25rem; }

.signoff-history summary {
    cursor: pointer;
    color: var(--ink-soft);
    font-size: 9px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 0.4rem 0;
}

/* Inline edit-in-place for the Note column on /sign-offs. View mode is
   the persisted text + a small âœŽ button that swaps the TD for the edit
   form. Edit form: input + Save (âœ“) + Cancel (âœ•). Escape on the input
   also cancels via an hx-on:keydown handler in the partial. */
.signoff-note-view {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    min-height: 15px;
}
.signoff-note-text {
    flex: 1;
    color: var(--ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.signoff-note-edit-btn { opacity: 0.55; flex: 0 0 auto; }
.signoff-note-cell:hover .signoff-note-edit-btn,
.signoff-note-edit-btn:focus-visible { opacity: 1; }
.signoff-note-edit {
    display: flex;
    align-items: center;
    gap: 0.3rem;
    margin: 0;
    padding: 0;
}
.signoff-note-input-inline {
    flex: 1;
    min-width: 0;
    padding: 0.25rem 0.5rem;
    border: 1px solid var(--accent);
    border-radius: 2px;
    font: inherit;
    font-size: 10px;
    background: var(--panel);
    color: var(--ink);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.signoff-note-input-inline:focus {
    outline: none;
}
.signoff-note-cell.is-editing { background: var(--accent-soft); }
.signoff-note-save  { color: var(--rag-green); }
.signoff-note-cancel { color: var(--ink-soft); }

dialog#detail-modal {
    border: 1px solid var(--rule);
    border-radius: 5px;
    padding: 0;
    background: var(--panel);
    color: var(--ink);
    max-width: 92vw;
}

/* Prominent loading badge anchored to the right end of the
   filter row. Hidden until .is-busy is toggled on by the same
   inflight tracker that drives the page-progress bar; provides
   a far more visible 'something is happening' cue for parm
   changes that otherwise look like a frozen toolbar for 1-3 s.
   Pulse on the accent dot draws the eye without being noisy. */
.filter-loading-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    margin-left: auto;
    padding: 0.5rem 0.95rem;
    /* Dark-blue (--ink) rather than the medium --accent so the badge
       holds the eye when work is in flight. Same colour family the
       login button switches to under .is-loading. */
    background: var(--ink);
    border: 1px solid var(--ink);
    border-radius: 669px;
    color: #fff;
    font-weight: 600;
    font-size: 10px;
    opacity: 0;
    transform: translateY(-1px);
    pointer-events: none;
    transition: opacity 140ms ease, transform 140ms ease;
}
.filter-loading-badge.is-busy {
    opacity: 1;
    transform: translateY(0);
}
.filter-loading-spinner {
    display: inline-block;
    width: 11px;
    height: 11px;
    /* Match the login spinner shape: 2 px ring + white pair on top/right
       + subtle outer ring so the spinner reads against the dark-blue
       badge background without disappearing. */
    border: 2px solid rgba(255, 255, 255, 0.35);
    border-top-color: #fff;
    border-right-color: #fff;
    border-radius: 50%;
    animation: spinner-spin 0.7s linear infinite;
    flex: 0 0 auto;
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.15);
}
.filter-loading-label {
    letter-spacing: 0.02em;
}
@media (prefers-reduced-motion: reduce) {
    .filter-loading-badge { transition: none; }
    .filter-loading-spinner { animation: none; }
}
/* â”€â”€ Excel upload column-mapping panel â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Shared shape between Finance + ESR uploads. Each canonical field
   appears on a row with its detected header, confidence badge, and
   a dropdown so the user can pick a different column if AI got it
   wrong or didn't find anything. Required fields with missing
   columns get a red row treatment so the eye lands there first. */
.upload-map-section {
    margin: 0.85rem 0 0.6rem;
    border: 1px solid var(--rule);
    border-radius: 5px;
    background: var(--panel);
    padding: 0.85rem 1rem 1rem;
}
.upload-map-title { margin: 0 0 0.25rem; font-size: 0.95rem; font-weight: 600; }
.upload-map-sub {
    margin: 0 0 0.7rem;
    color: var(--ink-soft);
    font-size: 0.78rem;
    line-height: 1.45;
    max-width: 80ch;
}
.upload-map-sub strong { color: var(--ink); }
.upload-map-form { display: flex; flex-direction: column; gap: 0.6rem; }
.upload-map-table { width: 100%; border-collapse: collapse; font-size: 0.78rem; }
.upload-map-table th {
    text-align: left;
    padding: 0.35rem 0.6rem;
    font-weight: 600;
    color: var(--ink-soft);
    border-bottom: 1px solid var(--rule);
    text-transform: uppercase;
    letter-spacing: 0.03em;
    font-size: 0.65rem;
}
.upload-map-table td {
    padding: 0.45rem 0.6rem;
    border-bottom: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
    vertical-align: middle;
}
.upload-map-table tr:last-child td { border-bottom: none; }
.upload-map-row-low td { background: color-mix(in srgb, var(--rag-red, #c1392b) 5%, var(--panel)); }
.upload-map-col-field    { width: 22%; }
.upload-map-col-header   { width: 22%; }
.upload-map-col-conf     { width: 12%; }
.upload-map-col-sample   { width: 24%; }
.upload-map-col-pick     { width: 22%; }
.upload-map-sample {
    /* Three comma-separated values pulled from the column. Mono so
       cost-centre codes line up; the muted ink ensures the sample
       reads as supporting context, not as the canonical detected-
       column header above it. */
    font-family: ui-monospace, "SF Mono", "Consolas", monospace;
    font-size: 11px;
    color: var(--muted);
}
.upload-map-sample-empty {
    color: var(--muted);
    opacity: 0.5;
}
.upload-map-req {
    margin-left: 0.4rem;
    padding: 1px 6px;
    border-radius: 2px;
    background: color-mix(in srgb, var(--rag-red, #c1392b) 12%, #fff);
    color: var(--rag-red, #c1392b);
    font-size: 0.62rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.upload-map-missing { color: var(--ink-muted, var(--ink-soft)); font-style: italic; }
.upload-map-badge {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 2px;
    font-size: 0.7rem;
    font-weight: 600;
    line-height: 1.4;
    border: 1px solid transparent;
}
.upload-map-badge-high {
    background: color-mix(in srgb, var(--rag-green, #2f7a3a) 12%, #fff);
    color: var(--rag-green, #2f7a3a);
    border-color: color-mix(in srgb, var(--rag-green, #2f7a3a) 28%, var(--rule));
}
.upload-map-badge-medium {
    background: color-mix(in srgb, var(--rag-amber, #c98c1a) 12%, #fff);
    color: color-mix(in srgb, var(--rag-amber, #c98c1a) 90%, var(--ink) 10%);
    border-color: color-mix(in srgb, var(--rag-amber, #c98c1a) 28%, var(--rule));
}
.upload-map-badge-ai {
    background: color-mix(in srgb, var(--accent) 12%, #fff);
    color: var(--accent);
    border-color: color-mix(in srgb, var(--accent) 28%, var(--rule));
}
.upload-map-badge-user {
    background: color-mix(in srgb, var(--accent) 18%, #fff);
    color: var(--accent);
    border-color: var(--accent);
}
.upload-map-badge-missing {
    background: color-mix(in srgb, var(--rag-red, #c1392b) 10%, #fff);
    color: var(--rag-red, #c1392b);
    border-color: color-mix(in srgb, var(--rag-red, #c1392b) 35%, var(--rule));
}
.upload-map-badge-skip {
    background: color-mix(in srgb, var(--ink) 5%, #fff);
    color: var(--ink-muted, var(--ink-soft));
    border-color: var(--rule);
}
.upload-map-select {
    width: 100%;
    padding: 0.3rem 0.5rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: #fff;
    font: inherit;
    font-size: 0.75rem;
    color: var(--ink);
}
.upload-map-select:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.upload-map-actions {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
    padding-top: 0.25rem;
}

/* Upload-zone busy lozenge - same chrome as the top-strip Loading
   lozenge, but lives inside the drop-zone label and shows only
   while the upload form has the .htmx-request class. Same shape
   everywhere: a busy state on any surface looks like the Loading
   lozenge so the audience never wonders "is this a different
   spinner?". */
.upload-busy-badge {
    display: none !important;
    margin-left: 0;
    opacity: 1 !important;
    transform: translateY(0) !important;
    margin: 0.5rem auto 0;
}
.htmx-request .upload-busy-badge,
.htmx-request.upload-busy-badge {
    display: inline-flex !important;
}

/* Confirm-and-save action bar at the foot of every upload preview.
   Shared between Budget Matching and Leave Matching so the
   button shape, copy and disabled state are identical on both. */
.upload-confirm-actions {
    margin: 1rem 0 0;
    padding: 0.85rem 1rem 0.9rem;
    border: 1px solid var(--rule);
    border-radius: 5px;
    background: color-mix(in srgb, var(--accent) 4%, var(--panel));
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem 1.25rem;
    align-items: center;
    justify-content: space-between;
}
.upload-confirm-meta {
    flex: 1 1 26ch;
    color: var(--ink);
    font-size: 0.82rem;
    line-height: 1.45;
}
.upload-confirm-meta strong { color: var(--ink); }
.upload-confirm-buttons {
    display: flex;
    gap: 0.5rem;
    flex: 0 0 auto;
    align-items: center;
}
.upload-confirm-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    position: relative;
}
.upload-confirm-btn[disabled] {
    opacity: 0.55;
    cursor: not-allowed;
}
.upload-confirm-btn.htmx-request .upload-confirm-label { visibility: hidden; }
.upload-confirm-indicator {
    display: none;
    position: absolute;
    inset: 0;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    color: #fff;
    font-weight: 600;
}
.upload-confirm-btn.htmx-request .upload-confirm-indicator { display: inline-flex; }

/* â”€â”€ Row accent stripe â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Slim accent-coloured stripe on the left edge of every grid row.
   Mirrors the Inbox .work-item-kind lozenge pattern - the eye picks
   up the row boundary at a glance even on a dense grid. Hover
   intensifies the stripe to the full accent colour. The stripe
   replaces the leftmost cell's left border so column borders aren't
   doubled. */
.balance-grid tbody tr > td:first-child,
.budgets-matrix tbody tr > td:first-child,
.leave-grid tbody tr > td:first-child,
.upload-map-table tbody tr > td:first-child,
.duties-grid tbody tr > td:first-child,
.kpi-grid tbody tr > td:first-child {
    border-left: 3px solid color-mix(in srgb, var(--accent) 35%, transparent);
}
.balance-grid tbody tr:hover > td:first-child,
.budgets-matrix tbody tr:hover > td:first-child,
.leave-grid tbody tr:hover > td:first-child,
.upload-map-table tbody tr:hover > td:first-child,
.duties-grid tbody tr:hover > td:first-child,
.kpi-grid tbody tr:hover > td:first-child {
    border-left-color: var(--accent);
}

/* â”€â”€ Sub-tab row (second-level navigation inside a main tab) â”€â”€â”€â”€â”€
   Pill-style sub-tabs sitting inside the toolbar shelf. The
   ACTIVE sub-tab has a solid white fill + accent top-edge so it
   merges visually with the white active main tab above - the eye
   reads "the main tab AND this sub-tab are the chosen path." The
   inactive sub-tabs sit on a soft accent tint so the row reads as
   a single navigation strip and the active one pops.
   Row sits flush against the content below (zero bottom margin)
   so real estate isn't wasted between nav and data. */
/* Sub-tabs are independent 2 px square buttons spaced apart - same
   visual language as every other button on the grid's top row (Export,
   Generate Notes, Hide Notes etc.). Active sub-tab fills --accent
   with white text. Per the square-button vocabulary, no joined
   segmented look - each button is its own rectangle with a 0.25 rem
   gap so the row reads as "row of buttons" not "one big control". */
.subtab-row {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0;
    margin: 0;
    background: transparent;
    border: none;
    border-radius: 0;
    overflow: visible;
    height: 24px;
}
.subtab {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    height: 24px;
    padding: 0 0.95rem;
    font: inherit;
    font-size: 9px;
    font-weight: 600;
    line-height: 1;
    color: var(--ink);
    text-decoration: none;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    cursor: pointer;
    white-space: nowrap;
    transition: background-color 140ms ease, color 140ms ease, border-color 140ms ease;
}
.subtab:not(.is-active):hover {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--ink);
}
.subtab:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.subtab.is-active {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
    font-weight: 700;
}
.subtab.is-active:hover {
    background: var(--accent-hover);
    border-color: var(--accent-hover);
    color: #fff;
}
.subtab .icon,
.subtab .icon-sm,
.subtab svg.icon {
    width: 11px;
    height: 11px;
    color: currentColor;
    flex: 0 0 auto;
}
.subtab-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 14px;
    height: 14px;
    padding: 0 4px;
    border-radius: 7px;
    background: var(--accent-soft);
    color: var(--accent);
    border: 1px solid transparent;
    font-size: 8px;
    font-weight: 700;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}
.subtab.is-active .subtab-count {
    background: rgba(255, 255, 255, 0.25);
    color: #fff;
    border-color: rgba(255, 255, 255, 0.4);
}@media (prefers-reduced-motion: reduce) {
    /* Drop every tab + sub-tab transition so users who prefer
       reduced motion get the final visual state with no
       intermediate animation. */
    .tab,
    .subtab {
        transition: none !important;
    }
}
/* In the site-header user-strip the badge sits as the first child,
   so the parm-row's "push right" auto-margin doesn't apply. Size
   tuned bigger than the parm-row variant so it reads at a glance
   on the top chrome line. */
.user-strip .filter-loading-badge {
    margin-left: 0;
    padding: 0.45rem 0.95rem;
    font-size: 12px;
}
.user-strip .filter-loading-spinner { width: 13px; height: 13px; }

/* â”€â”€ Column-header trigger + shared context menu â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Each sortable/filterable column header is a single button with
   a small caret hint; clicking opens #col-header-menu (Sort A->Z /
   Sort Z->A / Filter... / Clear filter) positioned below it. The
   approach scales to narrow columns where a per-column chevron
   would overflow. Filtered state tints the whole header cell so a
   glance shows which columns are constraining the visible rows. */
.col-header-trigger {
    display: flex;
    align-items: center;
    gap: 0.3rem;
    background: transparent;
    border: none;
    padding: 3px 4px;
    margin: 0;
    color: inherit;
    font: inherit;
    text-transform: inherit;
    letter-spacing: inherit;
    cursor: pointer;
    min-height: 24px;
    width: 100%;
    text-align: inherit;
    border-radius: 2px;
    transition: background 140ms ease;
}
.balance-grid thead th.num .col-header-trigger {
    justify-content: flex-end;
}
.col-header-trigger:focus-visible {
    outline: none;
}
/* Hover/focus background sits on the parent <th> via :has() further
   down so the WHOLE header cell darkens (rgba overlay), not just the
   button's content box. Without that the th's 0.85rem horizontal
   padding leaves an unhighlighted gutter, painfully visible on narrow
   columns like Contract and Roster Hrs. */
/* Filtered-state visual cue. Pale tint fills the whole header
   cell; a 3px accent-coloured bottom stripe is the strong glanceable
   signal. No icons (Greg's rule: never small icons). */
.balance-grid thead th.col-filtered {
    background: var(--accent-soft);
    box-shadow: inset 0 -3px 0 var(--accent);
}

.col-header-menu {
    position: fixed;
    margin: 0;
    padding: 4px;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    box-shadow: 0 5px 16px rgba(0, 0, 0, 0.18);
    min-width: 134px;
    z-index: 1000;
    list-style: none;
    display: flex;
    flex-direction: column;
}
/* Author rule for the closed state. The UA popover stylesheet has
   `[popover]:not(:popover-open) { display: none }` but UA origin
   loses to ANY author origin rule regardless of specificity, so the
   `.col-header-menu { display: flex }` block above kept the menu
   visible at top:0 / left:0 on every page load. Open state is still
   driven by showPopover() / hidePopover() in _Layout.cshtml. */
.col-header-menu:not(:popover-open) { display: none; }
.col-header-menu-item {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 5px 8px;
    min-height: 27px;
    background: transparent;
    border: none;
    text-align: left;
    font: inherit;
    font-size: 10px;
    color: var(--ink);
    cursor: pointer;
    border-radius: 2px;
    transition: background 120ms ease;
}
.col-header-menu-item[hidden] { display: none; }
.col-header-menu-item:hover:not(:disabled),
.col-header-menu-item:focus-visible:not(:disabled) {
    background: var(--accent-soft);
    outline: none;
}
.col-header-menu-item:disabled {
    color: var(--faint);
    cursor: default;
    opacity: 0.6;
}
.col-header-menu-glyph {
    flex: 0 0 12px;
    width: 12px;
    height: 12px;
    text-align: center;
    color: var(--ink-soft);
    line-height: 1;
}
.col-header-menu-item:hover:not(:disabled) .col-header-menu-glyph {
    color: var(--accent);
}
.col-header-menu-clear .col-header-menu-glyph {
    color: var(--rag-red);
}

/* â”€â”€ Locked columns â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Pinned-left columns - applied by barnaclesApplyLockedColumns in
   _Layout.cshtml on every grid render. The inline `left:Npx` sets
   each cell's cumulative offset; the class adds sticky + opaque
   background + a small lock-icon marker on header cells in the
   range. .col-locked-last carries a 1 px accent border on the
   right edge so the eye reads "the locked run ends here".

   Z-index ordering: locked tbody cells need an explicit z-index
   so they paint above the regular tbody cells that slide past
   them during horizontal scroll. Sticky-vs-static painting at
   z-index:auto is fragile when the surrounding grid has other
   sticky elements creating stacking contexts (the KPI grid's
   two-row sticky thead at z-index 2 / 3 was making the locked
   body cells appear underneath the scrolling cells). Locked
   thead / tfoot bump to 4 so the top-left and bottom-left
   corners stay above the rest of their respective rows during
   cross-axis scroll. */
.balance-grid th.col-locked,
.balance-grid td.col-locked {
    position: sticky;
    background-color: var(--panel);
    background-clip: padding-box;
}
.balance-grid td.col-locked-last,
.balance-grid th.col-locked-last {
    border-right: 1px solid var(--accent);
}
.balance-grid tbody td.col-locked { z-index: 1; }
.balance-grid thead th.col-locked { z-index: 4; }
.balance-grid tfoot tr.totals-row td.col-locked { z-index: 4; }
/* KPI grid's stacked thead has its own z-index rules (kpi-category-row
   z:3, row-2 z:2) that are more specific than the .col-locked default
   above, so without these targeted overrides a LOCKED kpi-grid thead
   cell ends up at the same z-index as its scrolling siblings - and
   later DOM siblings paint on top, so non-locked headers visibly slide
   over the locked column when the user scrolls right. Bumping locked
   cells to a higher z-index than their row's non-locked stickies fixes
   the "headers crash into locked columns" report. tfoot needs the same
   treatment because the KPI grid's totals row sits inside the
   .balance-grid family but the .col-locked rule wins on specificity
   for tbody already (single class .col-locked vs none on the parent),
   it's only the thead double-row that loses. */
.balance-grid.kpis-grid thead tr.kpi-category-row th.col-locked { z-index: 6; }
.balance-grid.kpis-grid thead tr:not(.kpi-category-row) th.col-locked { z-index: 5; }
.balance-grid.kpis-grid tfoot tr.totals-row td.col-locked { z-index: 5; }
.balance-grid.kpis-grid tbody tr.group-subtotal td.col-locked { z-index: 3; }
.balance-grid tbody tr:hover > td.col-locked {
    background-color: var(--accent-soft);
}
.col-lock-icon {
    display: inline-flex;
    align-items: center;
    margin-left: 0.35rem;
    color: var(--ink-soft);
    flex: 0 0 auto;
}
.col-lock-icon .icon {
    width: 11px;
    height: 11px;
}

/* Column-filter dialogs ride on the .unit-picker-dialog chrome
   (header + frame + toolbar at top), so they're narrower than the
   true multi-select pickers but share the search-and-actions row. */
.col-filter-dialog.unit-picker-dialog { width: min(322px, 96vw); }
.col-filter-form-modal { /* inherits .unit-picker-modal */ }
.col-filter-body {
    padding: 0.9rem 1rem 1rem;
}
/* Numeric dialogs put the spacer on the left of the toolbar so the
   Clear and Apply buttons hug the right edge - same visual rhythm
   as the text dialogs where the search input fills that space. */
.unit-picker-toolbar-spacer { flex: 1 1 auto; }

.col-filter-presets {
    margin: 0 0 1rem;
    padding: 0.6rem 0.75rem 0.75rem;
    background: var(--accent-soft);
    border-radius: 2px;
}
.col-filter-presets-head,
.col-filter-custom-head {
    margin: 0 0 0.6rem;
    font-size: 9px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--ink-soft);
    font-weight: 600;
}
.col-filter-preset {
    display: block;
    width: 100%;
    min-height: 27px;
    text-align: left;
    background: var(--panel);
    border: 1px solid var(--rule);
    color: var(--ink);
    padding: 0.6rem 0.8rem;
    margin-bottom: 0.45rem;
    border-radius: 2px;
    font: inherit;
    font-size: 10px;
    cursor: pointer;
    transition: background 140ms ease, border-color 140ms ease;
}
.col-filter-preset:last-child { margin-bottom: 0; }
.col-filter-preset:hover,
.col-filter-preset:focus-visible {
    background: var(--panel);
    border-color: var(--accent);
    color: var(--accent-hover);
}
.col-filter-custom {
    border-top: 1px dashed var(--rule);
    padding-top: 0.85rem;
}
.col-filter-fieldset {
    border: none;
    padding: 0;
    margin: 0 0 0.85rem;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.4rem 1rem;
}
.col-filter-fieldset-inline {
    grid-template-columns: repeat(3, auto);
    gap: 0 0.6rem;
    justify-content: start;
}

/* Compact one-line dialog variant: no header, no framed body, just
   radios + clear + apply on a single horizontal strip. Reserved for
   trivial binary / ternary picks where the column-header label
   already names the filter (Manager note: All / With / Without). */
.col-filter-dialog-compact {
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    box-shadow: 0 8px 24px rgba(16, 42, 67, 0.18);
    padding: 0;
    width: auto;
    max-width: none;
}
.col-filter-dialog-compact::backdrop {
    background: rgba(16, 42, 67, 0.08);
}
.col-filter-form-compact {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.45rem 0.55rem;
}
.col-filter-form-compact label {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    font-size: 10px;
    padding: 4px 6px;
    border-radius: 2px;
    cursor: pointer;
    white-space: nowrap;
}
.col-filter-form-compact label:hover {
    background: var(--accent-soft);
}
.col-filter-form-compact input[type="radio"] {
    width: 12px;
    height: 12px;
    margin: 0;
}

.col-filter-fieldset legend {
    grid-column: 1 / -1;
    font-size: 10px;
    color: var(--ink-soft);
    padding: 0;
    margin-bottom: 0.4rem;
}
.col-filter-fieldset label {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 10px;
    padding: 5px 5px;
    border-radius: 2px;
    cursor: pointer;
    min-height: 24px;
}
.col-filter-fieldset label:hover {
    background: var(--accent-soft);
}
.col-filter-fieldset input[type="radio"] {
    width: 12px;
    height: 12px;
}
.col-filter-numrow {
    display: flex;
    gap: 1rem;
    align-items: center;
    margin: 0;
    padding: 0.7rem 0.8rem;
    background: var(--bg);
    border-radius: 2px;
}
.col-filter-numrow label {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 10px;
    color: var(--ink-soft);
}
.col-filter-num {
    width: 64px;
    min-height: 24px;
    padding: 0.45rem 0.6rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    font: inherit;
    font-size: 10px;
    background: var(--panel);
    color: var(--ink);
}

/* â”€â”€ Top-of-viewport in-flight progress bar â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Lit by JS whenever the inflight Map has any open HTMX request.
   2 px high, accent-coloured, indeterminate shuttle animation so
   a slow request reads as 'still working' even when nothing else
   on the page is changing. Honours reduced-motion - the bar still
   appears (so the user knows something is in flight), it just
   doesn't shuttle. */
.page-progress {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 1px;
    background: transparent;
    z-index: 10000;
    pointer-events: none;
    opacity: 0;
    overflow: hidden;
    transition: opacity 140ms ease;
}
.page-progress::before {
    content: '';
    display: block;
    width: 30%;
    height: 100%;
    background: var(--accent);
    transform: translateX(-100%);
}
.page-progress.is-busy { opacity: 1; }
.page-progress.is-busy::before {
    animation: page-progress-shuttle var(--progress-loop, 1100ms) linear infinite;
}
@keyframes page-progress-shuttle {
    0%   { transform: translateX(-100%); }
    100% { transform: translateX(400%); }
}
@media (prefers-reduced-motion: reduce) {
    .page-progress.is-busy::before {
        animation: none;
        transform: none;
        width: 100%;
    }
}

/* â”€â”€ Ward Guardian badge â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Small accent shield rendered next to a unit name when that
   OrgUnitId is in the current user's BarnacleLocString (i.e. the
   user's ob_sentinel scope - the set the Ward Guardian programme
   covers for this user). Lives inline alongside the name so it
   reads as 'this unit, on the WG programme' at a glance. Hover
   title spells it out. */
/* Reserve a fixed-width slot for the 'Hide analysis' / 'Show analysis'
   label so the button doesn't change width when toggled (which would
   shift everything after it in the status bar). Both strings are 13
   chars so either side of the toggle is fine as the floor. */
.notes-collapse-label {
    display: inline-block;
    min-width: 72px;
    text-align: left;
}

.wg-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: 0.45rem;
    color: var(--rag-green-strong);
    vertical-align: -2px;
    line-height: 1;
}
.wg-badge .icon {
    width: 12px;
    height: 12px;
    stroke-width: 2;
}

/* WG-only button in the unit picker action row - shield icon in
   accent green so it visually pairs with the row-level WG shield
   badges below it. */
/* Combined-class specificity (2) so we win over the bare
   .unit-picker-action color rule (specificity 1) regardless of
   source order. Without this the shield icon picks up the dim
   ink-soft action default and reads as almost invisible. */
/* WG shield - 3-state cycle (All -> WG only -> Non-WG -> All).
   Specificity is .unit-picker-action.unit-picker-wg-only (2) so we
   beat the default .unit-picker-action ink-soft regardless of source
   order. */
.unit-picker-action.unit-picker-wg-only {
    color: var(--rag-green-strong);
    border-color: var(--rag-green-soft-border);
}
.unit-picker-action.unit-picker-wg-only:hover {
    background: var(--rag-green-soft-bg);
    border-color: var(--rag-green-strong);
}
.unit-picker-action.unit-picker-wg-only.is-wg-only {
    background: var(--rag-green-strong);
    border-color: var(--rag-green-strong);
    color: #fff;
}
.unit-picker-action.unit-picker-wg-only.is-wg-only:hover {
    background: var(--rag-green-strong);
    color: #fff;
    filter: brightness(0.95);
}
.unit-picker-action.unit-picker-wg-only.is-wg-non {
    background: var(--rag-red-strong);
    border-color: var(--rag-red-strong);
    color: #fff;
}
.unit-picker-action.unit-picker-wg-only.is-wg-non:hover {
    background: var(--rag-red-strong);
    color: #fff;
    filter: brightness(0.95);
}
/* Legacy .is-active state kept for any consumer that hasn't moved
   to the 3-way classes - same look as is-wg-only. */
.unit-picker-action.unit-picker-wg-only.is-active {
    background: var(--rag-green-strong);
    border-color: var(--rag-green-strong);
    color: #fff;
}
.unit-pick.is-wg .wg-badge { margin-left: 0.5rem; }

/* Rollup variant on the Divisions grid: shield + 'X / Y' count.
   Same accent shield as the per-unit badge but with a number
   inline so a row reads 'Surgery [shield] 8 / 14'. */
.wg-rollup {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    margin-left: 0.5rem;
    padding: 1px 5px 1px 3px;
    border: 1px solid var(--rule);
    border-radius: 669px;
    color: var(--accent-hover);
    font-size: 8px;
    font-weight: 600;
    line-height: 1.4;
    background: var(--accent-soft);
}
.wg-rollup .icon { width: 8px; height: 8px; }
.wg-rollup-count { font-variant-numeric: tabular-nums; }

/* â”€â”€ Active filters strip â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Lives between the toolbar and the grid. Hidden entirely when
   no filters are active (so the page stays calm); chips appear
   inline when filters bite. Each chip is a button - clicking it
   removes that filter. Re-fetched via HTMX on every
   balances-refresh so it stays in sync with the grid below it. */
.active-filters {
    display: flex;
    /* Single-row strip - never wraps onto a second line. If more
       chips exist than fit, the strip scrolls horizontally instead
       of pushing the grid down. The Filtering-by label + Clear all
       link stay pinned at either end via flex-shrink: 0 on them.
       The slot is ALWAYS rendered (see .is-empty rule below: only
       contents go invisible) so the grid below never shifts up or
       down when filters are added or cleared. */
    flex-wrap: nowrap;
    align-items: center;
    gap: 0.5rem;
    overflow-x: auto;
    overflow-y: hidden;
    /* Minimal reserved height. Shorter than the chips' own
       intrinsic 29 px - the chips can render slightly taller than
       the slot and the user won't notice. Critical bit: this
       height stays whether filters are active or not, so the grid
       row 1 is at a constant Y. */
    height: 22px;
    padding: 0.15rem 0.25rem;
    margin: 0.2rem 0;
    /* Pale-blue thin scrollbar matching the grid's own. */
    scrollbar-color: color-mix(in srgb, var(--accent) 55%, var(--panel)) transparent;
    scrollbar-width: thin;
}
.active-filters::-webkit-scrollbar { height: 4px; }
.active-filters::-webkit-scrollbar-track { background: transparent; }
.active-filters::-webkit-scrollbar-thumb {
    background: color-mix(in srgb, var(--accent) 55%, var(--panel));
    border-radius: 2px;
}
.active-filters-label,
.active-filter-chip,
.active-filters-clear-all { flex-shrink: 0; }
.active-filters.is-empty {
    /* Slot stays in the layout flow so the grid below never shifts
       when filters appear / disappear; only the contents go
       invisible. visibility:hidden preserves the reserved height
       from the rule above. */
    visibility: hidden;
}
.active-filters-label {
    color: var(--ink-soft);
    font-size: 9px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-right: 0.15rem;
}
.active-filter-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.3rem 0.55rem 0.3rem 0.85rem;
    background: var(--accent-soft);
    border: 1px solid var(--rule);
    border-radius: 669px;
    color: var(--ink);
    font: inherit;
    font-size: 9px;
    line-height: 1.2;
    cursor: pointer;
    transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.active-filter-chip:hover {
    background: var(--panel);
    border-color: var(--accent);
}
.active-filter-chip-key {
    color: var(--ink-soft);
    font-weight: 600;
}
.active-filter-chip-val {
    color: var(--ink);
    font-weight: 500;
    max-width: 28ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.active-filter-chip-close {
    color: var(--ink-soft);
    font-size: 11px;
    line-height: 1;
    margin-left: 0.1rem;
    width: 12px;
    height: 12px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    transition: background 120ms ease, color 120ms ease;
}
.active-filter-chip:hover .active-filter-chip-close {
    background: var(--rag-red-soft-bg);
    color: var(--rag-red);
}
/* Non-interactive variant (B6 / G19 H14). Used by
   _UnitIdsExportModal + _UnitProfilesModal where a chip-shaped
   element shows a unit / value for reference but is NOT clickable
   - drops the pointer cursor + hover affordance so the user
   doesn't expect a Clear-on-click behaviour that isn't there. */
.active-filter-chip.chip-display,
.chip-display.active-filter-chip {
    cursor: default;
}
.active-filter-chip.chip-display:hover,
.chip-display.active-filter-chip:hover {
    background: var(--accent-soft);
    border-color: var(--rule);
}

/* B9 - stale-KPI-profile inline notice. Amber strip above the
   KPI grid that surfaces when the active KPI Profile's saved
   category list includes entries that no current manifest group
   matches. Re-save the profile to update; the strip is locally
   dismissible but reappears on every render until that happens. */
.kpi-stale-profile-notice {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.4rem 0.7rem;
    margin: 0 0 0.5rem;
    background: var(--rag-amber-soft-bg);
    border: 1px solid var(--rag-amber-soft-border);
    border-radius: 2px;
    color: var(--rag-amber);
    font-size: 11px;
    line-height: 1.35;
}
.kpi-stale-profile-lead {
    font-weight: 700;
    flex: 0 0 auto;
}
.kpi-stale-profile-notice em {
    font-style: normal;
    font-weight: 600;
    color: var(--ink);
}
.kpi-stale-profile-notice strong { font-weight: 600; }
.kpi-stale-profile-dismiss {
    margin-left: auto;
    flex: 0 0 auto;
    width: 18px;
    height: 18px;
    border: none;
    background: transparent;
    color: inherit;
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
    padding: 0;
    border-radius: 50%;
    transition: background 120ms ease;
}
.kpi-stale-profile-dismiss:hover {
    background: rgba(0, 0, 0, 0.06);
}
.active-filters-clear-all {
    background: transparent;
    border: 0;
    color: var(--accent-hover);
    cursor: pointer;
    font: inherit;
    font-size: 9px;
    padding: 0.3rem 0.55rem;
    margin-left: 0.25rem;
    text-decoration: underline;
    text-underline-offset: 2px;
}
.active-filters-clear-all:hover { color: var(--rag-red); }

/* â”€â”€ Per-button busy feedback â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Any button triggering an HTMX request gets a brief in-flight
   look (slight dim + 'wait' cursor) so the click registers
   instantly even before the server responds. Combined with the
   top-of-viewport progress bar this gives the perception of
   optimistic UI without the complexity of true client-side
   reconciliation. Toggled by inline JS in _Layout.cshtml. */
.is-button-busy {
    opacity: 0.65;
    cursor: progress !important;
    pointer-events: none;
}

/* â”€â”€ Share-this-view button â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Lives next to 'Ask a question' in the toolbar. Click copies a
   URL that recreates the current filter state (units, grade,
   name, asOf, tab) when pasted into another browser. The
   .share-view-flash class is applied for ~1.6 s on success so
   the label can read 'Link copied' without a separate toast. */
.share-view-button {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}
.share-view-button.share-view-flash {
    background: var(--rag-green-soft-bg);
    border-color: var(--rag-green-soft-border);
    color: var(--rag-green);
}
.share-view-label { font-weight: 500; }
@media (prefers-reduced-motion: reduce) {
    .share-view-button.share-view-flash { transition: none; }
}

/* â”€â”€ Command palette (Ctrl+K typeahead) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Centred modal that searches units + staff in the user's scope.
   Single input at the top + a scrollable results pane below. The
   results pane is HTMX-loaded with 180 ms debounce on the input. */
.command-palette {
    border: 1px solid var(--rule);
    border-radius: 7px;
    padding: 0;
    background: var(--panel);
    color: var(--ink);
    box-shadow: 0 12px 32px rgba(16, 42, 67, 0.22);
    width: 429px;
    max-width: 92vw;
}
.command-palette::backdrop {
    background: rgba(16, 42, 67, 0.35);
}
.command-palette-modal {
    display: flex;
    flex-direction: column;
    max-height: 70vh;
}
.command-palette-search-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.85rem 1rem;
    border-bottom: 1px solid var(--rule);
}
.command-palette-search-icon {
    flex: 0 0 auto;
    color: var(--ink-soft);
    width: 15px;
    height: 15px;
}
.command-palette-input {
    flex: 1 1 auto;
    border: 0;
    outline: 0;
    background: transparent;
    color: var(--ink);
    font: inherit;
    font-size: 11px;
    padding: 0.35rem 0.25rem;
}
.command-palette-input::placeholder { color: var(--ink-soft); }
.command-palette-close {
    background: transparent;
    border: 0;
    color: var(--ink-soft);
    cursor: pointer;
    font-size: 15px;
    line-height: 1;
    padding: 0 0.4rem;
    border-radius: 2px;
}
.command-palette-close:hover { color: var(--ink); }
.command-palette-results-wrap {
    overflow-y: auto;
    flex: 1 1 auto;
    min-height: 43px;
}
.command-palette-hint,
.command-palette-empty {
    margin: 0;
    padding: 1rem 1.25rem;
    color: var(--ink-soft);
    font-style: italic;
}
.command-palette-empty strong {
    color: var(--ink);
    font-style: normal;
}
.command-palette-section {
    padding: 0.4rem 0 0.5rem;
    border-bottom: 1px solid var(--rule);
}
.command-palette-section:last-child { border-bottom: 0; }
.command-palette-section-title {
    margin: 0;
    padding: 0.4rem 1.1rem 0.3rem;
    font-size: 7px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ink-soft);
}
.command-palette-list {
    list-style: none;
    margin: 0;
    padding: 0;
}
.command-palette-item {
    display: flex;
    align-items: baseline;
    gap: 1rem;
    width: 100%;
    background: transparent;
    border: 0;
    border-left: 2px solid transparent;
    text-align: left;
    padding: 0.55rem 1.1rem;
    font: inherit;
    font-size: 10px;
    color: var(--ink);
    cursor: pointer;
}
.command-palette-item:hover,
.command-palette-item:focus {
    background: var(--accent-soft);
    border-left-color: var(--accent);
    outline: 0;
}
.command-palette-item-name {
    font-weight: 500;
    flex: 0 1 auto;
}
.command-palette-item-meta {
    color: var(--ink-soft);
    font-size: 9px;
    flex: 1 1 auto;
    text-align: right;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.command-palette-footer {
    display: flex;
    gap: 1rem;
    padding: 0.5rem 1rem;
    border-top: 1px solid var(--rule);
    background: var(--bg);
    color: var(--ink-soft);
    font-size: 8px;
}
.command-palette-footer kbd {
    font-family: var(--mono);
    font-size: 7px;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 1px 3px;
    margin-right: 0.2rem;
    color: var(--ink);
}

/* â”€â”€ Keyboard shortcuts dialog â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Opened by '?'. Native <dialog> chrome (modal layer + Esc close)
   wrapped in the existing .staff-modal pattern for visual parity. */
.shortcuts-dialog {
    border: 1px solid var(--rule);
    border-radius: 5px;
    padding: 0;
    background: var(--panel);
    color: var(--ink);
    box-shadow: 0 8px 21px rgba(16, 42, 67, 0.18);
    max-width: 92vw;
    width: 322px;
}
.shortcuts-modal { padding: 1.25rem 1.5rem; }
.shortcuts-title {
    margin: 0 0 1rem 0;
    font-size: 15px;
    font-weight: 600;
}
.shortcuts-list {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 0.6rem 1rem;
    margin: 0;
}
.shortcuts-list dt {
    display: flex;
    gap: 0.25rem;
    align-items: center;
    white-space: nowrap;
}
.shortcuts-list dd { margin: 0; color: var(--ink); }
.shortcuts-list kbd {
    font-family: var(--mono);
    font-size: 9px;
    line-height: 1;
    background: var(--accent-soft);
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 2px 5px;
    color: var(--ink);
}
.shortcuts-foot {
    margin: 1.25rem 0 0;
    color: var(--ink-soft);
    font-size: 9px;
    font-style: italic;
}

/* Staff Details rendered into the shared #detail-modal - wraps a copy of
   the same content the full /staff/{id} page renders, but without the
   tab bar or back link. */
.staff-modal {
    position: relative;
    width: min(880px, 95vw);
    max-height: 95vh;
    display: flex;
    flex-direction: column;
}

/* Tighter spacing inside the modal so every panel fits into 92vh
   without an outer scroll on typical desktop sizes (1080p and up).
   The full-page /staff/{id} view keeps the roomier original padding. */
.staff-modal .staff-panel {
    padding: 0.55rem 0.8rem;
}
.staff-modal .panel-title {
    font-size: 10px;
    margin-bottom: 0.15rem;
}
/* Tighter title row inside the modal so the chart starts almost
   immediately under the heading - the saved space rolls into the AI
   notes panel below. */
.staff-modal .panel-title-row {
    margin-bottom: 0.15rem;
}
/* Inside the modal the staff header gets the same bordered-panel
   chrome as the chart / contracts panels below, so the title + name
   read as a real first row rather than floating loose above the
   close X. */
.staff-modal .staff-header {
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    padding: 0.55rem 0.8rem;
}
.staff-modal .staff-header-top { align-items: flex-start; }
.staff-modal .stat-strip { gap: 0.4rem; }
.staff-modal .stat { padding: 0.35rem 0.7rem; }
.staff-modal .staff-duties-panel { display: flex; flex-direction: column; }
.staff-modal .staff-duties-panel .balance-grid-container { flex: 1 1 auto; }

/* Compress chart panel heights inside the modal so the headline +
   stat strip + Weekly chart + Mini Shift Viewer + Contract Time
   Records + AI notes all stack inside one 92vh dialog. The
   full-page view keeps the inline height set in the partial. */
.staff-modal .sdv-chart-panel .chart-canvas-inner { height: 22vh !important; min-height: 170px !important; max-height: 260px !important; }
.staff-modal .sdv-mini-panel { padding-bottom: 0.4rem !important; }
.staff-modal .sdv-mini-panel .chart-canvas-inner { height: 13vh !important; min-height: 110px !important; max-height: 150px !important; min-width: 580px !important; }
/* .chart-wrap has a hard 188 px height that left a dead band under the
   shorter mini chart (canvas-inner caps at 150 px). Auto lets the wrap
   collapse to whatever the inner div is, so the legend sits right under
   the chart with no gap. Same fix benefits the taller Weekly chart -
   the wrap grows to match the 22vh inner instead of clipping it. */
.staff-modal .chart-wrap { height: auto; }

/* Contract Time Records: 5-row scroll + sticky header + table-layout
   fixed so the panel honours its container width instead of demanding
   a horizontal scrollbar when Location runs long. The freed-up
   vertical space inside the modal goes to the AI Notes panel below. */
.staff-contracts-panel .balance-grid-scroll {
    max-height: 190px;
    overflow-y: auto;
    overflow-x: hidden;
}
.staff-modal .staff-contracts-panel .balance-grid-scroll {
    max-height: 105px;
    min-height: 80px;
}
.staff-contracts-panel .balance-grid {
    table-layout: fixed;
    width: 100%;
}
.staff-contracts-panel .balance-grid th,
.staff-contracts-panel .balance-grid td {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* Column widths: Staff stretches; the four date columns share a fixed
   slice; Contracted Time + Select Type + WTD Opt Out are narrow. */
.staff-contracts-panel .balance-grid th:nth-child(1),
.staff-contracts-panel .balance-grid td:nth-child(1) { width: 22%; }
.staff-contracts-panel .balance-grid th:nth-child(2),
.staff-contracts-panel .balance-grid td:nth-child(2) { width: 11%; }
.staff-contracts-panel .balance-grid th:nth-child(3),
.staff-contracts-panel .balance-grid td:nth-child(3) { width: 10%; }
.staff-contracts-panel .balance-grid th:nth-child(4),
.staff-contracts-panel .balance-grid td:nth-child(4) { width: 10%; }
.staff-contracts-panel .balance-grid th:nth-child(5),
.staff-contracts-panel .balance-grid td:nth-child(5) { width: 10%; }
.staff-contracts-panel .balance-grid th:nth-child(6),
.staff-contracts-panel .balance-grid td:nth-child(6) { width: 10%; }
.staff-contracts-panel .balance-grid th:nth-child(7),
.staff-contracts-panel .balance-grid td:nth-child(7) { width: 11%; }
.staff-contracts-panel .balance-grid th:nth-child(8),
.staff-contracts-panel .balance-grid td:nth-child(8) { width: 9%; }
.staff-contracts-panel .balance-grid th:nth-child(9),
.staff-contracts-panel .balance-grid td:nth-child(9) { width: 7%; }
.staff-contracts-panel .balance-grid thead th {
    position: sticky;
    top: 0;
    background: var(--panel);
    z-index: 1;
}

/* Tighten the rest of the modal so the column-only layout fits. */
.staff-modal .staff-name { font-size: 1rem; margin: 0.1rem 0 0.15rem; }
.staff-modal .staff-subtitle { font-size: 9.5px; margin: 0; }
.staff-modal .stat-label { font-size: 8.5px; }
.staff-modal .stat-value { font-size: 13px; }
.staff-modal .stat-meta { font-size: 8.5px; }
.staff-modal .staff-modal-scroll { gap: 0.5rem; padding: 0.9rem 1.1rem 0.7rem; }
/* The X close button (.staff-modal-close, top 1.2rem) is absolutely
   positioned in the modal's top-right. The title H1 sits on the LEFT of
   the staff-header so they don't visually collide - just reserve enough
   right padding on .staff-header-top so the history-selector input ends
   to the left of the X (X spans roughly right 2.2rem -> 3.7rem). For
   first children that don't have a left-anchored heading (a bare <h2>
   panel-title), keep a small breathing space so the heading isn't flush
   against the panel border. */
.staff-modal .staff-modal-scroll > :first-child { padding-top: 0.3rem; }
.staff-modal .staff-modal-scroll > h1:first-child,
.staff-modal .staff-modal-scroll > h2:first-child,
.staff-modal .staff-modal-scroll > h3:first-child,
.staff-modal .staff-modal-scroll > p:first-child { margin-top: 0.3rem; padding-top: 0; }
/* Reserved right-side space inside the panel for the close X.
   X now sits at right: 1.65rem with a 24 px / 1.5 rem width, so its
   left edge is at 1.65 + 1.5 = 3.15rem from the modal right edge,
   which is 3.15 - 1.1 = 2.05rem from the panel right edge. Reserve
   2.6rem on the header-top to leave a small visible gap between the
   "weeks of history" line end and the X. */
.staff-modal .staff-header-top { padding-right: 2.6rem; }
/* AI notes panel takes the slack freed by the tightened chart
   headers + reduced layout.padding above. min-height bumped so the
   panel reads as a real surface even on a freshly-loaded staff member
   who hasn't generated commentary yet. */
.staff-modal .staff-notes-panel { padding: 0.55rem 0.9rem; min-height: 130px; }
.staff-modal .staff-notes-cell { min-height: 120px; }
.staff-modal .sdv-mini-panel .daily-legend { margin-top: 0.25rem; gap: 0.3rem; }
.staff-modal .daily-legend-pill.is-static { font-size: 8px; padding: 0.05rem 0.4rem; min-height: 18px; }
.staff-modal .sdv-chart-average { padding: 0.2rem 0.6rem; font-size: 9px; }
.staff-modal .sdv-hint { display: none; }
.staff-modal .staff-history-selector { font-size: 9.5px; }

/* Zoom controls cluster on the Weekly chart header. */
.sdv-zoom-controls,
.sdv-week-nav {
    margin-left: 0.4rem;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}
.sdv-week-nav { margin-left: auto; }
.zoom-btn {
    width: 22px;
    height: 22px;
    padding: 0;
    border: 1px solid var(--rule);
    background: var(--panel);
    color: var(--ink);
    border-radius: 2px;
    font-size: 13px;
    font-weight: 600;
    line-height: 1;
    cursor: pointer;
    transition: background 140ms ease, border-color 140ms ease;
}
.zoom-btn:hover  { background: var(--accent-soft); border-color: var(--accent); }
.zoom-btn:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.zoom-reset { display: inline-flex; align-items: center; gap: 0.2rem; font-size: 10px; }
.zoom-reset span[aria-hidden] { font-size: 12px; }
.staff-modal .zoom-reset-label { display: none; }
.staff-modal .zoom-btn { width: 19px; height: 19px; font-size: 12px; }

.sdv-hint {
    margin: 0.25rem 0 0;
    font-size: 9.5px;
    color: var(--ink-soft);
    font-style: italic;
}

.staff-modal-close {
    position: absolute;
    /* X sits INSIDE the first panel (.staff-header) which is a
       bordered card starting at modal-scroll padding-top (0.9rem)
       and ending at modal-scroll padding-right (1.1rem). To make
       the X visually equidistant from the panel's top edge and
       right edge, both offsets sit ~0.55rem from the respective
       panel border:
         top  = 0.9rem (panel top) + 0.55rem = 1.45rem
         right = 1.1rem (panel right) + 0.55rem = 1.65rem
       The history-selector ("Show the last [52] weeks of history")
       picks up extra padding-right via the .staff-header-top rule
       below so it doesn't crash into the X. */
    top: 1.45rem;
    right: 1.65rem;
    z-index: 2;
    background: var(--panel);
    border: 1px solid transparent;
    color: var(--ink-soft);
    font-size: 17px;
    line-height: 1;
    /* 24 px hit-target floor per the readability guideline. */
    width: 24px;
    height: 24px;
    border-radius: 50%;
    cursor: pointer;
}
.staff-modal-close:hover { background: var(--accent-soft); color: var(--ink); }
.staff-modal-close:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }

.staff-modal-scroll {
    overflow-y: auto;
    padding: 1.25rem 1.5rem 1rem;
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.staff-back-inline {
    color: var(--accent);
    text-decoration: none;
}
.staff-back-inline:hover { text-decoration: underline; }

.drill-modal-btn {
    background: transparent;
    border: 1px solid var(--rule);
    color: var(--ink-soft);
    border-radius: 2px;
    /* Match the canonical .row-icon-btn dimensions (KPIs grid) so
       every row-level drill icon hits the same 24 px target floor
       and reads at the same scale across grids. */
    width: 24px;
    height: 24px;
    padding: 4px;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    vertical-align: middle;
}
/* Glyph size matches the KPI .wg-open-btn canon (18 px in a 24 px
   button) so the affordance reads at the same scale across every
   grid - the bar-chart on Staff, the unit-icon on Units, the
   calendar on Leave, the shield on KPI all sit at the same weight
   on the row. The svg's intrinsic viewBox keeps the aspect right;
   only the rendered size changes here. */
.drill-modal-btn svg { width: 16px; height: 16px; fill: currentColor; }
.drill-modal-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.drill-modal-btn:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }

/* Weeks selector + chart that can pan horizontally beyond viewport width.
   The inner div carries the min-width set by JS; the outer wrap clips and
   provides the horizontal scrollbar. */
.weeks-selector {
    margin-left: auto;
    font-size: 10px;
    color: var(--ink);
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}
.weeks-selector select {
    font: inherit;
    padding: 0.4rem 0.6rem;
    min-height: 24px;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    color: var(--ink);
    cursor: pointer;
}
.weeks-selector select:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

.chart-scrollable {
    overflow-x: auto;
    overflow-y: hidden;
}
.chart-canvas-inner {
    min-width: 100%;
    height: 214px;
}

/* Duty link in the staff-details subtitle - jumps back to /balances
   Staff tab pre-filtered to that unit. */
.duty-link {
    color: var(--accent);
    text-decoration: none;
    border-bottom: 1px dotted var(--accent);
}
.duty-link:hover { color: var(--accent-hover); border-bottom-style: solid; }

/* Bi-directional sync between weekly chart bars and the duties grid.
   Click a bar â†’ scroll the matching week into view + brief yellow flash.
   Click a duty row â†’ scroll the chart container to centre that week. */
tr.duty-row { cursor: pointer; }
tr.duty-row:hover { background: var(--accent-soft); }
tr.duty-row-flash { animation: duty-row-flash 1.2s ease-out; }
@keyframes duty-row-flash {
    0%   { background: #fef9c3; }
    100% { background: transparent; }
}

/* Dedicated Open-modal column on the Staff grid. Sits to the left of the
   Staff name so the action stays in the user's scan path even when the
   row is wider than the viewport. */
/* Two icon buttons per row: drill (open SDV) + duties (open
   shifts modal). Slightly wider drill-col + a small flex
   container so they sit side-by-side without ugly stacking on
   narrow viewports. Icons bumped to be obviously clickable on
   the lower-res Trust-issue laptops the audience uses. */
.drill-col { width: 56px; text-align: center; padding: 3px 4px; }
.row-actions {
    display: inline-flex;
    gap: 4px;
    align-items: center;
    justify-content: center;
}
.row-actions .drill-modal-btn { width: 24px; height: 24px; }
/* Matches the .drill-modal-btn svg canon above (16 px in 24 px). */
.row-actions .drill-modal-btn svg { width: 16px; height: 16px; }
.duties-btn .icon { width: 14px; height: 14px; }
.duties-btn:hover { color: var(--accent); }

/* Contract Time Records grid - the primary (currently-active)
   contract gets a subtle accent-soft row tint so the "today's
   contract" is obvious without dominating the table. */
.contract-primary { background: var(--accent-soft); }
.contract-primary-cell { color: var(--rag-green-strong); font-weight: 700; text-align: center; }
.contract-location { color: var(--ink); }
.contract-location-none { color: var(--ink-soft); font-style: italic; }

/* SDV header restructure: name + a right-aligned history-window
   stepper, matching the wireframe's "Show the last 186 weeks of
   history" control. */
.staff-name-prefix {
    color: var(--ink-soft);
    font-weight: 600;
    font-size: 1.1rem;
    margin-right: 0.4rem;
}

/* < > chevrons that flank the staff name and step through the
   session staff order. Small inline circles so they sit on the
   H1 baseline; disabled when the session has no next/prev. */
.sdv-nav-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    margin: 0 0.2rem;
    border: 1px solid var(--rule);
    border-radius: 50%;
    background: var(--panel);
    color: var(--ink);
    font-size: 15px;
    line-height: 1;
    font-weight: 600;
    text-decoration: none;
    vertical-align: middle;
    transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.sdv-nav-btn:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.sdv-nav-btn:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.sdv-nav-btn.is-disabled {
    color: var(--rule);
    cursor: not-allowed;
    pointer-events: none;
    opacity: 0.5;
}
/* Modal still clears the 24 px hit-target floor (icon-only square
   floor per design-brief Typography); kept at exactly 24 px so it
   matches every other modal chrome control without overflowing the
   dense header row. */
.staff-modal .sdv-nav-btn { width: 24px; height: 24px; font-size: 14px; }
/* The single-angle-quote glyph sits ~1 px below its line-box centre
   because of its baseline metrics. Translate just the glyph so the
   chevron's optical centre matches the staff-name text beside it. */
.sdv-nav-glyph {
    display: inline-block;
    transform: translateY(-1px);
}

.staff-history-selector {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    margin-left: auto;
    font-size: 10px;
    color: var(--ink);
}
.sdv-weeks-input {
    width: 54px;
    height: 24px;
    padding: 0 0.5rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    font: inherit;
    font-size: 10px;
    text-align: center;
    background: var(--panel);
    color: var(--ink);
    transition: border-color 140ms ease, box-shadow 140ms ease;
}
.sdv-weeks-input:hover  { border-color: var(--accent); }
.sdv-weeks-input:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

/* "Average: 33.18" chip on the Weekly Contract Shift Hours chart
   header, right-aligned next to the panel title. */
.sdv-chart-average {
    margin-left: auto;
    display: inline-flex;
    align-items: baseline;
    gap: 0.45rem;
    padding: 0.35rem 0.85rem;
    background: var(--accent-soft);
    border: 1px solid var(--rule);
    border-radius: 2px;
    font-size: 10px;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}
.sdv-avg-label { color: var(--ink-soft); font-weight: 600; }
.sdv-avg-value { font-weight: 700; font-size: 11px; }

/* Mini Shift Viewer - Chart.js bar chart sitting full-modal-width
   under the Weekly Contract Shift Hours panel. The chart-canvas
   inner sets the minimum width (780 px) so 21 columns + shift-code
   badges stay legible; the chart-scrollable wrap supplies the
   horizontal scrollbar when the modal is narrower than that. */
.sdv-mini-panel .panel-title { font-size: 10px; }
.sdv-mini-panel .daily-legend { margin-top: 0.5rem; }

/* Static legend pill (no click-to-isolate on the Mini Shift Viewer
   anymore - the chart's own per-bucket dataset toggling would
   complicate the badge logic). cursor:default and the no-op hover /
   focus rules below override the clickable .daily-legend-pill base
   so the MSV legend stays flat - earlier the base rule's :hover wash
   was still firing, making the pills look interactive. */
.daily-legend-pill.is-static {
    cursor: default;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    font-size: 9px;
    padding: 0.15rem 0.5rem;
    border: 1px solid var(--rule);
    border-left: 3px solid var(--rule);
    background: var(--panel);
    color: var(--ink-soft);
    border-radius: 2px;
    transition: none;
}
.daily-legend-pill.is-static:hover,
.daily-legend-pill.is-static:focus,
.daily-legend-pill.is-static:focus-visible {
    background: var(--panel);
    color: var(--ink-soft);
    border-color: var(--rule);
    box-shadow: none;
    outline: none;
}
.daily-legend-pill.is-static .daily-legend-swatch {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
}

/* Trailing-13-rosters bar chart on the Staff grid. Rendered as
   absolutely-positioned <span> bars inside a relative container - plain
   HTML paints faster than SVG when there are hundreds of staff rows.
   Zero baseline runs through 50%; blue bars grow up (over-worked), red
   bars grow down (under-worked). Native title-attr tooltip per bar. */
/* Trail cell: bounded box + overflow clipping so the inline sparkline
   bars can never paint outside the row, even if a hover scale or an
   off-screen content-visibility placeholder pushes them. The bars
   carry a native title= attribute now, so clipping doesn't fight the
   tooltip - the browser paints title text outside the cell anyway. */
.trail-col {
    width: 67px;
    position: relative;
    overflow: hidden;
}

/* Last hours checkpoint column - the RosterEndDate of the most
   recent personnethours row for this staff member. Stale (>8 wks
   ago) rows pick up the dimmed amber treatment so a Ward Manager
   can scan for data drift at a glance. */
.checkpoint-col {
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    font-size: 9px;
    color: var(--ink-soft);
    /* Right-aligned: variable-length "d MMM yy" (e.g. "1 Jan 26" vs
       "13 Feb 26") lines up cleanly on the year edge. */
    text-align: right;
}
.checkpoint-col.checkpoint-stale {
    color: var(--rag-amber);
    font-weight: 600;
}

/* Duties-modal toolbar: search input + Type dropdown + count
   pill + Export CSV button. Sits between the title and the
   grid. All controls match the 46 px / 17 px Ward-Manager floor
   per the design brief's First principles. */
.duties-toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    align-items: center;
    margin: 0.4rem 0 0.8rem;
}

/* Manager-note column on the Staff grid. The cell is a button that
   swaps to a textarea + Save / Cancel via HTMX. Read mode shows the
   note text (truncated at 3 lines via line-clamp) or "Add note" when
   empty. */
.manager-note-col { min-width: 161px; }
.staff-note-cell { display: flex; flex-direction: column; gap: 2px; }
.staff-note-view {
    border: none;
    background: transparent;
    text-align: left;
    padding: 0.15rem 0.25rem;
    cursor: pointer;
    color: var(--ink);
    width: 100%;
    border-radius: 2px;
    transition: background 140ms ease;
}
.staff-note-view:hover { background: var(--accent-soft); }
.staff-note-text {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
    white-space: normal;
    line-height: 1.3;
    font-size: 10px;
}
.staff-note-empty { color: var(--accent); font-size: 10px; text-decoration: underline; }
.staff-note-meta { font-size: 8px; color: var(--ink-soft); padding: 0 0.25rem; display: flex; gap: 4px; align-items: center; }
.staff-note-count {
    display: inline-block;
    padding: 0 4px;
    border-radius: 7px;
    background: var(--accent-soft);
    color: var(--accent-hover);
    font-weight: 600;
}

/* Manager-notes modal. Lists every note for one staff member,
   newest first. Add form at top, individual delete per note, clear-
   all button. Opens via openDetailModal('/staff/{id}/notes/modal')
   so it lands in the shared <dialog id="detail-modal">. */
.staff-notes-modal-title { margin: 0 0 0.3rem 0; font-size: 17px; }
.staff-notes-modal-sub {
    margin: 0 0 0.8rem 0;
    color: var(--ink-soft);
    font-size: 10px;
    display: flex;
    align-items: center;
    gap: 8px;
}
.staff-notes-clear {
    font-size: 10px;
    padding: 0.2rem 0.5rem;
    border-radius: 2px;
    cursor: pointer;
    border: 1px solid #f5c6c0;
    background: #fdecea;
    color: #8a1f1f;
    transition: background 140ms ease;
}
.staff-notes-clear:hover { background: #f9d6d1; }

.staff-notes-add-form {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-bottom: 0.8rem;
    padding: 0.6rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--accent-soft);
}
.staff-notes-add-textarea {
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.3rem 0.4rem;
    font: inherit;
    font-size: 11px;
    color: var(--ink);
    resize: vertical;
    background: var(--panel);
}
.staff-notes-add-textarea:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.staff-notes-add-actions { display: flex; justify-content: flex-end; }
.staff-notes-add-save {
    font-size: 11px;
    padding: 0.25rem 0.7rem;
    min-height: 24px;
    border-radius: 2px;
    cursor: pointer;
    border: 1px solid var(--accent);
    background: var(--accent);
    color: var(--panel);
    transition: background 140ms ease;
}
.staff-notes-add-save:hover { background: var(--accent-hover); }

.staff-notes-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.staff-notes-item {
    padding: 0.5rem 0.6rem;
    border: 1px solid var(--rule);
    border-left: 2px solid var(--accent);
    border-radius: 2px;
    background: var(--panel);
}
.staff-notes-item-text {
    white-space: pre-line;
    font-size: 11px;
    color: var(--ink);
    margin-bottom: 0.3rem;
}
.staff-notes-item-meta {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 9px;
    color: var(--ink-soft);
}
.staff-notes-item-when { font-weight: 600; }
.staff-notes-item-by { flex: 1 1 auto; }
.staff-notes-item-delete {
    font-size: 9px;
    padding: 0.15rem 0.45rem;
    border-radius: 2px;
    cursor: pointer;
    border: 1px solid var(--rule);
    background: var(--panel);
    color: var(--ink-soft);
    transition: background 140ms ease, color 140ms ease;
}
.staff-notes-item-delete:hover { background: #fdecea; color: #8a1f1f; border-color: #f5c6c0; }

/* Adjustment column on the Staff grid. Same cell chrome as the
   Manager Note cell (so the eye reads the two columns as a pair),
   but the headline is a signed +/-h figure tinted R/A/G via the
   shared BalanceCss.For class. The "*" mark next to Worked /
   Variance / Balance flags rows where the figures include a lift -
   the dedicated column is the source of truth, the marks are the
   in-context reminder. */
.staff-adjustment-col { min-width: 128px; }
.staff-adjustment-cell { display: flex; flex-direction: column; gap: 2px; }
.staff-adjustment-view {
    border: none;
    background: transparent;
    text-align: left;
    padding: 0.15rem 0.25rem;
    cursor: pointer;
    color: var(--ink);
    width: 100%;
    border-radius: 2px;
    transition: background 140ms ease;
}
.staff-adjustment-view:hover { background: var(--accent-soft); }
.staff-adjustment-value { font-weight: 600; font-size: 11px; }
.staff-adjustment-meta {
    font-size: 9px;
    color: var(--ink-soft);
    display: flex;
    gap: 4px;
    align-items: center;
    margin-top: 1px;
}
.staff-adjustment-reason {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    flex: 1 1 auto;
    line-height: 1.3;
}
.adjustment-mark {
    color: var(--accent);
    font-weight: 700;
    margin-left: 2px;
    cursor: help;
}

/* Adjustments modal mirrors the Manager Notes modal chrome (re-uses
   .staff-notes-* classes for items, list, form, buttons) plus a
   small block of overrides for the +/- hours input row and the
   per-entry hours pill that prefixes each reason. */
.staff-adjustments-add-row {
    display: flex;
    gap: 8px;
    align-items: flex-end;
}
.staff-adjustments-hours-label,
.staff-adjustments-reason-label {
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-size: 10px;
    color: var(--ink-soft);
    font-weight: 600;
}
.staff-adjustments-hours-label  { flex: 0 0 96px; }
.staff-adjustments-reason-label { flex: 1 1 auto; min-width: 0; }
.staff-adjustments-hours-input,
.staff-adjustments-reason-input {
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.3rem 0.4rem;
    font: inherit;
    font-size: 11px;
    background: var(--panel);
    color: var(--ink);
}
.staff-adjustments-hours-input  { width: 96px; }
.staff-adjustments-reason-input { width: 100%; }
.staff-adjustments-hours-input:focus,
.staff-adjustments-reason-input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.staff-adjustments-add-save { white-space: nowrap; }
.staff-notes-help {
    font-size: 10px;
    color: var(--ink-soft);
    margin: 0 0 0.7rem 0;
    line-height: 1.4;
}
.staff-notes-help-sign { margin-top: -0.3rem; }
.staff-adjustments-item-row {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 0.3rem;
}
.staff-adjustments-item-hours {
    font-weight: 600;
    font-size: 11px;
    white-space: nowrap;
    min-width: 48px;
}

/* Duties modal Shift column - reduced ~25% from its natural width so
   the wider Booking ref / Reason / Additional reason columns get more
   room. Long shift names ("Long Day Early") get truncated with an
   ellipsis; the cell carries the full name via title= for hover. */
.duties-grid .duties-shift-col,
.duties-grid td[data-col="shift"] {
    max-width: 88px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Duties modal: flex column inside the modal viewport. The outer
   .staff-modal-scroll suppresses its own vertical overflow (the inner
   grid wrapper owns it), and the duties-grid-scroll takes whatever
   vertical space remains - so its horizontal scrollbar always sits at
   the visible bottom of the modal, not at the bottom of a possibly-
   tall table that's pushed below the fold. */
.duties-modal .staff-modal-scroll { overflow-y: hidden; flex: 1 1 auto; min-height: 0; }
.duties-modal .balance-grid-container {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
}
.duties-grid-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-x: scroll;
    overflow-y: auto;
}
/* Group-header band inside the duties modal. Sticky below the thead
   so the group label stays visible while the user scrolls through a
   long group. Matches the .balance-grid tr.group-header treatment -
   the inner .group-toggle button owns the padding so the click target
   fills the row (G29). */
.duties-grid tr.duties-group-header td {
    position: sticky;
    top: 24px;
    z-index: 2;
    background: var(--accent-soft);
    border-top: 1px solid var(--accent);
    border-bottom: 1px solid var(--accent);
    padding: 0;
}
.duties-grid tr.duties-group-header .group-toggle-count::before { content: 'Â· '; color: var(--ink-soft); }
/* Inline-flex pair of bulk toggles in the duties toolbar - hidden by
   default (no grouping); shown by JS when a group dimension is picked. */
.duties-group-bulk { display: inline-flex; align-items: center; gap: 0.25rem; }
.duties-group-bulk[hidden] { display: none; }
.duties-search,
.duties-type-filter {
    height: 31px;
    padding: 0 0.85rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    color: var(--ink);
    font: inherit;
    font-size: 11px;
    transition: border-color 140ms ease, box-shadow 140ms ease;
}
.duties-search { min-width: 188px; flex: 1 1 188px; }
.duties-type-filter { min-width: 107px; cursor: pointer; }
.duties-group-select { min-width: 142px; }
.duties-search:hover,
.duties-type-filter:hover { border-color: var(--accent); }
.duties-search:focus-visible,
.duties-type-filter:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.duties-date-range {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 9px;
    color: var(--ink-soft);
}
.duties-date-label { font-weight: 600; color: var(--ink); }
.duties-date-input {
    height: 31px;
    padding: 0 0.6rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    color: var(--ink);
    font: inherit;
    font-size: 10px;
    cursor: pointer;
    transition: border-color 140ms ease, box-shadow 140ms ease;
}
.duties-date-input:hover { border-color: var(--accent); }
.duties-date-input:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.duties-count {
    font-size: 9px;
    color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
    margin-left: 0.2rem;
}
/* Lozenge - rounded pill - matches the other export buttons across
   the app (Status bar export on the staff/units/divisions grids).
   The shifts modal previously had this as a square button which
   broke the visual taxonomy. */
.duties-export-btn {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    height: 31px;
    padding: 0 1rem;
    border: 1px solid var(--rule);
    border-radius: 999px;
    background: var(--panel);
    color: var(--accent);
    cursor: pointer;
    font: inherit;
    font-size: 10px;
    font-weight: 600;
    transition: background 140ms ease, border-color 140ms ease, box-shadow 140ms ease;
}
.duties-export-btn:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
}
.duties-export-btn:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.duties-export-btn .icon { width: 12px; height: 12px; }

/* Unit-modal header actions row - groups the sentinel-report
   icon buttons + the sign-off lozenge so they sit on one line
   to the right of the unit identity. Square ghost icon buttons
   match .unit-picker-action chrome for consistency. */
.unit-detail-actions {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
}
.unit-action-btn {
    flex: 0 0 auto;
    width: 25px;
    height: 25px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    color: var(--accent);
    cursor: pointer;
    text-decoration: none;
    transition: background 140ms ease, border-color 140ms ease, box-shadow 140ms ease;
}
.unit-action-btn:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
}
.unit-action-btn:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.unit-action-btn .icon { width: 13px; height: 13px; }
.trail-bars {
    position: relative;
    display: inline-block;
    width: 60px;
    height: 15px;
    line-height: 0;
    vertical-align: middle;
}
.trail-bars::after {
    content: '';
    position: absolute;
    left: 0; right: 0; top: 50%;
    height: 1px;
    background: var(--rule);
}
.trail-bar {
    position: absolute;
    display: block;
    cursor: pointer;
}
/* Sparkline trail bars - share the accent (positive = blue/owing trust)
   and the softened red (negative = trust owes). Kept distinct from the
   activity-palette tokens because trail bars convey balance sign, not
   duty type. */
.trail-bar-pos {
    bottom: 50%;
    background: var(--accent);
}
.trail-bar-neg {
    top: 50%;
    background: var(--rag-red-strong);
}
.trail-bar:hover { opacity: 0.7; }

/* Daily detail strip - one absolutely-positioned cell per day, each
   cell a vertical stack of NERG-bucket segments coloured per the
   activity palette. Zero baseline at the bottom; bars grow up. */
/* â”€â”€ Group-by selector + grouped Staff grid rows â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Toolbar control toggles server-side grouping; the grid then emits
   sticky group-header rows + per-group subtotal rows interspersed
   with the staff rows. Collapse is client-side only (per visit). */
.group-by-label {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    height: 31px;
    padding: 0 0.85rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    font-size: 11px;
    color: var(--ink-soft);
}
.group-by-text { font-weight: 500; }
.group-by-select {
    padding: 0.3rem 0.5rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    font: inherit;
    color: var(--ink);
    background: var(--panel);
}
.group-by-select:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

/* Group-by select inside the Staff grid's .status-bar. Matches the
   46 px chrome of the toolbar pickers (Units, Grades, Bands, As-of)
   exactly - same border, padding, focus ring - per the "same job,
   same component" rule in design-brief.md First principles. Options
   carry the "Group by" hint in their label text so no separate label
   is needed alongside ("View as flat list" / "Grouped by Division"). */
.group-by-status {
    height: 24px;
    width: auto;
    min-width: 147px;
    flex: 0 0 auto;
    font-size: 9px;
    padding: 0 0.7rem;
    border: 1px solid var(--rule);
    /* 6 px to match the toolbar-action button family. */
    border-radius: 2px;
    background-color: var(--panel);
    color: var(--ink);
    cursor: pointer;
    transition: border-color 140ms ease, box-shadow 140ms ease;
}
.group-by-status:hover {
    border-color: var(--accent);
}
.group-by-status:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

/* Expand-all / collapse-all icon buttons next to the group-by select.
   Same chrome as .unit-picker-action so the toolbar reads as one row
   of consistent square icon buttons. */
.group-bulk-toggle {
    /* Match the toolbar-action button family - 24 px square icon button
       with 4 px radius so the action row reads as one shape. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    padding: 0;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    color: var(--ink);
    cursor: pointer;
    transition: background 140ms ease, border-color 140ms ease, box-shadow 140ms ease;
}
.group-bulk-toggle:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
}
.group-bulk-toggle:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.group-bulk-toggle .icon { width: 12px; height: 12px; }

/* Sticky group header - sits below the sticky thead. `top` is the
   approximate thead height; if a header overlaps a cell, tune here. */
.balance-grid tr.group-header td {
    position: sticky;
    top: 24px;
    z-index: 2;
    background: var(--accent-soft);
    border-top: 1px solid var(--accent);
    border-bottom: 1px solid var(--accent);
    padding: 0;
}
.group-toggle {
    width: 100%;
    text-align: left;
    background: transparent;
    border: 0;
    cursor: pointer;
    padding: 0 0.85rem;
    /* Match the toolbar input floor (46 px) so the +/- group icons
       align with the grid params above them. */
    min-height: 31px;
    font: inherit;
    font-weight: 600;
    color: var(--ink);
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
}
.group-toggle:hover { background: rgba(111, 149, 187, 0.18); }
.group-toggle:focus-visible {
    outline: none;
    box-shadow: inset 0 0 0 2px var(--accent-soft);
}
.group-toggle-arrow {
    display: inline-block;
    width: 1ch;
    font-size: var(--ui-arrow-size);
    font-weight: var(--ui-arrow-weight);
    line-height: 1;
    color: var(--accent-hover);
    transition: transform 120ms ease;
}
.group-toggle-label { color: var(--ink); }
.group-toggle-count {
    margin-left: auto;
    color: var(--ink-soft);
    font-weight: 500;
    font-size: 9px;
}

.balance-grid tr.group-subtotal td {
    background: var(--bg);
    font-weight: 600;
    border-top: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule);
    padding: 0.3rem 0.85rem;
    font-style: italic;
    color: var(--ink);
}
.balance-grid tr.group-subtotal td:first-child {
    font-style: normal;
    color: var(--ink-soft);
}
@media (prefers-reduced-motion: reduce) {
    .group-toggle-arrow { transition: none; }
}

/* â”€â”€ Top movers card row (above the Staff grid) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Slim horizontal strip of 4-6 cards naming the staff with the
   biggest balance change roster-on-roster, scored by |delta| Ã— WTE.
   Each card is a button that opens Staff Details. Cards colour by
   direction-vs-position (deteriorating = pastel red, improving =
   pastel green, neutral = grey) so a manager can scan the strip and
   immediately see who is moving away from balance vs back toward it. */
/* Top-movers strips sit AT THE TOP of each grid with no chrome -
   borderless, transparent background. The summary row uses the
   same lozenge component as the Activity bar (.activity-lozenge)
   so the two disclosure surfaces read as one family. */
.top-movers {
    background: transparent;
    border: none;
    border-radius: 0;
    margin: 0;
}
.top-movers-summary {
    list-style: none;
    cursor: pointer;
    padding: 0.45rem 0.25rem;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    color: var(--ink-soft);
    user-select: none;
    font-size: 9px;
}
.top-movers-summary::-webkit-details-marker { display: none; }
.top-movers[open] .top-movers-summary .activity-lozenge-arrow { transform: rotate(180deg); }
.top-movers-caret {
    display: inline-block;
    color: var(--accent-hover);
    transition: transform 120ms ease;
    width: 1ch;
    font-size: var(--ui-arrow-size);
    font-weight: var(--ui-arrow-weight);
    line-height: 1;
}
.top-movers:not([open]) .top-movers-caret { transform: rotate(-90deg); }
.top-movers-title { color: var(--ink); }
.top-movers-count {
    display: inline-block;
    padding: 0.05rem 0.45rem;
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    border-radius: 7px;
    font-size: 7px;
    font-weight: 600;
    color: var(--ink);
}

.top-movers-scroller {
    position: relative;
    padding: 0.6rem 0;
}
.top-movers-row {
    display: flex;
    flex-wrap: nowrap;
    gap: 0.55rem;
    overflow-x: auto;
    padding: 0 2.4rem 0.4rem;
    scroll-behavior: smooth;
    scroll-snap-type: x proximity;
}
.top-movers-row > .top-mover-card { scroll-snap-align: start; }
.top-movers-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 27px;
    height: 48px;
    border: 1px solid var(--rule);
    background: var(--panel);
    color: var(--ink-soft);
    font-size: 21px;
    font-weight: var(--ui-arrow-weight);
    line-height: 1;
    border-radius: 2px;
    cursor: pointer;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.top-movers-nav:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--ink);
}
.top-movers-nav:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.top-movers-prev { left: 0.5rem; }
.top-movers-next { right: 0.5rem; }
@media (prefers-reduced-motion: reduce) {
    .top-movers-caret, .top-movers-nav { transition: none; }
    .top-movers-row { scroll-behavior: auto; }
}
.top-mover-card {
    flex: 0 0 auto;
    min-width: 117px;
    max-width: 147px;
    text-align: left;
    padding: 0.55rem 0.75rem 0.65rem;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-left: 2px solid var(--rule);
    border-radius: 2px;
    cursor: pointer;
    transition: background 120ms ease, border-color 120ms ease, transform 120ms ease;
    font: inherit;
    color: inherit;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}
.top-mover-card:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
    border-left-color: var(--accent);
    transform: translateY(-1px);
}
.top-mover-card:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.top-mover-card.top-mover-bad  { border-left-color: var(--rag-red-strong); }
.top-mover-card.top-mover-good { border-left-color: var(--rag-green-strong); }
.top-mover-name {
    font-weight: 600;
    font-size: 9px;
    color: var(--ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.top-mover-meta {
    font-size: 9px;
    color: var(--ink-soft);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    margin-bottom: 0.15rem;
}
.top-mover-delta {
    font-family: var(--mono);
    font-variant-numeric: tabular-nums;
    font-size: 13px;
    font-weight: 600;
    color: var(--ink);
    line-height: 1.1;
    display: flex;
    align-items: baseline;
    gap: 0.15rem;
}
.top-mover-bad .top-mover-delta  { color: var(--rag-red); }
.top-mover-good .top-mover-delta { color: var(--rag-green); }
.top-mover-arrow { font-size: 15px; font-weight: var(--ui-arrow-weight); line-height: 1; }
.top-mover-unit  { font-size: 9px; color: var(--ink-soft); font-weight: 400; margin-left: 1px; }
.top-mover-prevcurrent {
    font-family: var(--mono);
    font-variant-numeric: tabular-nums;
    font-size: 9px;
    color: var(--ink-soft);
}
@media (prefers-reduced-motion: reduce) {
    .top-mover-card { transition: none; }
    .top-mover-card:hover { transform: none; }
}

/* When top-movers is rendered above the grid container, drop the
   container's top border-radius so the strip and the grid read as
   one stacked surface. */
.top-movers + .status-bar + .balance-grid-container { border-radius: 0 0 4px 4px; }

/* â”€â”€ Daily detail strip on Staff Details â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Horizontal day cells with stacked NERG-bucket segments. Pastel
   palette matches CSS --duty-* tokens. Day count is controlled by
   the days-back selector (90 / 180 / 365); the strip's height is
   fixed but cells dynamically scale to fit. */
.daily-axis {
    position: relative;
    width: 100%;
    height: 9px;
    margin-bottom: 1px;
}
.daily-axis-tick {
    position: absolute;
    top: 0;
    font-size: 7px;
    color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
    transform: translateX(-1px);
    white-space: nowrap;
}
.daily-axis-tick::before {
    content: '';
    position: absolute;
    left: 0;
    top: 100%;
    width: 1px;
    height: 3px;
    background: var(--rule);
}
.daily-strip {
    position: relative;
    width: 100%;
    height: 134px;
    background: linear-gradient(to right,
        transparent 0,
        transparent calc(100% / 13 - 1px),
        var(--rule) calc(100% / 13 - 1px),
        var(--rule) calc(100% / 13),
        transparent calc(100% / 13));
    background-size: calc(100% / 13) 100%;
    background-color: var(--bg);
    border: 1px solid var(--rule);
    border-radius: 2px;
}
.daily-cell {
    position: absolute;
    bottom: 0;
    top: 0;
    cursor: default;
}
/* Empty-day cell - the shape is visible (so the user knows there's
   a day there) but with no segment bars. Subtle striped background
   distinguishes "no data" from "0 hours". */
.daily-cell-empty {
    background: repeating-linear-gradient(45deg,
        transparent 0,
        transparent 2px,
        color-mix(in srgb, var(--ink-soft) 6%, transparent) 2px,
        color-mix(in srgb, var(--ink-soft) 6%, transparent) 3px);
}
/* Today marker - vertical accent line so the user can see "where
   we are" against the historical strip. Renders on top of any
   segments via a high z-index. */
.daily-cell-today {
    box-shadow: inset 1px 0 0 var(--accent), inset -1px 0 0 var(--accent);
    z-index: 2;
}
.daily-cell-today::before {
    content: "";
    position: absolute;
    left: 50%;
    top: -3px;
    width: 4px;
    height: 4px;
    background: var(--accent);
    border-radius: 50%;
    transform: translateX(-50%);
}
.daily-cell-anchor {
    animation: cell-anchor-pulse 1400ms ease-out 1;
}
@keyframes cell-anchor-pulse {
    0%   { box-shadow: inset 1px 0 0 var(--accent), inset -1px 0 0 var(--accent), 0 0 0 0 var(--accent-soft); }
    60%  { box-shadow: inset 1px 0 0 var(--accent), inset -1px 0 0 var(--accent), 0 0 0 5px transparent; }
    100% { box-shadow: inset 1px 0 0 var(--accent), inset -1px 0 0 var(--accent); }
}
@media (prefers-reduced-motion: reduce) {
    .daily-cell-anchor { animation: none; }
}
.daily-seg {
    position: absolute;
    left: 0;
    right: 0;
    display: block;
    transition: opacity 140ms ease;
}
/* Click-to-isolate on the legend: dim every segment whose bucket
   isn't the chosen one. .is-isolating on the strip is set when a
   legend pill is pressed; .is-dim is set per non-matching segment. */
.daily-strip.is-isolating .daily-seg.is-dim { opacity: 0.12; }

.daily-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-top: 0.7rem;
    align-items: center;
}
/* Mirrors .heatmap-bucket so the SDV daily-strip legend reads as
   the same control as the Activity heatmap filter buttons. Same
   shape, size, font; left-border tint carries the taxonomy colour
   via the inline `border-left-color` style the JS sets per bucket;
   active state fills the button with the same colour. */
.daily-legend-pill {
    font: inherit;
    font-size: 9px;
    padding: 0.15rem 0.5rem;
    min-height: 21px;
    border: 1px solid var(--rule);
    border-left: 3px solid var(--rule);
    background: var(--panel);
    color: var(--ink-soft);
    border-radius: 2px;
    cursor: pointer;
    transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.daily-legend-pill:hover {
    background: var(--accent-soft);
    color: var(--ink);
    border-color: var(--accent);
}
.daily-legend-pill:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.daily-legend-pill.is-isolated {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--ink);
    font-weight: 600;
}
/* Legend swatch is no longer needed - the left border carries the
   taxonomy colour. Hidden visually but kept in the DOM so existing
   selectors keep working. */
.daily-legend-swatch { display: none; }
.daily-legend-reset {
    margin-left: 0.25rem;
    font-size: 8px;
}
@media (prefers-reduced-motion: reduce) {
    .daily-seg, .daily-legend-pill { transition: none; }
}
/* .daily-cell + .heatmap-cell tooltips render via the JS-driven
   .barnacles-tip singleton (see _Layout.cshtml). The earlier
   pseudo-element arrow rule was removed when the tooltip moved
   off pseudo-elements; the JS bubble paints its own arrow. */

/* â”€â”€ Activity heatmap â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   GitHub-style calendar grid on the Unit Details modal: 7 rows
   (days Monâ†’Sun) Ã— 13 columns (weeks oldestâ†’newest). Each cell is
   coloured on a 5-tier ramp that blends from --rule (no activity)
   toward the softened brand --accent (peak day). Future cells get
   a striped treatment so they don't read as "no activity". */
.heatmap-wrap {
    padding: 0.25rem 0 0;
    overflow-x: auto;
}

/* Bucket filter buttons that sit in the panel title row above the
   heatmap. Each carries its taxonomy colour (the same --duty-*
   palette the .duty-tag-* lozenges use) so the row of buckets reads
   as a legend - users see the colour and instantly map it to the
   tag they see elsewhere on the staff drill / shifts list. The
   active button is solid-filled with its colour; inactive buttons
   carry a thin left-border tint as a visual hint. */
.heatmap-bucket-filter {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 9px;
    margin-left: auto;
    align-items: flex-end;
}
/* Each bucket-group is a labelled cluster - the data-group-label
   shows up as an eyebrow above the buttons via a ::before pseudo
   so users see the taxonomy structure rather than a flat list of
   nine buttons. */
.heatmap-bucket-group {
    display: inline-flex;
    gap: 3px;
    flex-wrap: wrap;
    position: relative;
    padding-top: 12px;
}
.heatmap-bucket-group::before {
    content: attr(data-group-label);
    position: absolute;
    top: 0;
    left: 0;
    font-size: 8px;
    color: var(--ink-soft);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-weight: 600;
}
.heatmap-bucket-group + .heatmap-bucket-group {
    border-left: 1px solid var(--rule);
    padding-left: 9px;
    margin-left: 0;
}
.heatmap-bucket {
    font: inherit;
    font-size: 9px;
    padding: 0.15rem 0.5rem;
    min-height: 21px;
    border: 1px solid var(--rule);
    background: var(--panel);
    color: var(--ink-soft);
    border-radius: 2px;
    cursor: pointer;
    transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.heatmap-bucket:hover { background: var(--accent-soft); color: var(--ink); border-color: var(--accent); }

/* Per-bucket inactive accent: 3 px left border in the taxonomy
   colour so the button reads as "this bucket has this colour"
   without overpowering the row. */
.bucket-local       { border-left: 3px solid var(--duty-local); }
.bucket-bank        { border-left: 3px solid var(--duty-bank); }
.bucket-agency      { border-left: 3px solid var(--duty-agency); }
.bucket-sickness    { border-left: 3px solid var(--duty-sickness); }
.bucket-annualleave { border-left: 3px solid var(--duty-annual); }
.bucket-studyleave  { border-left: 3px solid var(--duty-study); }
.bucket-parenting   { border-left: 3px solid var(--duty-parenting); }
.bucket-otherleave  { border-left: 3px solid var(--duty-other); }
.bucket-workingday  { border-left: 3px solid var(--duty-workingday); }
/* Day / Night - time-of-day axis, not part of the duty taxonomy so
   uses neutral warm / cool tints rather than --duty-* variables. */
.bucket-day         { border-left: 3px solid #f59e0b; }
.bucket-night       { border-left: 3px solid #4338ca; }

/* Active button: solid-filled with its taxonomy colour, ink text
   for the pale tones (annual / working day) and panel text for the
   darker ones. */
.heatmap-bucket.is-active {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--panel);
    font-weight: 600;
}
.bucket-local.is-active       { background: var(--duty-local);      border-color: var(--duty-local); color: var(--panel); }
.bucket-bank.is-active        { background: var(--duty-bank);       border-color: var(--duty-bank); color: var(--panel); }
.bucket-agency.is-active      { background: var(--duty-agency);     border-color: var(--duty-agency); color: var(--panel); }
.bucket-sickness.is-active    { background: var(--duty-sickness);   border-color: var(--duty-sickness); color: var(--panel); }
.bucket-annualleave.is-active { background: var(--duty-annual);     border-color: var(--duty-annual); color: var(--ink); }
.bucket-studyleave.is-active  { background: var(--duty-study);      border-color: var(--duty-study); color: var(--panel); }
.bucket-parenting.is-active   { background: var(--duty-parenting);  border-color: var(--duty-parenting); color: var(--panel); }
.bucket-otherleave.is-active  { background: var(--duty-other);      border-color: var(--duty-other); color: var(--panel); }
.bucket-workingday.is-active  { background: var(--duty-workingday); border-color: var(--duty-workingday); color: var(--ink); }
.bucket-day.is-active         { background: #f59e0b; border-color: #f59e0b; color: var(--ink); }
.bucket-night.is-active       { background: #4338ca; border-color: #4338ca; color: var(--panel); }
.heatmap-bucket.is-active:hover { filter: brightness(0.92); }
.heatmap-grid {
    display: grid;
    grid-auto-rows: 11px;
    gap: 2px;
    align-items: center;
    width: max-content;
}
.heatmap-month-label {
    font-size: 9px;
    color: var(--ink-soft);
    height: 11px;
    line-height: 11px;
    text-align: left;
    font-variant-numeric: tabular-nums;
}
.heatmap-day-label {
    font-size: 9px;
    color: var(--ink-soft);
    text-align: right;
    padding-right: 0.35rem;
    line-height: 11px;
}
.heatmap-cell {
    width: 11px;
    height: 11px;
    border-radius: 2px;
    background: #eef2f6;
    position: relative;
    transition: outline-color 100ms ease;
}
.heatmap-cell[data-tip]:hover {
    outline: 1px solid var(--ink-soft);
    outline-offset: 1px;
    cursor: help;
}
/* 5-tier pastel ramp toward --accent (#6f95bb). Tier-0 is the
   "no activity" base; tiers 1-4 climb in 25-percentile bands of the
   window's busiest day. */
.heatmap-tier-0 { background: #eef2f6; }
.heatmap-tier-1 { background: #d8e4ef; }
.heatmap-tier-2 { background: #b3c9de; }
.heatmap-tier-3 { background: #8fadce; }
.heatmap-tier-4 { background: #6f95bb; }
/* Future days inside the window - present in the grid for alignment
   but visually opted out so the eye doesn't read "no activity". */
.heatmap-future {
    background: repeating-linear-gradient(
        45deg,
        #f5f8fb 0, #f5f8fb 3px,
        #eef2f6 3px, #eef2f6 5px
    );
}
.heatmap-legend {
    margin-top: 0.75rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 1rem;
    font-size: 9px;
    color: var(--ink-soft);
    flex-wrap: wrap;
}
.heatmap-legend-meta { font-variant-numeric: tabular-nums; }
.heatmap-legend-scale { display: inline-flex; align-items: center; gap: 0.25rem; }
.heatmap-legend-cell {
    width: 8px;
    height: 8px;
    border-radius: 1px;
    display: inline-block;
}
.heatmap-loading, .heatmap-error {
    color: var(--ink-soft);
    font-size: 9px;
    padding: 0.5rem 0;
}
.heatmap-error { color: var(--error-text); }
.daily-cell:hover { outline: 1px solid var(--accent); }

/* Flash highlight when the user clicks a bar in the weekly chart -
   the seven days inside that ISO week pulse so the eye can pick them
   out of the surrounding strip. */
.daily-cell.daily-cell-flash {
    outline: 1px solid var(--accent);
    background: color-mix(in srgb, var(--accent-soft) 60%, transparent);
    animation: daily-cell-pulse 1.4s ease-out;
}
@keyframes daily-cell-pulse {
    0%   { box-shadow: 0 0 0 0 var(--accent-soft); }
    40%  { box-shadow: 0 0 0 5px transparent; }
    100% { box-shadow: 0 0 0 0 transparent; }
}

/* Container must allow the tooltip's data-bar context to escape on the
   y-axis even though the bubble itself is now a body-attached element. */
.trail-bars { overflow: visible; }

/* â”€â”€ Structural org/unit tree (Units > Optima tree view) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Hierarchical browse surface: nested <details> per level, leaves are
   .ward-row rows that pivot to Staff via HTMX. Lifted from
   _UnitsTreeView.cshtml's inline <style> block per the Section 0 rule
   that all CSS lives in site.css. See W4 / Table 17. */
.org-tree {
    border: 1px solid var(--rule);
    background: var(--panel);
    border-radius: 2px;
    overflow: hidden;
}
.org-tree details { border-top: 1px solid var(--rule); }
.org-tree details:first-of-type { border-top: 0; }
.org-tree summary {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.4rem 0.6rem;
    cursor: pointer;
    list-style: none;
    font-size: 11px;
    transition: background 140ms ease;
}
.org-tree summary:hover { background: var(--accent-soft); }
.org-tree summary::-webkit-details-marker { display: none; }
.org-tree summary .chev {
    display: inline-block;
    width: 9px;
    text-align: center;
    transition: transform 140ms ease;
}
.org-tree details[open] > summary .chev { transform: rotate(90deg); }
.org-tree .node-name { font-weight: 500; }
.org-tree .rollup {
    margin-left: auto;
    display: flex;
    gap: 0.8rem;
    color: var(--ink-soft);
    font-size: 10px;
}
.org-tree .rollup .num { font-variant-numeric: tabular-nums; }
.org-tree .child-list {
    padding: 0.15rem 0 0.4rem 1.4rem;
    border-top: 1px solid var(--rule);
    background: var(--bg);
}
.org-tree .ward-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.3rem 0.6rem 0.3rem 1.4rem;
    font-size: 11px;
    border-top: 1px solid var(--rule);
    transition: background 140ms ease;
}
.org-tree .ward-row:first-child { border-top: 0; }
.org-tree .ward-row:hover { background: var(--accent-soft); }
.org-tree .ward-link {
    color: var(--accent);
    cursor: pointer;
    font-size: 11px;
    text-decoration: none;
}
.org-tree .ward-link:hover { text-decoration: underline; }

/* â”€â”€ Global custom tooltip â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   A singleton .barnacles-tip element appended to <body> (or to the
   nearest open <dialog> when the target is inside a modal) is shown
   on hover/focus by the JS in _Layout.cshtml. The JS positions it
   ABOVE the target by default, flips it BELOW when there isn't room
   above, and clamps the horizontal position to the viewport so it
   never extends past the edge. Replaces the previous CSS-pseudo
   approach which had hard-to-fix edge-clip and modal-stacking
   problems. Native `title=` attributes are promoted to `data-tip=`
   by barnaclesUpgradeTooltips() so existing markup just works. */
.barnacles-tip {
    position: fixed;
    top: -1000px;
    left: -1000px;
    background: var(--panel);
    color: var(--ink);
    border: 1px solid var(--accent);
    padding: 0.55rem 0.95rem;
    border-radius: 2px;
    font-size: 11px;
    font-weight: 500;
    line-height: 1.4;
    max-width: 320px;
    width: max-content;
    text-align: center;
    box-shadow: 0 4px 12px rgba(16, 42, 67, 0.12);
    pointer-events: none;
    z-index: 2147483646;
    white-space: pre-wrap;
    opacity: 0;
    transition: opacity 140ms ease;
}
.barnacles-tip.is-visible { opacity: 1; }
.barnacles-tip::after {
    content: '';
    position: absolute;
    bottom: -5px;
    left: var(--bt-arrow-x, 50%);
    transform: translateX(-50%);
    width: 0;
    height: 0;
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-top: 5px solid var(--accent);
}
.barnacles-tip.is-below::after {
    bottom: auto;
    top: -5px;
    border-top: 0;
    border-bottom: 5px solid var(--accent);
}
@media (prefers-reduced-motion: reduce) {
    .barnacles-tip { transition: none; }
}

/* Unit-modal balance distribution - one bar per staff member, zero
   baseline, blue above (positive AllHours = Owes Trust), red below
   (negative = Trust Owes). Reuses .trail-bars + .trail-bar geometry
   but spans the full panel width. */
.unit-balance-bars {
    display: block;
    width: 100%;
    height: 54px;
}

.spark-empty { color: var(--faint); font-size: 9px; }

.spark-link { display: inline-block; line-height: 0; cursor: pointer; }
.spark-link:hover .spark { filter: drop-shadow(0 0 1px var(--accent)); }

/* Balance-history page prev/next staff buttons */
.history-nav {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    white-space: nowrap;
}
.link-disabled { color: var(--faint); cursor: not-allowed; }



/* â”€â”€ Button taxonomy â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Three canonical button looks across the app - keep these consistent
   when adding new actions.

   1. **Primary** - filled accent on a coloured background, white text,
      3px radius. For main CTAs (Sign in, Ask a question, Sign off the
      year). Classes: .primary-button, .ask-button (which inherits).
   2. **Ghost / outline** - transparent background, 1px var(--accent)
      border, accent text, fills with accent on hover. 3px radius.
      For secondary actions and table-row affordances. Classes:
      .generate-notes, .generate-all-btn.
   3. **Quiet ghost** - transparent background, 1px var(--rule) border,
      var(--ink-soft) text, lights up accent on hover. 3px radius. For
      "All" clear pills, modal close Ã— buttons, row icon buttons.
      Classes: .filter-clear, .drill-modal-btn, .row-icon-btn,
      .staff-modal-close.

   Link-style buttons (.drill-link, .link-action) carry no background
   or border - they look like text. .lozenge is a round status pill,
   separate.

   Every button-like element shares the focus ring rule below. */

/* Unified focus ring on every interactive button-like element. */
button:focus-visible,
a.drill-link:focus-visible,
.drill-link:focus-visible,
.link-action:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--accent-soft);
}

/* â”€â”€ Animations â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Subtle micro-interactions: easing on hover/focus colour changes, a
   short fade+scale on modal open, a fade-in on HTMX content swaps, and
   a soft "press" on primary buttons. All respect prefers-reduced-motion. */

button,
.drill-link,
.drill-modal-btn,
.filter-input,
.filter-clear,
.lozenge,
.tab,
.header-nav-link,
.duty-link,
.staff-back,
.staff-back-inline,
.sort-link,
.link-action,
.primary-button,
.nl-example,
.spark-link,
.trail-bar,
input[type="search"],
input[type="date"],
input[type="email"],
input[type="number"],
textarea,
select,
summary {
    transition:
        background-color 140ms ease,
        color 140ms ease,
        border-color 140ms ease,
        box-shadow 140ms ease,
        transform 90ms ease,
        opacity 140ms ease;
}

.primary-button:active { transform: translateY(1px); }
.lozenge-button:active { transform: scale(0.94); }
.drill-modal-btn:active { transform: scale(0.94); }

/* Smooth row hover on every grid */
.balance-grid tbody tr {
    /* No transition on hover. Even 60 ms read as laggy on a dense
       grid - the cursor sweeps multiple rows faster than each
       animation finishes and the eye sees the colour "catching up"
       behind the pointer. Instant paint matches what the user
       expects ("the row I'm on is highlighted"). */
}

/* Modal: fade + slight scale on open. Dialogs are appended to the top
   layer with [open] when showModal() runs, so an animation on the
   [open] selector fires once on entrance. */
dialog#detail-modal[open],
dialog#ai-dialog[open] {
    animation: modal-in 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

@keyframes modal-in {
    from { opacity: 0; transform: scale(0.96) translateY(-3px); }
    to   { opacity: 1; transform: scale(1) translateY(0); }
}

dialog::backdrop {
    animation: backdrop-in 180ms ease;
}

@keyframes backdrop-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* HTMX content-swap fade. The grid container is swapped via outerHTML
   on every filter/sort change - animating the new node gives a soft
   fade-in without any per-element wiring. Also covers the modal partial
   contents and the tab bar. */
#balance-grid,
#tab-bar,
#units-filter-wrap,
#unit-results {
    animation: swap-in 200ms ease;
}

@keyframes swap-in {
    from { opacity: 0.35; }
    to   { opacity: 1; }
}

/* Sparkline bar hover (already opacity-based - slow it down a hair) */
.trail-bar:hover { transform: scaleY(1.08); transform-origin: center; }

/* Accessibility: when the user has asked for reduced motion, drop the
   animations to a single millisecond - keeps the final visual state but
   skips the journey. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* Version stamp at the bottom of the login screen. Built at compile
   time by ComputeGitVersion in Barnacles.Api.csproj - shows the SemVer,
   short commit SHA, and commit date so support can validate releases. */
.login-version {
    margin: 1rem auto 0;
    text-align: center;
    color: var(--ink-soft);
    font-size: 9px;
    letter-spacing: 0.01em;
}
.login-version-num {
    font-weight: 600;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}
.login-version-sha {
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
    font-size: 9px;
    color: var(--ink-soft);
}
.login-version-date {
    font-variant-numeric: tabular-nums;
    color: var(--ink-soft);
}
.login-version-sep {
    margin: 0 0.4em;
    color: var(--faint);
}

/* Account-menu inline version chip - small, low-contrast, sits to the
   right of the "What's new" label so the menu doubles as a build-stamp. */
.user-menu-version {
    margin-left: auto;
    padding: 1px 5px;
    border: 1px solid var(--rule);
    border-radius: 669px;
    font-size: 9px;
    font-variant-numeric: tabular-nums;
    color: var(--ink-soft);
    background: var(--panel);
}

/* Release-notes modal - list of every recorded release with a version
   chip, date, short title, and labelled "what changed" / "why" blocks.
   The currently-running version gets an accent border + "Now running"
   pill so the user can spot their build at a glance. */
.release-notes-modal h2 {
    margin: 0 0 0.5rem;
    font-size: 15px;
}
.release-notes-lead {
    color: var(--ink-soft);
    margin: 0 0 1.25rem;
    font-size: 11px;
    line-height: 1.5;
}
.release-notes-empty {
    color: var(--ink-soft);
    font-style: italic;
}
.release-notes-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}
.release-note {
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.85rem 1rem 1rem;
    background: var(--panel);
}
.release-note.is-current {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent) inset;
}
.release-note-head {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    font-size: 9px;
    color: var(--ink-soft);
    margin-bottom: 0.35rem;
    flex-wrap: wrap;
}
.release-version-chip {
    display: inline-block;
    padding: 1px 7px;
    border: 1px solid var(--rule);
    border-radius: 669px;
    background: var(--panel);
    color: var(--ink);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    font-size: 9px;
}
.release-note-date {
    font-variant-numeric: tabular-nums;
}
.release-note-sha {
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
    font-size: 9px;
    color: var(--faint);
}
.release-note-current {
    margin-left: auto;
    padding: 3px 9px;
    border-radius: 669px;
    background: var(--accent);
    color: white;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.release-note-title {
    margin: 0 0 0.5rem;
    font-size: 13px;
    font-weight: 600;
    color: var(--ink);
}
.release-note-section {
    margin-top: 0.55rem;
}
.release-note-label {
    display: inline-block;
    font-size: 9px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--faint);
    margin-bottom: 0.2rem;
}
.release-note-body {
    color: var(--ink);
    font-size: 11px;
    line-height: 1.5;
    white-space: pre-wrap;
}

/* Forgot-password / register links + placeholder body copy on those
   two stub pages. Subtle so they don't pull focus from Sign in. */
.login-links {
    margin-top: 1.25rem;
    text-align: center;
    font-size: 11px;
}
.login-links a {
    color: var(--accent);
    text-decoration: none;
}
.login-links a:hover { text-decoration: underline; }
.login-link-sep {
    color: var(--faint);
    margin: 0 0.5rem;
}
.login-placeholder {
    color: var(--ink-soft);
    font-size: 10px;
    line-height: 1.5;
    margin: 0 0 0.5rem;
}
.login-placeholder a { color: var(--accent); }

/* Audit-trail page (/sign-offs) - top-level list of every ward-manager
   sign-off across the user's accessible units. */
.audit-header { margin: 1rem 0; }

/* Headline cards above the audit list - three side-by-side stats showing
   the cumulative hours and Â£ equivalents (Owes Trust / Trust Owes / Net)
   summed across the latest event per (unit, year). */
.audit-headlines {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.75rem;
    margin: 0.5rem 0 0.75rem;
}
.audit-headline {
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.85rem 1rem;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}
.audit-headline-label {
    font-size: 9px;
    color: var(--ink-soft);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-weight: 600;
}
.audit-headline-hours {
    font-size: 17px;
    font-weight: 600;
}
.audit-headline-money {
    font-size: 10px;
    color: var(--ink);
}
.audit-rate {
    margin: 0 0 0.75rem;
    font-size: 10px;
    color: var(--ink);
}

/* Filter toolbar above the audit table - three selects + an optional
   Clear pill on the right. Submits on every change (no Apply button). */
.audit-filters {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
    flex-wrap: wrap;
}
.audit-filter {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 11px;
    color: var(--ink-soft);
}
/* Match the Staff toolbar chrome - 46 px tall, 17 px font - so an
   admin landing here from /balances doesn't suddenly see tiny
   controls. Per design-brief First principles: "same job, same
   component" + "Readable and clickable, always". */
.audit-filter select,
.audit-filter input[type=datetime-local],
.audit-filter input[type=date] {
    font: inherit;
    font-size: 11px;
    height: 31px;
    padding: 0 0.85rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    color: var(--ink);
    cursor: pointer;
    min-width: 121px;
    transition: border-color 140ms ease, box-shadow 140ms ease;
}
.audit-filter select:hover,
.audit-filter input:hover { border-color: var(--accent); }
.audit-filter select:focus-visible,
.audit-filter input:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.audit-filters .filter-clear {
    margin-left: auto;
    height: 24px;
    line-height: 24px;
    padding: 0 0.95rem;
    text-decoration: none;
    display: inline-block;
}

/* Per-row Edit / Delete icon buttons in the audit table. Small, ghost,
   stay visually quiet until hover. */
.signoff-actions-col {
    width: 54px;
    text-align: center;
    white-space: nowrap;
}
.row-icon-btn {
    background: transparent;
    border: 1px solid var(--rule);
    color: var(--ink-soft);
    border-radius: 2px;
    /* 36 x 36 hit target per design brief First principles.
       Was 28 x 28 - too small to read on a Trust laptop. */
    width: 24px;
    height: 24px;
    padding: 0;
    cursor: pointer;
    font-size: 11px;
    line-height: 1;
    margin: 0 0.15rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    vertical-align: middle;
    transition: background 140ms ease, border-color 140ms ease, color 140ms ease, box-shadow 140ms ease;
}

/* Matches the .drill-modal-btn svg canon (16 px in 24 px). The
   .wg-open-btn variant below bumps to 18 px for the shield because
   it's a deliberately bolder affordance. */
.row-icon-btn .icon { width: 16px; height: 16px; }
.row-icon-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.row-icon-btn:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.row-icon-btn.row-icon-danger:hover { border-color: var(--rag-red-strong); color: var(--rag-red-strong); background: var(--rag-red-soft-bg-hover); }
/* Ward Guardian launcher = green shield. SHIELD ICON ONLY. Bigger
   glyph than the row-icon-btn default (16 px -> 18 px) so the
   affordance reads at a glance across the KPI / Units / Leave grids;
   still fits inside the 24 x 24 button so the row height is unchanged.
   Green hover keeps the WG identity instead of jumping to the default
   accent blue.
   DO NOT apply `.wg-open-btn` to sibling icons (Email, Duties drill,
   etc.) that share the same row - green is reserved for the WG report
   launcher only. Sibling icons stay on the standard `.row-icon-btn` /
   `.drill-modal-btn` / `.unit-action-btn` blue palette. */
.wg-open-btn { color: var(--rag-green); }
.wg-open-btn .icon { width: 18px; height: 18px; }
.wg-open-btn:hover { border-color: var(--rag-green-strong); color: var(--rag-green-strong); background: var(--rag-green-soft-bg); }
.wg-open-btn:focus { outline: none; border-color: var(--rag-green-strong); box-shadow: 0 0 0 2px var(--rag-green-soft-bg); }
.audit-title {
    font-weight: 600;
    font-size: 19px;
    margin: 0 0 0.25rem 0;
    color: var(--ink);
    letter-spacing: -0.01em;
}
.audit-subtitle { margin: 0; color: var(--ink-soft); }

.header-nav-link {
    color: var(--accent);
    text-decoration: none;
    font-size: 10px;
}
.header-nav-link:hover { text-decoration: underline; }

/* Drill-direction arrows on column headers - â€¹ means the column's cells
   drill back up the hierarchy, â€º means they drill down. Tight against
   the column name so the whole thing reads as one label. */
.drill-arrow {
    margin-left: 0.1rem;
    color: var(--accent);
    font-weight: 700;
    /* Drill arrows in column headers ride alongside the label text -
       they should read at the same scale, just bolder. The big
       glyph carets in disclosure controls (lozenges, division
       carets) keep using --ui-arrow-size. */
    font-size: 1em;
    line-height: 1;
    line-height: 1;
    vertical-align: -1px;
}
.drill-arrow-up   { color: var(--ink-soft); }
.drill-arrow-down { color: var(--accent); }

/* Grid column headers must stay on a single line so the drill-arrow and
   sort indicator don't wrap the label onto a second row. */
.balance-grid th { white-space: nowrap; }

/* Body cells are also single-line by default - long names render
   with ellipsis, never wrap into a second line. Per the design
   brief First principles consistency rule: the Staff grid was
   already nowrap (via .unit-col); the Annual Leave grid wasn't,
   so a long unit name there spilled to two rows while the Staff
   grid kept it on one. Standardise to nowrap everywhere; cells
   that legitimately need wrap (multi-line notes, AI commentary,
   sign-off note edits, top-mover meta lines) opt back in below. */
.balance-grid td {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.balance-grid td.notes-cell,
.balance-grid td.commentary-cell,
.balance-grid td.signoff-note-cell,
.balance-grid td.signoff-matched-cell,
.balance-grid td.row-meta-cell,
.balance-grid td .commentary-text,
.balance-grid td .row-meta {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
}

.panel-title-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
}
.panel-title-row .panel-title { margin: 0; }

dialog#detail-modal::backdrop {
    background: rgba(16, 42, 67, 0.45);
}

.staff-panel {
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 1.25rem 1.5rem;
}

.panel-title {
    /* Inherit body Inter sans. Was var(--display) (Fraunces serif)
       which read as 'wrong font' next to the sans-serif grids and
       tile strips everywhere else - paired with .staff-name and the
       Audit / Sign-off / Shortcuts titles below, this completes the
       sweep that pulls all heading-shaped surfaces onto the same
       typeface. */
    font-weight: 600;
    font-size: 12px;
    margin: 0 0 0.5rem 0;
    color: var(--ink);
}

.panel-title .panel-subtle {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    font-weight: 400;
    font-size: 9px;
    color: var(--ink-soft);
    margin-left: 0.5rem;
}

.placeholder-text {
    margin: 0;
    color: var(--ink-soft);
    font-style: italic;
    font-size: 11px;
}

/* Collapsible "Technical detail" surface used by AI surfaces (the AI
   result modal + the Inbox activation drift dialog) to hide raw SQL /
   exception text behind a single click. Designed-against pattern: a
   one-off <details><summary style="font-size:10px;..."> + <p style=
   "font-size:10px;..."> on every caller. The class collapses both. */
.tech-detail {
    margin-top: 0.4rem;
}
.tech-detail summary {
    font-size: 10px;
    color: var(--ink-soft);
    cursor: pointer;
}
.tech-detail p {
    margin: 0.3rem 0 0;
    font-size: 10px;
    color: var(--ink-soft);
}

/* OrgUnitIds export textarea - read-only mono block the user copies
   from. The .filter-input base sets the height + border + focus ring;
   this override flips it to monospace + drops the height ceiling so a
   3-row textarea reads naturally. */
.unit-ids-export-textarea {
    font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
    font-size: 11px;
    resize: vertical;
    height: auto;
}

/* Dim ID-prefix inside a unit chip - reads as a quiet label so the
   eye lands on the unit name. Used by the OrgUnitIds export modal
   where each chip shows the numeric ID + the unit name. */
.unit-chip-id {
    opacity: 0.6;
}

/* Counter variant of .kpi-profiles-trust-tag - same shape but the
   "N units" annotation reads as muted meta, not a Trust-shared badge.
   Used by the unit-profiles modal list rows. */
.kpi-profiles-count-tag {
    background: transparent;
    color: var(--ink-soft);
}

.chart-wrap {
    height: 188px;
    position: relative;
}

.chart-wrap-small {
    height: 80px;
    position: relative;
}

* { box-sizing: border-box; }

html, body {
    margin: 0;
    padding: 0;
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    font-size: 13px;
    line-height: 1.5;
    background: var(--bg);
    color: var(--ink);
    -webkit-font-smoothing: antialiased;
}

/* Form controls don't follow the body's 1.5 prose line-height -
   otherwise descenders (g, j, p, q, y) clip inside fixed-height
   inputs and selects after the 67% rescale. 1.2 gives the line box
   enough room for descenders without overflowing common control
   heights (24-31 px). */
input, select, textarea {
    line-height: 1.2;
}

/* Balances page is designed to fit one viewport - the grid handles its own
   vertical scroll internally. Suppress the body vertical scrollbar so the
   page never shows the "one row away from fitting" right-hand slider.
   Pages that genuinely need long scroll (Staff Details, login) opt out via
   the .page-scrolls modifier below.
   overflow-x: hidden on body pins the page to viewport width so any wide
   inner content (KPI / Staff / Approvals grids) has to scroll INSIDE its
   own wrapper rather than widening the whole page. Without this rule the
   body grew to fit the widest grid and the inner .balance-grid-scroll
   never engaged its horizontal scrollbar. */
body { overflow-y: hidden; overflow-x: hidden; }
body.page-scrolls { overflow-y: auto; }

/* â”€â”€ Header â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

.site-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.75rem 1.5rem;
    background: var(--panel);
    border-bottom: 1px solid var(--rule);
}

.brand {
    font-weight: 600;
    color: var(--accent);
    font-size: 1.25rem;
    letter-spacing: -0.01em;
}

.brand-tagline {
    font-weight: 400;
    color: var(--ink-soft);
    font-size: 0.95rem;
    margin-left: 0.25rem;
}

.user-strip {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    color: var(--ink-soft);
    font-size: 11px;
}

.user-email { font-weight: 500; color: var(--ink); }
.trust-name { font-weight: 500; color: var(--ink); }

.trust-selector {
    padding: 0.3rem 0.55rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    font: inherit;
    background: var(--panel);
}

main { padding: 0; }

/* â”€â”€ Mini-icons â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Inline-SVG sprite icons via <svg class="icon"><use href="#..."/></svg>.
   Inherits currentColor from the surrounding text so the icon
   takes its hue from whatever the button/label happens to be.
   The wrapper svg is inline-flex aligned to the cap height of
   adjacent text and given a touch of right-margin so the call
   site can write "<svg/>Label" and have it look like one chip. */
.icon {
    display: inline-block;
    width: var(--icon-size);
    height: var(--icon-size);
    vertical-align: -2px;
    flex: 0 0 auto;
    color: inherit;
    fill: none;
    stroke: currentColor;
}
.icon-sm { width: var(--icon-size-sm); height: var(--icon-size-sm); vertical-align: -1px; }
.icon-lg { width: var(--icon-size-lg); height: var(--icon-size-lg); vertical-align: -3px; }
.icon + span,
.icon-leading + * {
    margin-left: 0.45rem;
}

/* The activity bar lives fixed at the bottom of every signed-in
   page. Reserve EXACTLY its height so the grid's scrollbar lane
   rests flush on top of the activity strip with no body-bg gap. */
body { padding-bottom: var(--activity-strip-h); }

/* â”€â”€ Login page â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

.login-wrap {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: calc(100vh - 40px);
    padding: 1rem;
}

.login-card {
    background: var(--panel);
    border-radius: 5px;
    padding: 2rem;
    width: 100%;
    max-width: 348px;
    box-shadow: 0 1px 3px rgba(16, 42, 67, 0.04);
    border: 1px solid var(--rule);
}

.login-logo {
    display: block;
    margin: 0 auto 1.25rem;
    max-width: 214px;
    height: auto;
}

.login-title {
    margin: 0 0 1.25rem 0;
    font-size: 1.375rem;
    font-weight: 600;
    text-align: center;
}

.login-form {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.login-form label {
    font-size: 11px;
    color: var(--ink-soft);
}

.login-form input {
    padding: 0.45rem 0.75rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    font: inherit;
    background: var(--panel);
}

.login-form input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

.primary-button {
    padding: 0.7rem 1.1rem;
    background: var(--accent);
    color: white;
    border: 1px solid var(--accent);
    border-radius: 2px;
    font: inherit;
    font-weight: 500;
    cursor: pointer;
    line-height: 1.2;
}

.primary-button:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.primary-button:focus-visible {
    outline: none;
    border-color: var(--accent-hover);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

/* Ghost twin of .primary-button - IDENTICAL dimensions so modal action
   rows ("Cancel" + "Save changes") read as a matched pair. Use this for
   any secondary action that's a button (Cancel, Close, Re-sign). Keep
   .link-action for inline text links (e.g. "Cancel" next to a small
   form field, not a footer action row). */
.secondary-button {
    padding: 0.7rem 1.1rem;
    background: var(--panel);
    color: var(--ink);
    border: 1px solid var(--rule);
    /* 6 px matches .subtab + the consolidated button family. */
    border-radius: 2px;
    font: inherit;
    font-weight: 500;
    cursor: pointer;
    line-height: 1.2;
    text-decoration: none;
    display: inline-block;
}
.secondary-button:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--ink);
}
.secondary-button:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

.login-error {
    background: var(--error-bg);
    border: 1px solid var(--error-border);
    color: var(--error-text);
    padding: 0.7rem 0.8rem;
    border-radius: 2px;
    font-size: 11px;
    margin-bottom: 1rem;
}

/* Friendly /Error page reuses .login-wrap + .login-card chrome. Only
   the body copy, action button, and reference line need new rules. */
.error-body {
    margin: 0 0 1.25rem;
    color: var(--ink-soft);
    font-size: 12px;
    line-height: 1.5;
    text-align: center;
}
.error-action {
    display: block;
    text-align: center;
    text-decoration: none;
}
.error-ref {
    margin: 1rem 0 0;
    text-align: center;
    color: var(--ink-soft);
    font-family: 'JetBrains Mono', monospace;
    font-size: 10px;
    word-break: break-all;
}

/* Informational notice on the login page - used for the "you've been
   signed out after 2 hours of inactivity" message. Calmer than
   .login-error (accent blue, not red) because an idle timeout isn't
   the user's fault. */
.login-notice {
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    color: var(--ink);
    padding: 0.7rem 0.8rem;
    border-radius: 2px;
    font-size: 11px;
    margin-bottom: 1rem;
}

/* Login button loading state: form's onsubmit toggles .is-loading on
   the button while the page navigates, so the user has clear visual
   feedback their click was registered. The spinner uses the same arc
   as the global .spinner just sized down to sit beside the label. */
.login-submit {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.55rem;
    min-height: 28px;
    white-space: nowrap;
    /* Extra breathing room above the button so it doesn't crowd
       the password field on the password-required form. */
    margin-top: 0.85rem;
}
.login-spinner {
    display: none;
    width: 13px;
    height: 13px;
    border: 2px solid rgba(255, 255, 255, 0.35);
    border-top-color: #fff;
    border-right-color: #fff;
    border-radius: 50%;
    animation: spinner-spin 0.7s linear infinite;
    flex: 0 0 auto;
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.15);
}
.login-submit.is-loading .login-spinner { display: inline-block; }
.login-label-loading {
    display: none;
    color: #fff;
    font-weight: 600;
    letter-spacing: 0.02em;
}
.login-submit.is-loading .login-label-idle { display: none; }
.login-submit.is-loading .login-label-loading { display: inline; }
/* Don't fade the button while loading - the spinner needs the
   solid accent background behind it to read clearly. Use --ink
   (the dark blue heading colour) rather than the medium accent
   so white spinner + white label both have strong contrast. */
.login-submit.is-loading {
    background: var(--ink);
    border-color: var(--ink);
    color: #fff;
    opacity: 1;
}
.login-submit:disabled { cursor: progress; }

/* â”€â”€ Balances page â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

/* Flex column so #balance-grid (and its inner .balance-grid-container)
   can fill the remaining viewport below the chrome above it. This is
   what keeps the grid's horizontal scrollbar pinned to the visible
   bottom of the container - the previous block-flow layout used a
   magic-number height calc on .balance-grid-scroll that went stale
   every time chrome above the grid changed (filter chips, group-by row,
   status bar tweaks), so the wrapper bottom + its scrollbar slipped
   below the viewport. Height bound: viewport minus .site-header (~58 px)
   minus the body padding-bottom that reserves space for the fixed
   activity strip (30 px). */
.balances-page {
    /* Zero bottom padding so the .balance-grid-scroll wrapper's
       scrollbar lane sits flush on top of the fixed activity
       strip. Any bottom padding here would re-introduce a band
       of dead body-bg space between the scrollbar and the strip. */
    padding: 1.25rem 1.5rem 0;
    display: flex;
    flex-direction: column;
    height: calc(100vh - 58px - var(--activity-strip-h));
    box-sizing: border-box;
}
.balances-page > .tab-row,
.balances-page > .toolbar,
.balances-page > #active-filters,
.balances-page > .top-movers,
.balances-page > .audit-headlines,
.balances-page > .audit-rate,
.balances-page > .empty-state,
.balances-page > form.toolbar {
    flex: 0 0 auto;
}
.balances-page > #balance-grid {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
}
.balances-page > #balance-grid > .status-bar,
.balances-page > #balance-grid > .top-movers,
.balances-page > #balance-grid > .empty-state {
    flex: 0 0 auto;
}
.balances-page > #balance-grid > .skeleton-grid,
.balances-page > #balance-grid > .balance-grid-container,
.balances-page > .balance-grid-container {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
}
.balances-page > #balance-grid > .skeleton-grid > .balance-grid-container {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
}
.balances-page .balance-grid-container > .balance-grid-scroll {
    flex: 1 1 0;
    min-height: 0;
    /* Override the legacy block-flow height calc + .top-movers
       max-height overrides so flex sizing wins. */
    height: auto;
    max-height: none;
}
.balances-page > .status-bar {
    flex: 0 0 auto;
}

/* â”€â”€ Tab bar (overarching folder-style tabs) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

/* The tab bar sits flush against the toolbar below - the active tab's
   bottom edge becomes the toolbar's top edge, so the three tabs read as
   tabs OF the whole page rather than a separate widget. */

.tab-bar {
    display: flex;
    align-items: flex-end;
    gap: 0;
    margin-bottom: 0;
    /* Zero horizontal padding so the first and last main tabs are
       flush with the parent edge - lines up exactly with the sub-
       menu blue strip below (which also goes edge-to-edge). */
    padding: 0;
    position: relative;
    /* z-index has to outrank the .toolbar's sticky pills + any sticky thead
       below; with z-index: 1 the .mega-menu rendered behind the toolbar. */
    z-index: 1000;
    /* Fill the .tab-row so the .tabs flex container inside has real
       width to give the right-cluster tabs (Approvals + Budgets +
       Sign-offs) somewhere to push to via margin-left: auto. Without
       this the .tab-row's single flex child was content-sized, and
       the right-cluster auto-margin had no slack to consume. */
    flex: 1 1 auto;
}

.tabs {
    display: flex;
    align-items: flex-end;     /* keep siblings at their natural height
                                  so the active tab's extra padding-top
                                  lifts it above them - default stretch
                                  would force them all to match height. */
    gap: 0.25rem;
    flex: 1 1 auto;
}
/* My Inbox + My Outbox + Budgets + Sign-offs (and Approvals when
   present) are personal / governance / audit concerns, not primary
   day-to-day working tabs - push them to the far-right edge so they
   read as a separate cluster from Divisions / Units / Staff. Markup
   order is myinbox -> myoutbox -> approvals -> budgets -> signoffs;
   whichever appears FIRST in that cluster takes the auto-margin,
   the rest sit adjacent. My Inbox is always present so it takes the
   auto-margin in practice. */
/* No right-cluster auto-margin - all tabs sit flush-left in their
   saved order. Was: ".tab-myinbox / -myoutbox / -approvals / -budgets
   / -signoffs { margin-left: auto }" pushed Inbox/Audit/Approvals/
   Administration to the far right edge, which collided with the user-
   strip (Account menu) above on narrower laptops. Drag-reorder still
   works via .tabs[data-tab-reorder]. */
/* Kill the auto-margin on right-cluster tabs that aren't first, so
   the cluster stays tight regardless of which subset is unlocked. */
/* sibling-zero rule removed: was unwinding the auto-margin from the cluster's later siblings */

/* Drag-reorder affordance on the tab strip. cursor:grab gives the
   draggable hint without being so prominent it competes with the
   tab's primary 'click to switch' job. .tab-dragging fades the
   source while it's in flight; .tab-drop-target underlines the
   insertion point so the eye can predict where the drop lands. */
.tabs[data-tab-reorder] > .tab {
    cursor: pointer;
}
.tabs[data-tab-reorder] > .tab:active {
    cursor: grabbing;
}
.tab-dragging,
.tabs[data-tab-reorder] > .tab.tab-dragging {
    cursor: grabbing;
}
.tab-dragging {
    opacity: 0.4;
}
.tab-drop-target {
    box-shadow: -2px 0 0 0 var(--accent);
}

/* Reset-default glyph at the end of the tab strip. Only shows
   when a custom order is saved (see the @if in _TabBar.cshtml).
   Sized to sit alongside the tab buttons without competing. */
.tab-reset {
    background: transparent;
    border: 1px solid var(--rule);
    color: var(--ink-soft);
    font-size: 0.95rem;
    padding: 0.15rem 0.45rem;
    margin-left: 0.4rem;
    border-radius: 2px;
    cursor: pointer;
    align-self: center;
    transition: background-color 140ms ease, color 140ms ease, border-color 140ms ease;
}
.tab-reset:hover {
    background: color-mix(in srgb, var(--accent) 8%, #fff);
    border-color: var(--accent);
    color: var(--accent);
}

/* Inbox notification badge - small circle that sits to the right
   of the tab label and counts pending items. Hidden when count is
   0 (the Razor block doesn't render it). Accent-coloured rather
   than red because a pending-work count is a neutral nudge, not a
   variance alert - status colour stays reserved for tolerance,
   drift and over-budget signalling per the design brief. */
.tab .tab-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* .tab is now inline-flex with gap: 0.4rem so the badge picks
       up its spacing from gap, not margin-left. */
    /* Sized to sit WITHIN the tab's intrinsic line-height (icon =
       16px) so the inbox tab doesn't visually float above its
       siblings just because it carries a count. Previously 18 px
       which pushed the tab box ~3 px taller than sibling tabs
       without a badge. */
    min-width: 16px;
    height: 14px;
    padding: 0 5px;
    border-radius: 7px;
    /* Inactive tab is filled with --accent, so the badge needs to be
       white-on-accent to read against it. The active-tab arm below
       flips this back to accent-on-white. */
    background: #fff;
    color: var(--accent);
    font-size: 0.6rem;
    font-weight: 700;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    border: 1px solid #fff;
}
/* Active tab is white, so the badge inverts back to accent-on-white
   with an accent border so the circle stays visible. */
.tab.active .tab-badge {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
}

/* Discovery attention-badge variants. The Discovery surface runs long
   jobs (multi-minute SQL chains), so the tab badge doubles as an
   activity-monitor: blue = something running or queued for you,
   green-pulse = a chain just finished and you haven't visited yet.
   Both inherit size and position from .tab .tab-badge above; the
   modifier classes override only the colour signal + pulse. */
.tab .tab-badge.tab-badge-discovery-running {
    background: var(--accent-soft);
    color: var(--accent-hover);
    border-color: var(--accent);
}
.tab.active .tab-badge.tab-badge-discovery-running {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
}
.tab .tab-badge.tab-badge-discovery-unseen {
    background: var(--rag-green-soft-bg);
    color: var(--rag-green);
    border-color: var(--rag-green-soft-border);
    animation: discovery-tab-pulse 1.6s ease-in-out infinite;
}
.tab.active .tab-badge.tab-badge-discovery-unseen {
    background: var(--rag-green);
    color: #fff;
    border-color: var(--rag-green);
}
@keyframes discovery-tab-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(63, 122, 74, 0.55); }
    50%      { box-shadow: 0 0 0 5px rgba(63, 122, 74, 0); }
}
@media (prefers-reduced-motion: reduce) {
    .tab .tab-badge.tab-badge-discovery-unseen { animation: none; }
}

.tab {
    /* .tab is used by BOTH <button> and direct-link <a> tabs (KPIs,
       Approvals, Discovery). text-decoration: none kills the default
       anchor underline that was leaking through on the <a> variants. */
    text-decoration: none;
    /* inline-flex + align-items: center keeps the icon, text and
       optional count badge centred on the same axis so a tab
       carrying a badge (Inbox) renders at the SAME height as the
       siblings without it. Default <button> baseline-alignment let
       the 14 px badge nudge the Inbox tab box ~1 px taller than
       the icon-only tabs. */
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    background: var(--accent);
    border: 1px solid var(--accent);
    border-bottom: none;
    color: white;
    cursor: pointer;
    font: inherit;
    font-size: 11px;
    font-weight: 500;
    padding: 0.45rem 1.4rem;
    border-radius: 5px 5px 0 0;
    position: relative;
    /* No bottom overlap. The -1px previously here was vestigial -
       it dated from when the sub-tab strip didn't exist and the
       tab's bottom edge fused with the toolbar's top border. Now
       the sub-tab strip sits between tabs and toolbar; the -1px
       made the active (white) tab bleed 1 px into the blue
       sub-tab strip below. The toolbar has `border-top: none`
       so there's nothing left to overlap. */
    margin-bottom: 0;
    /* transition: none overrides the global button / .tab transition
       at the top of the file (140 ms across background-color,
       border-color, box-shadow etc.). That global rule was the source
       of the blue-border flicker the user kept seeing - the local
       .tab rule's missing `transition` did NOT cancel the global; only
       explicit `none` overrides it. */
    transition: none;
}

/* Tab icon: no hover scale. The 1.05 transform was a compositor-
   layer animation that fought the row hover and the parent
   transition on a dense grid - the cursor crossed multiple tabs
   faster than the scale could resolve, which read as jerk. */

/* Hover preview - an inactive tab flashes to the same white fill
   it would land on when clicked, so the cursor reads as "this is
   what you'd be selecting". Border stays the toolbar-matching
   --rule for visual continuity with the panel beneath. Adds a
   1 px inset highlight at the top so the hover fill reads as
   "lifted" rather than "flat" - subtle depth, no shadow. */
.tab:not(.active):hover {
    background: var(--panel);
    color: var(--ink);
    /* No blue border on hover. The base border is var(--accent)
       (blue); keeping it on hover would re-introduce the blue
       outline the user explicitly asked to remove. Matching the
       hover background hides the border into the white fill. */
    border-color: var(--panel);
}

.tab.active {
    background: var(--panel);
    color: var(--ink);
    /* Toolbar-matching border colour: --rule is the same line the
       toolbar shelf uses on its sides + bottom, so the active tab
       and the toolbar below it read as one continuous chrome
       outline rather than two stacked shapes. */
    border-color: var(--rule);
    /* No height bump on activation - the active state is already
       distinguished by the white fill, the --rule border and the
       600 weight. The previous padding-top lift was causing the
       whole tab row to grow taller whenever any tab was active,
       which made the gap below it (sub-tab strip OR toolbar)
       jump every time the user switched tabs - most noticeable on
       Inbox / Audit which have the count badge sharing the row. */
    border-bottom: none;
    font-weight: 600;
    z-index: 2;
}

.export-link {
    margin-left: auto;
    margin-bottom: 0.5rem;
    color: var(--accent);
    text-decoration: none;
    font-size: 10px;
    padding: 0.45rem 0.85rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
}

.export-link:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
}

/* â”€â”€ Drill link buttons in grid cells â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

.drill-link {
    background: none;
    border: none;
    color: var(--link);
    cursor: pointer;
    font: inherit;
    padding: 0;
    text-align: left;
    text-decoration: none;
    border-bottom: 1px dotted var(--accent);
}

.drill-link:hover {
    color: var(--link-hover);
    border-bottom-style: solid;
}

/* â”€â”€ Duty tags â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

.duty-tag {
    display: inline-block;
    font-size: 7px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 0.15rem 0.5rem;
    border-radius: 669px;
    color: white;
}

.duty-tag-local      { background: var(--duty-local); }
.duty-tag-bank       { background: var(--duty-bank); }
.duty-tag-agency     { background: var(--duty-agency); }

/* NERG sub-types - colour map matches sql/schema/ob_shifts.md.
   Yellow + lighter blue need dark text for AA contrast. */
.duty-tag-workingday { background: var(--duty-workingday); color: var(--ink); }
.duty-tag-annualleave{ background: var(--duty-annual);     color: var(--ink); }
.duty-tag-sickness   { background: var(--duty-sickness); }
.duty-tag-studyleave { background: var(--duty-study); }
.duty-tag-parenting  { background: var(--duty-parenting); }
.duty-tag-otherleave { background: var(--duty-other); }
.duty-tag-unknown    { background: var(--duty-other); }

/* Fallback for any NERG group name we haven't styled yet. */
.duty-tag-nerg       { background: var(--duty-other); }

.row-meta {
    display: block;
    color: var(--ink-soft);
    font-size: 8px;
    margin-top: 0.15rem;
}

.toolbar {
    background: var(--panel);
    /* No top border - the tab strip above provides the visual chrome.
       Side + bottom borders only so the toolbar reads as a continuous
       surface with whichever tab is active. */
    border-left: 1px solid var(--rule);
    border-right: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule);
    border-top: none;
    border-top-left-radius: 0; /* tabs flow into this corner */
    border-top-right-radius: 5px;
    border-bottom-left-radius: 5px;
    border-bottom-right-radius: 5px;
    padding: 0.8rem 1rem;
    position: relative;
    /* Above the table's sticky thead / tfoot (both z-index: 1) so the
       Grade and Units dropdowns float over column headers and totals
       instead of being clipped beneath them. */
    z-index: 10;
}

/* Single-row toolbar: no wrapping. On narrower screens the page
   itself gets a horizontal scrollbar - the toolbar does NOT clip
   its own overflow because that would also cut off the picker
   popovers (Grade, Units) which absolute-position inside it. */
.filter-row {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: 1.25rem;
}

.filter-input {
    padding: 0.55rem 0.75rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    font: inherit;
    /* Use background-color (not shorthand `background:`) so the
       Units filter's background-image chevron isn't reset by this
       base rule. */
    background-color: var(--panel);
    color: var(--ink);
    height: 31px;
}

/* Unify placeholder colour with the Grade picker's resting label so
   the three primary filters (Name, Units, Grade) read as the same
   visual family when nothing is selected. */
.filter-input::placeholder {
    color: var(--ink-soft);
    opacity: 1;
}

/* Name shortened 30% from the previous 720 px so the toolbar can
   keep everything (Name, Units, Grade, Date, Ask) on a single line
   on a typical wide screen. */
.filter-name { width: 338px; }

/* <select>.filter-input width floor for the parm rows where a bare
   select sits next to a flex sibling that can squeeze it - Inbox age,
   Outbox type / days / scope, SignOffs year / unit / signer. The
   previous global rule unintentionally overrode .group-by-status
   (which sets its own 147 px floor) and added a 1.75 rem right
   padding that looked like dead space on the Staff toolbar's
   group-by select, so this rule is now scoped to the parm rows that
   originally had the clipping problem. */
.inbox-parm-row select.filter-input,
.work-audit-filters select.filter-input {
    min-width: 13ch;
}

/* Units control - rebuilt as a plain dropdown using <details> so it
   matches the Grade picker's chrome (clickable summary + popover).
   The actual search-as-you-type input lives INSIDE the popover so
   the toolbar reads as a button, not a text field. */
.units-search-wrap {
    position: relative;
    display: inline-block;
}
.unit-picker {
    position: relative;
    z-index: 100;
}
.unit-summary {
    list-style: none;
    cursor: pointer;
    padding: 0.6rem 0.95rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    color: var(--ink-soft);
    height: 31px;
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    line-height: 20px;
    user-select: none;
    /* Shortened 20% from the previous 880 px so the toolbar can
       keep Name, Units, Grade, Date and Ask on one line. The
       popover beneath is allowed to stay wider so long unit
       names still fit. */
    min-width: 472px;
    max-width: 100%;
    box-sizing: border-box;
}
.unit-summary::-webkit-details-marker { display: none; }
.unit-summary:hover { border-color: var(--accent); }
details.unit-picker[open] > .unit-summary {
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.unit-summary-label {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--ink-soft);
}
.unit-summary-caret {
    font-size: var(--ui-arrow-size);
    font-weight: var(--ui-arrow-weight);
    color: var(--ink-soft);
    line-height: 1;
    flex: 0 0 auto;
}

/* Trigger button that opens the units picker dialog. Same chip shape
   as the legacy .unit-summary so the toolbar layout stays identical. */
.unit-picker-trigger {
    cursor: pointer;
    padding: 0.6rem 0.95rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    color: var(--ink-soft);
    height: 31px;
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    line-height: 20px;
    user-select: none;
    min-width: 472px;
    max-width: 100%;
    box-sizing: border-box;
    font: inherit;
    text-align: left;
    transition: border-color 140ms ease, box-shadow 140ms ease;
}
.unit-picker-trigger:hover  { border-color: var(--accent); }
.unit-picker-trigger:focus  { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.unit-picker-trigger .unit-summary-label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-soft); }
.unit-picker-trigger .unit-summary-caret { font-size: var(--ui-arrow-size); font-weight: var(--ui-arrow-weight); color: var(--ink-soft); line-height: 1; flex: 0 0 auto; }

/* Grade + Band + Units (compact) pickers use a STABLE fixed
   width so the toolbar doesn't reflow when the label changes
   ("Divisions / Units" -> "Ward A +3" -> "1 unit: Coronary
   Care Unit"). The width comfortably swallows the typical
   "first-unit-name + +N pill" content; longer labels truncate
   via the existing text-overflow: ellipsis on the inner spans.
   Pickers stay readable and the rest of the toolbar (date
   controls, Patterns, Share, Ask + Help) stays at a constant X. */
.unit-picker-trigger.grade-picker-trigger,
.unit-picker-trigger.unit-picker-trigger-compact {
    width: 220px;
    min-width: 220px;
    max-width: 220px;
    padding: 0.55rem 0.8rem;
}
/* Inner spans inside the compact triggers ellipsis-truncate the
   first-unit-name when it exceeds the trigger's content width -
   stops a long ward name from pushing the +N pill out of the
   220 px box. */
.unit-picker-trigger.unit-picker-trigger-compact .grade-summary-selection,
.unit-picker-trigger.grade-picker-trigger .grade-summary-selection {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
    flex: 1 1 auto;
    font: inherit;
    color: var(--ink-soft);
}
.unit-picker-trigger.unit-picker-trigger-compact .grade-summary-count,
.unit-picker-trigger.grade-picker-trigger .grade-summary-count {
    font: inherit;
    font-size: 10px;
    color: var(--ink-soft);
    background: transparent;
    border-color: var(--rule);
    /* Stop the count lozenge shrinking when the unit / grade name
       inside the trigger expands - default flex behaviour is 0 1 auto
       (can shrink), which clips three-digit counts like +129 to +12.
       Floor is wide enough for a four-character payload ("+999"). */
    flex: 0 0 auto;
    min-width: 4ch;
    padding: 0 0.4rem;
}

/* Modal version of the picker. One outer dialog, one inner framed
   card. Only the hierarchy inside the frame scrolls; no nested
   scrollbars on the modal itself. */
.unit-picker-dialog {
    border: none;
    background: transparent;
    padding: 0;
    width: min(482px, 96vw);
    max-height: 90vh;
    overflow: visible;
}
.unit-picker-dialog::backdrop {
    background: rgba(16, 42, 67, 0.45);
}
.unit-picker-modal {
    background: var(--panel);
    border-radius: 5px;
    box-shadow: 0 8px 24px rgba(16, 42, 67, 0.18);
    max-height: 90vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    position: relative;
}
.unit-picker-modal-head {
    position: relative;
    padding: 1.1rem 3rem 0.6rem 1.5rem;
    flex: 0 0 auto;
}
.unit-picker-modal-title {
    margin: 0 0 0.2rem 0;
    font-size: 1.15rem;
    color: var(--ink);
}
.unit-picker-modal-hint {
    margin: 0;
    font-size: 9px;
    color: var(--ink-soft);
    line-height: 1.4;
}
.unit-picker-modal-close {
    position: absolute;
    top: 0.65rem;
    right: 0.65rem;
    width: 21px;
    height: 21px;
    border-radius: 50%;
    border: 1px solid transparent;
    background: transparent;
    color: var(--ink-soft);
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.unit-picker-modal-close:hover { background: var(--accent-soft); color: var(--ink); }

/* The framed card inside the modal: search row at the top,
   hierarchy below. Single scroller (.unit-picker-frame-body). */
.unit-picker-frame {
    flex: 1 1 auto;
    margin: 0 1.25rem 1.25rem 1.25rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    min-height: 0;
}
.unit-picker-frame-toolbar {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.6rem;
    background: var(--accent-soft);
    border-bottom: 1px solid var(--rule);
}
.unit-picker-frame-body {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: 0.4rem 0.6rem 0.6rem 0.6rem;
    min-height: 0;
}
/* Search input + Clear / Apply icon-buttons - shared between the
   framed toolbar (current) and the legacy search-row (kept for any
   stray caller). */
.unit-picker-search-icon {
    color: var(--ink-soft);
    flex: 0 0 auto;
}
.unit-picker-search-input {
    flex: 1 1 auto;
    padding: 0.55rem 0.75rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    color: var(--ink-medium);
    font: inherit;
    font-size: 10px;
    min-width: 0;
}
.unit-picker-search-input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.unit-picker-search-input::-webkit-search-cancel-button { display: none; }

.unit-picker-action {
    flex: 0 0 auto;
    width: 25px;
    height: 25px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    color: var(--ink-soft);
    cursor: pointer;
    transition: background 140ms ease, color 140ms ease, border-color 140ms ease;
}
.unit-picker-action:hover {
    background: var(--accent-soft);
    color: var(--ink);
    border-color: var(--accent);
}
.unit-picker-action:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.unit-picker-apply {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}
.unit-picker-apply:hover {
    background: var(--accent-hover);
    border-color: var(--accent-hover);
    color: #fff;
}
.unit-picker-apply:disabled,
.unit-picker-apply[disabled] {
    background: var(--rule);
    border-color: var(--rule);
    color: var(--ink-soft);
    cursor: not-allowed;
}
.unit-picker-apply:disabled:hover,
.unit-picker-apply[disabled]:hover {
    background: var(--rule);
    border-color: var(--rule);
    color: var(--ink-soft);
}


.filter-group {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}

.filter-clear {
    background: transparent;
    border: 1px solid var(--rule);
    color: var(--ink-soft);
    cursor: pointer;
    height: 31px;
    padding: 0 0.95rem;
    border-radius: 2px;
    font: inherit;
    font-size: 9px;
    font-weight: 500;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.filter-clear:hover {
    border-color: var(--accent);
    color: var(--accent);
    background: var(--accent-soft);
}

.status-meta {
    color: var(--ink-soft);
    font-style: italic;
    margin-left: 0.5rem;
}

.filter-input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

/* Date + roster-nav cluster - date input on the left, three small
   pills (back 28 / Today / forward 28) on the right, all at the same
   46 px control height so they sit flush in the toolbar row. */
.date-controls {
    display: inline-flex;
    align-items: stretch;
    gap: 0.35rem;
}
/* â”€â”€ Custom date popover (Today + month nav inside) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

.date-popover-wrap {
    position: relative;
    display: inline-flex;
    align-items: stretch;
}

/* Date trigger matches the Grade picker summary - same height,
   padding, border, background and font so all three primary
   pickers (Units, Grade, Date) read as one family. */
.date-popover-trigger {
    height: 31px;
    min-width: 147px;
    padding: 0.6rem 0.95rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    color: var(--ink-soft);
    font: inherit;
    line-height: 20px;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    text-align: left;
    user-select: none;
}
#date-popover-label {
    color: var(--ink-soft);
    font-weight: 400;
}

.date-popover-trigger:hover { border-color: var(--accent); }
.date-popover-trigger:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

.date-popover-trigger-icon {
    font-size: var(--ui-arrow-size);
    font-weight: var(--ui-arrow-weight);
    color: var(--accent-hover);
    line-height: 1;
    margin-left: auto;
}

/* Roster Start (KPI mode) wears the same date-popover-trigger chrome
   as the As-of date so the toolbar's two date parms read as a matched
   pair. The native <input type="date"> is kept in the DOM (HTMX still
   posts the ISO date on change) but visually collapsed so it doesn't
   double up with the styled button - showPicker() on the button
   opens the picker, and oninput on the input updates the trigger's
   formatted label. The 1 x 1 px size + opacity 0 keeps focus +
   showPicker() working on every browser that supports the API. */
.kpi-roster-start-popover-wrap {
    position: relative;
}
.kpi-roster-start-input {
    position: absolute;
    inset: 0;
    width: 1px;
    height: 1px;
    margin: 0;
    padding: 0;
    border: 0;
    opacity: 0;
    pointer-events: none;
    z-index: -1;
}

.date-popover {
    position: absolute;
    top: calc(100% + 3px);
    left: 0;
    z-index: 200;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    box-shadow: 0 5px 19px rgba(16, 42, 67, 0.12);
    padding: 0;
    display: none;
    width: 241px;
    user-select: none;
}
.date-popover.is-open { display: block; }

.date-popover-header {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.6rem 0.7rem;
    border-bottom: 1px solid var(--rule);
    background: var(--bg);
}
/* Month-step buttons in the date popover header reuse .sdv-nav-btn -
   one canonical round chevron-button across SDV staff nav, MSV week
   stepper, and the date popover. See docs/design-brief.md -> Charts
   -> "Nav glyphs inside chart headers" for the visual contract. */
.date-popover-today {
    margin-left: auto;
    padding: 0 1rem;
    height: 28px;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    color: var(--ink);
    font: inherit;
    font-size: 10px;
    font-weight: 600;
    cursor: pointer;
}
.date-popover-today:hover { background: var(--accent-soft); border-color: var(--accent); }
.date-popover-label {
    font-weight: 600;
    color: var(--ink);
    font-size: 11px;
    flex: 1 1 auto;
    text-align: center;
    min-width: 0;
}

.date-popover-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    padding: 0.4rem 0.5rem 0.5rem;
    gap: 1px;
}
.date-popover-dow {
    font-size: 8px;
    font-weight: 600;
    color: var(--ink-soft);
    text-align: center;
    padding: 0.35rem 0;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.date-popover-day {
    height: 28px;
    border: 1px solid transparent;
    background: transparent;
    color: var(--ink);
    font: inherit;
    font-size: 10px;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
    border-radius: 2px;
    padding: 0;
    text-align: center;
}
.date-popover-day:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
}
.date-popover-day.is-other-month { color: var(--faint); }
.date-popover-day.is-today {
    border-color: var(--accent);
    font-weight: 700;
}
.date-popover-day.is-selected {
    background: var(--accent);
    color: white;
    font-weight: 700;
    border-color: var(--accent-hover);
}

/* Parm-row sibling for .secondary-button (and .primary-button). The
   bare .secondary-button is sized to pair with a 44 px .primary-button
   in modal action rows; in the parm row that height clashes with the
   31 px Ask button + 31 px pickers + 31 px date controls. .toolbar-action
   shrinks it to the status-bar-lozenge family without disturbing modal usage.
   Anywhere a secondary-button or primary-button anchors a top-of-grid
   action (Activation, Uploads, Upload budget, Upload ESR, Compare
   selected), add this modifier so the row reads as one consistent strip.
   Single source of truth: edit here, never inline-override per page. */
.toolbar-action {
    /* Status-bar-lozenge scale (matches .status-bar-export +
       .generate-notes + .generate-all-btn) so every secondary
       action across the product reads at the same 24 px height,
       9 px font, accent-coloured label. The accent
       fill is what flags this as "an action you can take" without
       the heavier 31 px / 44 px chrome the picker family + modal
       footers reserve. Combine with .secondary-button (border +
       panel background + hover swap) OR .primary-button (solid
       accent fill) - the modifier only tunes height, padding,
       font and icon size; colour comes from the base class. */
    height: 24px;
    padding: 0 0.95rem;
    font-size: 9px;
    font-weight: 600;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    white-space: nowrap;
    line-height: 1;
}
.toolbar-action .icon { width: 11px; height: 11px; }
/* Secondary-button base ships ink-coloured text by default; the
   status-bar lozenge family reads in accent so the action stands
   out from the surrounding meta copy. Match it here so a button
   wearing both classes lands in the same colour family. */
/* No accent-text override - .secondary-button.toolbar-action wears --ink
   so Export / Generate Notes / Hide Notes / Upload etc. read with the SAME
   text colour as the sub-tab buttons (Divisions / Units / Staff). The
   user's reference style is .subtab; this keeps the whole button row in
   one visual family. */

/* G12 destructive modifier - same chrome as .secondary-button.toolbar-action
   (24 px lozenge, pale border, hover wash) but the accent text + border
   swap to RAG red so it visibly reads as "cancel / abort / reject" without
   inventing a fresh class. Pair with .secondary-button.toolbar-action. */
.toolbar-action-destructive {
    color: var(--rag-red, #b91c1c);
    border-color: var(--rag-red, #b91c1c);
}
.toolbar-action-destructive:hover {
    background: var(--rag-red-soft-bg, #fdecea);
    color: var(--rag-red, #8a1f1f);
    border-color: var(--rag-red, #b91c1c);
}

/* Respect the native [hidden] attribute even though .toolbar-action
   sets display: inline-flex (which would otherwise override the
   user-agent's display: none for hidden elements). The Stop button
   ships hidden by default and only shows during a Generate-all run. */
.toolbar-action[hidden] { display: none; }

/* G27 - .row-action-text family. Modifier for buttons that carry a
   TEXT label inside a grid row or a section header (NOT inside a
   parm-row / status-bar - that's .toolbar-action; NOT icon-only -
   that's .row-icon-btn). 28 px tall (between .toolbar-action's 24 px
   and the canonical primary/secondary modal-footer 36 px) so the
   button reads as actionable without bullying the row content. Combine
   with .primary-button (commit / proceed - "Sign off", "Mark actioned",
   "Mark all clean actioned") OR .secondary-button (passive / explore -
   "Propose change" as a starting form, read-only context).
   Sites: Budgets SignOffs.cshtml "Sign off", Activation.cshtml "Mark
   actioned" + "Mark all clean actioned", Approvals "Propose change". */
.row-action-text {
    height: 28px;
    padding: 0 0.8rem;
    font-size: 0.78rem;
    font-weight: 600;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    white-space: nowrap;
    line-height: 1;
}
.row-action-text .icon,
.row-action-text .icon-sm { width: 14px; height: 14px; }

.ask-button {
    /* Match the toolbar-action family (24 px, 9 px font) so every
       button in the top action row reads as one shape. */
    height: 24px;
    padding: 0 0.95rem;
    font-size: 9px;
    font-weight: 600;
    margin-left: auto;
    /* "Ask a question" must never wrap on a small Trust laptop. The
       icon + label needs a hard floor on both width AND a no-wrap
       constraint so the button keeps its full label even when the
       parm row is tight. flex-shrink:0 stops siblings squeezing it. */
    white-space: nowrap;
    flex: 0 0 auto;
    min-width: 114px;
}

/* â”€â”€ Grade multi-select picker â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

.grade-picker {
    position: relative;
    flex: 0 0 auto;
    z-index: 100;
}

.grade-summary {
    list-style: none;
    cursor: pointer;
    padding: 0.6rem 0.95rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    color: var(--ink-soft);
    height: 31px;
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    line-height: 20px;
    user-select: none;
    /* Doubled to match the Name + Units filter widths so the three
       primary filters read as a single visual family. */
    min-width: 456px;
}

.grade-summary::-webkit-details-marker { display: none; }
.grade-summary::after {
    content: 'â–¾';
    margin-left: 0.45rem;
    color: var(--ink-soft);
    font-size: var(--ui-arrow-size);
    font-weight: var(--ui-arrow-weight);
    line-height: 1;
}

.grade-summary:hover { border-color: var(--accent); }
.grade-summary-label { color: var(--ink-soft); flex: 0 0 auto; }
.grade-summary-selection {
    color: var(--ink);
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    flex: 1 1 auto;
    min-width: 0;
}
.grade-summary-count {
    display: inline-block;
    min-width: 15px;
    padding: 0.05rem 0.45rem;
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    border-radius: 7px;
    font-size: 9px;
    font-weight: 600;
    color: var(--ink);
    text-align: center;
}

details[open] > .grade-summary {
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

.grade-form {
    position: absolute;
    top: 100%;
    left: 0;
    z-index: 200;
    min-width: 161px;
    margin-top: 3px;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    box-shadow: 0 3px 11px rgba(16, 42, 67, 0.08);
    display: flex;
    flex-direction: column;
}

.grade-list {
    max-height: 214px;
    overflow-y: auto;
    padding: 0.3rem;
}

label.grade-pick {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.35rem 0.6rem;
    cursor: pointer;
    border-radius: 2px;
}

label.grade-pick:hover { background: var(--accent-soft); }

label.grade-pick input[type=checkbox] {
    width: 11px;
    height: 11px;
    accent-color: var(--accent);
    cursor: pointer;
    flex: 0 0 auto;
}

.grade-empty {
    padding: 0.75rem;
    color: var(--ink-soft);
    font-style: italic;
    font-size: 10px;
}

.grade-actions {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.6rem;
    padding: 0.65rem 0.8rem;
    border-bottom: 1px solid var(--rule);
    background: var(--bg);
    /* Sticky-top so Apply / Clear stay visible regardless of how far
       the user has scrolled down the list. */
    position: sticky;
    top: 0;
    z-index: 1;
}

.grade-actions .primary-button,
.grade-actions .secondary-button {
    margin: 0;
    padding: 0.55rem 1.4rem;
    height: 29px;
    font-size: 10px;
    font-weight: 600;
}

/* "All" toggle as the first row inside the Grade list - same row
   layout as a regular grade, with a subtle separator below so it
   reads as a meta-row, not "another grade called All". */
.grade-pick.is-all {
    border-bottom: 1px solid var(--rule);
    font-weight: 600;
}

/* â”€â”€ AI dialog â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

dialog.ai-dialog {
    width: min(375px, 92vw);
    border: 1px solid var(--rule);
    border-radius: 5px;
    padding: 0;
    background: var(--panel);
    color: var(--ink);
    box-shadow: 0 8px 21px rgba(16, 42, 67, 0.18);
}

dialog.ai-dialog::backdrop {
    background: rgba(16, 42, 67, 0.45);
}

.ai-dialog-form {
    padding: 1.4rem 1.5rem 1.2rem;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.ai-dialog-title {
    margin: 0;
    font-size: 1.25rem;
    font-weight: 600;
}

.ai-dialog-hint {
    margin: 0;
    color: var(--ink-soft);
    font-size: 9px;
}

.nl-examples {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    margin-top: 0.25rem;
}

.nl-examples-label {
    font-size: 9px;
    color: var(--ink-soft);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-weight: 600;
}

.nl-example {
    background: var(--accent-soft);
    border: 1px solid var(--rule);
    color: var(--ink);
    cursor: pointer;
    font: inherit;
    font-size: 9px;
    padding: 0.5rem 0.75rem;
    border-radius: 2px;
    text-align: left;
    line-height: 1.4;
}

.nl-example:hover {
    border-color: var(--accent);
    background: white;
}

.ai-dialog-hint em {
    color: var(--ink);
    font-style: italic;
}

.nl-input {
    width: 100%;
    padding: 0.6rem 0.8rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    font: inherit;
    background: var(--panel);
    color: var(--ink);
}

.nl-input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

.ai-dialog-actions {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 0.6rem;
    margin-top: 0.2rem;
}

.ai-dialog-actions .primary-button { margin: 0; padding: 0.5rem 1rem; }

/* Generic button-spinner pattern (B6 / G19 H13). Works for both
   HTMX-driven buttons (htmx adds .htmx-request automatically to the
   requesting element) AND plain form POST buttons (form's onsubmit
   adds .is-loading + sets disabled). Each .btn-with-spinner has two
   children: .btn-label (default visible) + .btn-spinner (default
   hidden). During load the visibility flips. Pair with disabled=true
   on the button so the user can't double-submit. Moved here from
   budgets.css so the Ask dialog (which appears on every page) and
   the Inbox modals (which can render on /my-inbox + /balances) both
   pick up the spinner styling.
   Replaces the legacy .ask-submit + .ask-spinner family that was
   bespoke to the Ask dialog. */
.btn-with-spinner {
    position: relative;
    min-width: 9ch;
}
.btn-with-spinner .btn-spinner {
    display: none;
    gap: 0.35rem;
    align-items: center;
}
.btn-with-spinner.is-loading .btn-label,
.btn-with-spinner.htmx-request .btn-label { display: none; }
.btn-with-spinner.is-loading .btn-spinner,
.btn-with-spinner.htmx-request .btn-spinner { display: inline-flex; }
.btn-with-spinner[disabled] { cursor: progress; opacity: 0.85; }
/* The Ask form sets .htmx-request on the FORM during the in-flight
   request, not on the button itself - the rule below proxies that to
   the inner button so .btn-with-spinner.htmx-request fires without
   per-form onsubmit JS. Scoped to .ai-dialog-form so it doesn't
   accidentally trigger on every other htmx form on the page. */
.ai-dialog-form.htmx-request .btn-with-spinner {
    cursor: progress;
    opacity: 0.85;
    pointer-events: none;
}
.ai-dialog-form.htmx-request .btn-with-spinner .btn-label { display: none; }
.ai-dialog-form.htmx-request .btn-with-spinner .btn-spinner { display: inline-flex; }

/* Sql-action result modal - opens inside the shared #detail-modal
   slot. Reuses staff-modal chrome with a small result table that
   inherits .balance-grid styling so numbers right-align natively. */
.ai-result-modal .ai-result-title { margin: 0 0 0.75rem; font-size: 14px; }
.ai-result-modal .ai-result-question,
.ai-result-modal .ai-result-interp,
.ai-result-modal .ai-result-limitation {
    margin: 0 0 0.35rem;
    font-size: 10px;
    line-height: 1.45;
}
.ai-result-modal .ai-result-limitation { color: var(--rag-amber); }
.ai-result-modal .ai-result-label { color: var(--ink-soft); font-weight: 600; margin-right: 0.35rem; }
.ai-result-modal .ai-result-error {
    background: var(--rag-red-soft-bg);
    border: 1px solid var(--rag-red-soft-border);
    color: var(--rag-red);
    padding: 0.6rem 0.8rem;
    border-radius: 2px;
    margin: 0.75rem 0;
    font-size: 10px;
}
.ai-result-modal .ai-result-narrative {
    margin: 0.75rem 0 0.5rem;
    padding: 0.65rem 0.85rem;
    background: var(--accent-soft);
    border-left: 2px solid var(--accent);
    border-radius: 2px;
    font-size: 11px;
    line-height: 1.5;
    color: var(--ink);
}
.ai-result-modal .ai-result-table { margin: 0.5rem 0 1rem; width: auto; min-width: 320px; }
.ai-result-modal .ai-result-chart-wrap {
    margin: 0.5rem 0 1rem;
    padding: 0.75rem 0.85rem 0.85rem;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
}
.ai-result-modal .ai-result-chart-title {
    margin: 0 0 0.6rem;
    font-size: 11px;
    font-weight: 600;
    color: var(--ink);
}
.ai-result-modal .ai-result-chart {
    width: 100% !important;
    height: 280px !important;
    max-height: 320px;
}
/* Doughnut needs a square-ish frame or Chart.js fits to the wide
   container and renders an oval. Cap width + centre the canvas. */
.ai-result-modal .ai-result-chart-wrap[data-chart-type="doughnut"] {
    text-align: center;
}
.ai-result-modal .ai-result-chart-wrap[data-chart-type="doughnut"] .ai-result-chart {
    max-width: 320px;
    margin: 0 auto;
}
.ai-result-modal .ai-result-footer { margin-top: 1rem; font-size: 9px; color: var(--ink-soft); }
.ai-result-modal .ai-result-footer-stat { margin-right: 0.5rem; }
.ai-result-modal .ai-result-sql-details { display: inline-block; margin-left: 0.25rem; }
.ai-result-modal .ai-result-sql-details summary { cursor: pointer; color: var(--accent-hover); font-size: 9px; }
.ai-result-modal .ai-result-sql {
    background: var(--bg);
    border: 1px solid var(--rule);
    padding: 0.55rem 0.7rem;
    font-family: var(--mono);
    font-size: 9px;
    line-height: 1.5;
    white-space: pre-wrap;
    word-break: break-word;
    margin-top: 0.4rem;
}

/* Natural-language Question chip on the Filtering-by strip. Sits
   alongside every other .active-filter-chip but reads a touch
   stronger so the user can see "this is the question that drove
   the filters below it". Same chrome - just a saturated accent
   border so it doesn't disappear into the soft-pastel neighbours. */
.active-filter-chip-question {
    border-color: var(--accent);
    background: var(--panel);
}
.active-filter-chip-question .active-filter-chip-key {
    color: var(--accent-hover);
}
/* "Couldn't answer" state - pale red wash so it reads as a failed
   ask at a glance without shouting. Used when Actionable=false on
   the active log row (rate-limited / not-configured / unparseable
   / AI declined to apply filters). The key colour shifts to RAG red
   so it reads as a status, not just a different chip. */
.active-filter-chip-question-unanswered {
    border-color: var(--rag-red-soft-border);
    background: var(--rag-red-soft-bg);
}
.active-filter-chip-question-unanswered .active-filter-chip-key {
    color: var(--rag-red);
}
/* "Partial" state - the AI applied SOME filters but flagged
   something it couldn't do (the Limitation field on AiSearchResult
   is non-empty). Pale amber so the user sees their headline ask
   wasn't fully answered, even though the grid did move - matching
   the R/A/G amber vocabulary used elsewhere for "warning / attention
   required". */
.active-filter-chip-question-partial {
    border-color: var(--rag-amber-soft-border);
    background: var(--rag-amber-soft-bg);
}
.active-filter-chip-question-partial .active-filter-chip-key {
    color: var(--rag-amber);
}

/* Search results panel inside the units picker dialog - flows inline
   beneath the sticky search input. The dialog chrome provides the
   border/shadow/background. */
.unit-results {
    background: var(--panel);
    overflow-y: auto;
    flex: 1 1 auto;
    min-height: 0;
}

.unit-results:empty { display: none; }

.unit-results-empty {
    padding: 1rem;
    color: var(--ink-soft);
    font-style: italic;
    font-size: 11px;
}

.unit-pick-scroll {
    overflow-y: auto;
    flex: 1 1 auto;
    min-height: 0;
}

/* Division (collapsible group) */

details.division {
    border-bottom: 1px solid var(--rule);
}
details.division:last-of-type { border-bottom: none; }

.division-summary {
    list-style: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.55rem 0.8rem;
    background: var(--panel);
    color: var(--ink);
    user-select: none;
    /* Stick to the top of the scrollable picker as the user scrolls
       past each division. The next division's summary pushes the
       previous one out when it reaches the top - one header is
       visible at any time, which gives the user a constant sense of
       which division they're currently inside. */
    position: sticky;
    top: 0;
    z-index: 1;
    border-bottom: 1px solid var(--rule);
}
details.division:not(:last-of-type) .division-summary {
    /* Each header carries the bottom rule of its OWN section so the
       sticky stack visually separates from the rows below it. */
}

.division-summary::-webkit-details-marker { display: none; }

.division-summary:hover { background: var(--accent-soft); }

/* Division collapse caret. One glyph (â–¸) rotated 90Â° when the
   <details> opens, so the closed -> open transition animates
   instead of doing an instant glyph swap. Sized + weighted via
   the shared --ui-arrow-* tokens so it matches every other
   disclosure arrow in the app. Honours prefers-reduced-motion. */
.caret {
    display: inline-block;
    width: 1ch;
    font-size: var(--ui-arrow-size);
    font-weight: var(--ui-arrow-weight);
    color: var(--accent-hover);
    line-height: 1;
    margin-right: 0.15rem;
    transform-origin: 50% 50%;
    transition: transform 140ms ease;
}
.caret::before { content: '\25B8'; }
details[open] > .division-summary .caret { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) {
    .caret { transition: none; }
}

/* Division-level "tick all units in this group" checkbox. Wrapped
   in a label so the hit area is comfortable; click bubbling is
   stopped in JS so it doesn't also toggle the <summary> expand. */
.division-pick {
    display: inline-flex;
    align-items: center;
    margin-right: 0.15rem;
    cursor: pointer;
}
.division-pick-input {
    width: 12px;
    height: 12px;
    cursor: pointer;
    accent-color: var(--accent);
}

.division-name {
    font-weight: 600;
    flex: 1 1 auto;
}

.division-count {
    color: var(--ink-soft);
    font-size: 9px;
}

.division-body {
    padding: 0.25rem 0.4rem 0.4rem 1.5rem;
    background: var(--bg);
}

.division-actions {
    display: flex;
    gap: 0.75rem;
    padding: 0.25rem 0.6rem 0.5rem;
}

.link-action {
    background: none;
    border: none;
    color: var(--accent);
    cursor: pointer;
    font: inherit;
    font-size: 9px;
    padding: 0;
    text-decoration: underline;
}

.link-action:hover { color: var(--accent-hover); }

label.unit-pick {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.3rem 0.6rem;
    cursor: pointer;
    border-radius: 2px;
}

label.unit-pick:hover { background: var(--accent-soft); }

label.unit-pick input[type=checkbox] {
    width: 11px;
    height: 11px;
    accent-color: var(--accent);
    cursor: pointer;
    flex: 0 0 auto;
}

/* "All units" toggle row at the top of the Unit-search results -
   same visual treatment as the Grade picker's "All" row so the
   pattern reads as one shared component across filters. */
label.unit-pick.is-all {
    padding: 0.55rem 0.85rem;
    border-bottom: 1px solid var(--rule);
    font-weight: 600;
    background: var(--bg);
    border-radius: 0;
}

/* â”€â”€ Unit chips â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

.unit-chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin: 0.9rem 0 0;
    min-height: 1.75rem;
}

/* When no chips are selected the strip collapses - Staff grid already
   defaults to "all units in scope", so the row would otherwise be a
   confusing empty box. */
.unit-chips.is-empty {
    margin: 0;
    min-height: 0;
}

.unit-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.3rem 0.55rem 0.3rem 0.85rem;
    background: var(--accent-soft);
    color: var(--ink);
    border: 1px solid var(--rule);
    /* Square-rectangle button family (see design-brief). All clickable
       buttons across the product are sharp 2 px rectangles. */
    border-radius: 2px;
    font-size: 10px;
}

.unit-chip button {
    background: transparent;
    border: none;
    color: var(--ink-soft);
    cursor: pointer;
    font: inherit;
    padding: 0;
    width: 13px;
    height: 13px;
    line-height: 12px;
    border-radius: 50%;
    text-align: center;
    font-size: 11px;
}

.unit-chip button:hover {
    background: var(--accent);
    color: white;
}

.clear-all {
    background: transparent;
    border: 1px solid var(--rule);
    color: var(--ink-soft);
    cursor: pointer;
    font: inherit;
    font-size: 9px;
    padding: 0.2rem 0.7rem;
    /* Square-rectangle button family (see design-brief). All clickable
       buttons across the product are sharp 2 px rectangles. */
    border-radius: 2px;
    margin-left: auto;
}

.clear-all:hover {
    border-color: var(--accent);
    color: var(--accent);
}

/* â”€â”€ Balance grid â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

.empty-state {
    padding: 3rem 1.5rem;
    text-align: center;
    color: var(--ink-soft);
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    margin-top: 1rem;
    font-size: 11px;
}
/* G6 canonical empty-state children (rendered by _EmptyState.cshtml).
   Promotes the previously-inline `style="..."` paragraphs / buttons
   into named classes so every empty state across the app reads the
   same. Body floor is 10 px per Section 0 typography. */
.empty-state-title {
    margin: 0 0 0.4rem;
    color: var(--ink);
    font-size: 11px;
}
.empty-state-body {
    margin: 0 0 0.6rem;
}
.empty-state-hint {
    margin: 0.6rem 0 0;
    color: var(--ink-soft);
    font-size: 10px;
}
/* Section-action button at the 28 px floor per Section 0 (not the
   24 px lozenge family - this is a top-level CTA inside the empty
   state, not a status-bar action). Inherits .primary-button colour /
   border but overrides the bare 44 px padding to a more compact size
   that suits the empty-state context. */
.empty-state-cta {
    padding: 6px 14px;
    font-size: 11px;
    min-height: 28px;
    line-height: 1.2;
}
/* Optional debug <details> block used by the KPIs no-rows empty
   state to surface the SQL call that returned no rows. Lifts the
   four inline styles that used to live on details / summary / pre
   into one named class set. */
.empty-state-debug {
    margin-top: 0.6rem;
    text-align: left;
}
.empty-state-debug > summary {
    font-size: 10px;
    color: var(--ink-soft);
    cursor: pointer;
}
.empty-state-debug > pre {
    margin: 0.5rem 0 0;
    padding: 0.7rem 0.9rem;
    background: var(--bg);
    border: 1px solid var(--rule);
    border-radius: 2px;
    font-size: 10px;
    white-space: pre-wrap;
    word-break: break-all;
}

/* Animated spinner used while a grid (or any HTMX target) is loading. */
@keyframes spinner-spin {
    to { transform: rotate(360deg); }
}

.spinner {
    display: inline-block;
    width: 19px;
    height: 19px;
    border: 2px solid var(--rule);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spinner-spin 0.8s linear infinite;
}

/* Empty-state loader for the balances grid. Reserves the full grid
   height so the page doesn't reflow when the first render arrives -
   the toolbar / movers / status-bar all stay anchored. */
.loading-state {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    min-height: calc(100vh - 228px);
    color: var(--ink-soft);
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    margin-top: 1rem;
    font-size: 11px;
}
.loading-text { font-weight: 500; color: var(--ink); }

/* â”€â”€ Skeleton rows for in-flight grid loads â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Shown in place of the empty-grid placeholder while the first
   /balances/grid request is in flight; replaces the brief "blank
   panel then content" flicker with a stable shape that reads as
   'loading the right amount of data'. Honours reduced-motion. */
/* Skeleton renders as a real .balance-grid table so the column
   widths line up exactly with the eventual grid - .staff-col,
   .unit-col, .contract-col, .num and .trail-col all apply.
   Container fills viewport; inner table scrolls if there are
   more rows than fit. */
.skeleton-grid {
    margin-top: 1rem;
}
.skeleton-scroll {
    /* Fill the viewport between the toolbar and the activity
       strip. min-height anchors the panel even when there are
       fewer rows than needed; max-height clips overflow so the
       page doesn't grow taller than the viewport. */
    min-height: calc(100vh - 147px);
    max-height: calc(100vh - 147px);
}
.skeleton-balance-grid {
    /* Suppress the hover affordances that the real grid has -
       skeleton rows aren't interactive. */
    pointer-events: none;
    /* Fixed layout so the colgroup widths anchor every column,
       independent of what content sits inside each cell. Without
       this the empty .skeleton-cell divs would let auto layout
       collapse columns to their min-width. */
    table-layout: fixed;
    width: max-content;
    min-width: 100%;
}
.skeleton-balance-grid tbody tr:hover {
    background: inherit;
}
.skeleton-balance-grid td,
.skeleton-balance-grid th {
    /* Defeat any .balance-grid max-width caps that would otherwise
       fight the colgroup. With table-layout: fixed + explicit col
       widths these caps don't apply, but be explicit for clarity. */
    max-width: none;
    min-width: 0;
}
/* Reserve a strip for where the real column headers + the few
   sticky toolbar / status-bar rows will land when data arrives -
   same background as the real grid head, no animation, no
   content. Tall enough to cover the header row plus a couple of
   row heights so the animated body starts further down. */
.skeleton-head-strip th {
    background: var(--bg);
    border-bottom: 1px solid var(--rule);
    /* Was 74 px (reserving space for filters / top-movers above the body).
       Now matches the real .balance-grid thead at ~28 px so the skeleton's
       body rows ARE the data-loading affordance, not a tall blank strip. */
    height: 28px;
}

/* The animated bar inside each table cell. Gentle opacity
   pulse rather than the previous dramatic diagonal sweep -
   reads as 'data on its way' without competing for attention. */
.skeleton-cell {
    height: 9px;
    background: var(--accent);
    border-radius: 2px;
    animation: skeleton-pulse 1500ms ease-in-out infinite;
    opacity: 0.35;
}
/* Pad each skeleton row out so it matches the real grid's row
   height (the real .balance-grid td has ~0.5rem vertical padding;
   the empty skeleton cell would otherwise collapse to ~9px). */
.skeleton-balance-grid tbody td {
    padding-top: 0.55rem;
    padding-bottom: 0.55rem;
}
.skeleton-cell.is-head {
    height: 6px;
    background: var(--accent-soft);
    border: 1px solid var(--rule);
    width: 55%;
    animation: none;
    opacity: 1;
}
.skeleton-cell.is-narrow { width: 65%; }
.skeleton-cell.is-short  { width: 40%; }

/* Stagger the pulse across rows so it reads as a slow wave
   moving down the grid rather than every row pulsing in unison.
   Six tiers - rows cycle through them via :nth-child. */
.skeleton-balance-grid tbody tr:nth-child(6n+1) .skeleton-cell { animation-delay: 0ms; }
.skeleton-balance-grid tbody tr:nth-child(6n+2) .skeleton-cell { animation-delay: 120ms; }
.skeleton-balance-grid tbody tr:nth-child(6n+3) .skeleton-cell { animation-delay: 240ms; }
.skeleton-balance-grid tbody tr:nth-child(6n+4) .skeleton-cell { animation-delay: 360ms; }
.skeleton-balance-grid tbody tr:nth-child(6n+5) .skeleton-cell { animation-delay: 480ms; }
.skeleton-balance-grid tbody tr:nth-child(6n+0) .skeleton-cell { animation-delay: 600ms; }

@keyframes skeleton-pulse {
    0%, 100% { opacity: 0.18; }
    50%      { opacity: 0.6; }
}
@media (prefers-reduced-motion: reduce) {
    .skeleton-cell { animation: none; opacity: 0.35; }
}

/* KPI grid - a fail flag on the matching Pass column tints the
   parent metric's cell red. Same colour weight as the R/A/G red
   used elsewhere so the eye reads "outside target". */
.balance-grid.kpis-grid td.kpi-fail {
    color: var(--rag-red);
    font-weight: 600;
}
/* Compliance mode cells: large tick / cross glyphs centred in the
   cell instead of a numeric value. kpi-pass tints green; kpi-fail
   (defined above) tints red - same RAG palette as everywhere else. */
.balance-grid.kpis-grid td.kpi-compliance-cell {
    text-align: center;
    font-size: 12px;
    font-weight: 700;
}
.balance-grid.kpis-grid td.kpi-pass {
    color: var(--rag-green);
    font-weight: 600;
}

/* Compliance + Grouped: per-column "% pass" cells in the group-header
   row. Sit alongside the existing group toggle (colspan=3 across the
   WG icon cells) and pick up the same green / red tint when the
   group-wide pass rate is high / low. */
.balance-grid.kpis-grid tr.group-header-compliance td.kpi-compliance-pct {
    font-weight: 700;
    font-size: 10px;
    padding: 0.2rem 0.6rem;
    background: color-mix(in srgb, var(--accent) 8%, var(--panel));
}
/* Explicit horizontal scroll for the KPI grid - the proc returns
   a wide column set that always exceeds the viewport. The outer
   .balance-grid-container would otherwise clip the inner scroll
   bar (it has overflow: hidden for the corner-radius); override
   that for KPI specifically so the bar can be seen. */
/* KPI grid scroll fix. Was relying on :has() but that's not honoured
   reliably on the Trust-laptop Chromium versions in the field. The
   Razor view now stamps explicit kpis-grid-container + kpis-grid-scroll
   classes on the wrappers so these rules win on every browser. */
.balance-grid-container.kpis-grid-container {
    overflow: visible;
}
.balance-grid-scroll.kpis-grid-scroll {
    overflow-x: auto;
    overflow-y: auto;
    max-width: 100%;
    width: 100%;
    min-width: 0;
}
.balance-grid.kpis-grid {
    /* Per-column min-width so dense KPI tables stay readable rather
       than crushing to the narrowest possible width when the natural
       table width happens to fit the container. */
    min-width: max-content;
    width: max-content;
}
.balance-grid.kpis-grid th,
.balance-grid.kpis-grid td {
    white-space: nowrap;
    min-width: 74px;
}
/* Per-row action icons - Open / Email / Duties live in a single
   column with an sr-only header (no visible label) so the column
   width doesn't drift with header text length. The three icons sit
   inside a .row-actions flex container with explicit gap so they
   are equidistant. Same shape as the Staff grid's .drill-col. The
   .kpi-wg-col rule (kept for the Notes column heading) stays - the
   Notes column has its own .notes-cell width override.
   24 px per button x 3 = 72 px, gap:4 x 2 = 8 px, total 80 px - the
   cell sets 84 px so it has a hair of breathing room on each side. */
.balance-grid.kpis-grid th.kpi-wg-actions-col,
.balance-grid.kpis-grid td.kpi-wg-actions-col {
    width: 84px;
    min-width: 84px;
    max-width: 84px;
    padding: 3px 2px;
    text-align: center;
}
/* Kill the .row-icon-btn default 0.15rem lateral margin when the
   button sits inside a .row-actions flex container - spacing is the
   container's gap only, exactly like the Staff grid's drill-modal-btn.
   Without this override the gap + the margin compound and the three
   icons spread apart wider than the Staff pair. */
.row-actions .row-icon-btn { margin: 0; }
.balance-grid.kpis-grid th.kpi-wg-col,
.balance-grid.kpis-grid td.kpi-wg-col {
    padding: 3px 4px;
    text-align: center;
}
/* Category-row header (row 1 of the two-row KPI header). One cell
   per category, colspans the count of metric columns under it.
   Subtle alternating tint per category so the eye can keep its
   place when scrolling sideways through a wide table. */
.balance-grid.kpis-grid thead tr.kpi-category-row th {
    position: sticky;
    top: 0;
    background: var(--accent-soft);
    color: var(--ink);
    text-align: center;
    font-weight: 700;
    font-size: 9px;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    border-bottom: 1px solid var(--accent);
    padding: 3px 5px;
    z-index: 3;
}
/* KPI category-row tints alternate pale-blue / white per section
   so the eye reads "this is a new metric family" without the
   previous 14-pastel rainbow. The Ward Guardian leftmost cell
   keeps a slightly deeper blue so it reads as identity / always-
   on rather than a regular metric section. */
.balance-grid.kpis-grid .kpi-category-row .kpi-cat-ward-guardian {
    background: color-mix(in srgb, var(--accent) 18%, var(--panel));
}
.balance-grid.kpis-grid .kpi-category-row th.kpi-cat:nth-of-type(odd) {
    background: color-mix(in srgb, var(--accent) 10%, var(--panel));
}
.balance-grid.kpis-grid .kpi-category-row th.kpi-cat:nth-of-type(even) {
    background: var(--panel);
}
/* When every column under a band is hidden, suppress the band
   visually - background, border, padding all gone so the row-1 strip
   disappears alongside its columns. The th stays in the DOM with its
   colspan so subsequent row-1 cells still line up against the right
   row-2 columns. */
.balance-grid.kpis-grid .kpi-category-row th.kpi-cat.kpi-cat-empty {
    background: transparent !important;
    border-bottom: 0 !important;
    padding: 0 !important;
}
/* Row-2 column headers inherit the same odd / even tint as their
   row-1 category band so a column visually belongs to its group
   even after sideways scroll. The kpi-col-cat-* class is stamped
   server-side from the column -> CategoryGroups index (so the
   parity stays correct under any KPI Profile / column-hide state). */
.balance-grid.kpis-grid thead tr:not(.kpi-category-row) th.kpi-col-cat-odd {
    background: color-mix(in srgb, var(--accent) 10%, var(--panel));
}
.balance-grid.kpis-grid thead tr:not(.kpi-category-row) th.kpi-col-cat-even {
    background: var(--panel);
}
/* The row-2 sticky thead has to sit below the row-1 thead. The
   existing .balance-grid thead th rule sets top: 0; row-2 needs
   to push down by the row-1 height (~ 30 px including borders). */
.balance-grid.kpis-grid thead tr:not(.kpi-category-row) th {
    top: 20px;
    z-index: 2;
}
/* Category-row trigger - the button wrapping the kpi-cat-label so
   the whole category cell opens the shared #col-header-menu (Hide
   all / Show all for the columns under that category). Strips the
   browser-default button chrome so the cell still reads as a label;
   accent-soft on hover signals it's clickable. Full width so a
   click anywhere on the cell opens the menu - the kpi-cat th is
   text-align:center, so the button inherits that. */
.balance-grid.kpis-grid .kpi-category-row .kpi-cat-trigger {
    display: block;
    width: 100%;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: inherit;
    cursor: pointer;
    border-radius: 2px;
    transition: background 120ms ease;
}
.balance-grid.kpis-grid .kpi-category-row .kpi-cat-trigger:hover,
.balance-grid.kpis-grid .kpi-category-row .kpi-cat-trigger:focus-visible {
    background: color-mix(in srgb, var(--accent) 16%, transparent);
    outline: none;
}

/* Icon-only Share button - sized to match the surrounding parm
   inputs (46 px tall to align with .filter-input). Square footprint
   so the icon sits centered; no inner label. No auto-margin -
   Share is treated as a parm-row item with the standard gap; the
   Ask cluster (below) is what gets pushed to the far right. */
.share-view-button.share-view-iconic {
    height: 31px;
    width: 31px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/* Ask + Help pair wrapped together so they sit a few pixels apart
   while the cluster as a whole anchors to the far-right edge of
   the parm row. Help is rightmost; Ask sits immediately to its
   left. Override the row's larger gap by giving the cluster its
   own tight inner gap. */
.parm-row-ask-cluster {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    margin-left: auto;
}
.share-view-button.share-view-iconic .icon { width: 13px; height: 13px; }

/* Prev/next-month arrows flanking the As-of date picker. Match the
   date-popover-trigger height so the trio reads as one control. */
.date-nav-step {
    height: 31px;
    width: 24px;
    padding: 0;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    color: var(--ink-soft);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.date-nav-step:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.date-nav-step:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.date-nav-step .icon { width: 12px; height: 12px; }

/* Prev / next-staff navigation pair on the Balance History modal.
   Arrow + person icon side-by-side reads as "navigate to a person"
   without needing the verbose label that used to sit there. Same
   visual idiom as the .row-icon-btn family but a touch wider so
   the two SVGs sit comfortably. */
.history-nav-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    height: 24px;
    padding: 0 0.6rem;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    color: var(--ink-soft);
    cursor: pointer;
    text-decoration: none;
    transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.history-nav-btn:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.history-nav-btn:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.history-nav-btn .icon { width: 12px; height: 12px; }
.history-nav-btn .icon-sm { width: 9px; height: 9px; }
.history-nav-disabled { opacity: 0.35; cursor: not-allowed; pointer-events: none; }

/* KPI cell-click charts modal - distribution bar + per-unit
   trend with linear-regression forecast band. Modal sized so the
   whole thing fits in one viewport without a vertical scroller -
   the inner scroll container is forced to overflow-y: hidden so
   the chart panels share the available height by getting smaller,
   not by being clipped behind a scroll bar. */
/* Modal sized for typical desktop screens. Bars need ~18 px each
   to read distinctly; with ~50 bars that's 900 px of chart - fits
   inside 1200 px modal with room for axis labels. More units than
   that scroll horizontally inside the chart wrap (overflow-x:auto
   on .kpi-chart-canvas-wrap). The modal itself scrolls vertically
   if the content (distribution + trend + AI commentary) overflows
   the viewport - max-height keeps it inside one screen. */
.kpi-chart-modal { width: min(1200px, 96vw); max-height: 92vh; }
.kpi-chart-modal .staff-modal-scroll { overflow-y: auto; gap: 0.5rem; padding: 0.6rem 0.9rem 0.6rem; }
.kpi-chart-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    margin: 0 0 0.4rem;
    /* Reserve room on the right so the action chips + toggle never
       crash into the absolutely-positioned close X (top: 1.45rem,
       right: 1.65rem, width: 24 px = ~3 rem of clearance from the
       modal right edge). */
    padding-right: 3rem;
}
.kpi-chart-header-text { flex: 1 1 auto; min-width: 0; }
.kpi-chart-header-actions {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex: 0 0 auto;
}
.kpi-chart-title {
    margin: 0.1rem 0 0.15rem;
    font-size: 16px;
    font-weight: 700;
    color: var(--ink);
}
.kpi-chart-meta { margin: 0; font-size: 12px; color: var(--ink-soft); }
.kpi-chart-actions { display: flex; align-items: center; gap: 0.6rem; }
.kpi-chart-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 9px;
    color: var(--ink);
    cursor: pointer;
}
.kpi-chart-toggle input { width: 11px; height: 11px; accent-color: var(--accent); cursor: pointer; }

.kpi-chart-section {
    margin: 0 0 0.5rem;
    padding: 0.4rem 0.6rem 0.5rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
}
.kpi-chart-section-title {
    margin: 0 0 0.15rem;
    font-size: 13px;
    font-weight: 700;
    color: var(--ink);
}
/* Section header strip - title on the left, optional action button
   (e.g. the new "Show full history" toggle) on the right. Plain
   block layout when only the title is present. */
.kpi-chart-section-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.6rem;
}
.kpi-chart-section-head .kpi-chart-section-title { margin-bottom: 0; }
/* Insight line - one short human sentence above the regression
   sub-line on the trend chart ("Below target by 3.2 (currently
   91.8 vs 95). Trending up by 0.4 per roster."). Reads in the
   accent dark colour and a hair heavier than the sub-line so it
   anchors the eye on the trend-section header. */
.kpi-chart-section-insight {
    margin: 0 0 0.2rem;
    font-size: 12px;
    font-weight: 600;
    color: var(--ink);
}
.kpi-chart-section-sub {
    margin: 0 0 0.35rem;
    font-size: 11px;
    color: var(--ink-soft);
}
/* Canvas wrap. Height kept tight so the whole modal fits one
   viewport without a vertical scroller (G-rule: modals never grow
   their own scroll bar; if they don't fit they get smaller, not
   scrollier). overflow-x auto + overflow-y hidden lets the inline
   canvas grow wider than the modal (we set canvas.style.minWidth
   from JS based on bar count) so each bar keeps its slot even when
   50+ units are charted - the user scrolls sideways within the wrap
   instead of squashing bars to invisibility. */
.kpi-chart-canvas-wrap {
    position: relative;
    height: 200px;
    overflow-x: auto;
    overflow-y: hidden;
}

/* AI commentary paragraph inside the distribution modal. Borrows the
   accent-soft / left-rule look from the staff-grid .commentary-text
   family. 12 px line. Length safety is server-side (ClampToTwoSentences
   + 45-word cap); modal scrolls vertically if it overflows. */
.kpi-commentary-text {
    display: block;
    font-size: 12px;
    line-height: 1.45;
    color: var(--ink);
    border-left: 3px solid var(--accent);
    background: var(--accent-soft);
    padding: 0.5rem 0.7rem;
    border-radius: 2px;
    white-space: normal;
    margin: 0;
}
#commentary-section { padding: 0.3rem 0.55rem 0.4rem; }
#commentary-section .kpi-chart-section-title { margin: 0 0 0.25rem; }
/* Inline spinner that sits at the start of the commentary paragraph
   while the AI fetch is in flight. ~10 px accent ring with a single
   gap; rotates via the shared spin keyframe. prefers-reduced-motion
   freezes it at half-opacity so the dot still reads as "loading"
   without spinning. */
.kpi-commentary-spinner {
    display: inline-block;
    width: 10px;
    height: 10px;
    border: 1.5px solid color-mix(in srgb, var(--accent) 30%, transparent);
    border-top-color: var(--accent);
    border-radius: 50%;
    vertical-align: -1px;
    margin-right: 0.4rem;
    animation: barnacles-spin 0.9s linear infinite;
}
@keyframes barnacles-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .kpi-commentary-spinner { animation: none; opacity: 0.6; }
}

/* KPI grid - clickable numeric cells. The whole cell becomes
   clickable (data-metric attribute supplied by the view) so
   the chart modal opens with the right metric pre-selected. */
.balance-grid.kpis-grid td[data-metric] {
    cursor: pointer;
    transition: background 120ms ease;
}
.balance-grid.kpis-grid td[data-metric]:hover {
    background: color-mix(in srgb, var(--accent) 8%, var(--panel));
    outline: 1px solid var(--accent);
    outline-offset: -1px;
}

/* Clickable-column affordance: a tiny three-bar chart glyph in
   the top-right corner of every numeric column header in a
   distribution-cells grid. Visible always (not hover-only) so a
   user glance-scanning the strip can spot "these columns drill
   into a chart" without hovering. Pulses up to full opacity on
   hover so the cue gets a bit louder right when the user looks
   for it. Inline SVG data-URL avoids a sprite round-trip. */
table[data-distribution-cells] thead th.num { position: relative; }
table[data-distribution-cells] thead th.num::after {
    content: "";
    position: absolute;
    top: 3px;
    right: 3px;
    width: 9px;
    height: 9px;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237faf8c'><rect x='3' y='14' width='4' height='7' rx='0.5'/><rect x='10' y='9' width='4' height='12' rx='0.5'/><rect x='17' y='4' width='4' height='17' rx='0.5'/></svg>");
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
    opacity: 0.55;
    pointer-events: none;
    transition: opacity 140ms ease;
}
table[data-distribution-cells] thead th.num:hover::after { opacity: 1; }
/* The KPI grid hides this glyph on identity headers (orgUnitName
   etc) - those carry the .num class for right-align but aren't
   chartable. JS sets data-no-hide on the identity column; we
   reuse that signal so the affordance never appears where the
   click won't fire. */
table[data-distribution-cells] thead th.num[data-no-hide]::after { display: none; }

/* Non-KPI grids opt in via `data-distribution-cells` on the
   <table>. Every body `td.num` becomes clickable - the body-
   level delegate in _Layout.cshtml reads the column from the
   rendered DOM and pops the distribution chart. Subtotal /
   totals / group-header rows are excluded so the cursor
   matches the live click affordance. The KPI grid keeps its
   own rule above (data-metric); excluded here to avoid
   double-styling. */
table[data-distribution-cells] tbody tr:not(.group-header):not(.group-subtotal):not(.group-summary):not(.totals-row):not(.window-sentinel) td.num:not([data-metric]):not(.staff-adjustment-col):not(.kpi-compliance-pct) {
    cursor: pointer;
    transition: background 120ms ease;
}
table[data-distribution-cells] tbody tr:not(.group-header):not(.group-subtotal):not(.group-summary):not(.totals-row):not(.window-sentinel) td.num:not([data-metric]):not(.staff-adjustment-col):not(.kpi-compliance-pct):hover {
    background: color-mix(in srgb, var(--accent) 8%, var(--panel));
    outline: 1px solid var(--accent);
    outline-offset: -1px;
}

/* KPI grid status bar - multiple controls (Roster start picker,
   verbosity, Unit + KPI profile pickers, mode toggle, Export CSV).
   Single-row strip - if more controls than fit, the strip scrolls
   horizontally so the grid below never gets pushed down. Same
   pattern as .active-filters (see design-brief "Strips above the
   grid never grow vertically").
   display: flex set explicitly so we don't depend on the parent's
   :has(> .status-text) rule (browser-compat). gap matches the
   active-filters strip; align-items vertically centres labels +
   controls. */
.kpi-status-bar {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 0.6rem;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-color: color-mix(in srgb, var(--accent) 55%, var(--panel)) transparent;
    scrollbar-width: thin;
}
.kpi-status-bar > .status-text { flex: 1 1 auto; min-width: 0; }
.kpi-status-bar::-webkit-scrollbar { height: 4px; }
.kpi-status-bar::-webkit-scrollbar-track { background: transparent; }
.kpi-status-bar::-webkit-scrollbar-thumb {
    background: color-mix(in srgb, var(--accent) 55%, var(--panel));
    border-radius: 2px;
}
.kpi-status-control {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
}

/* KPI column-header target chip. Rendered below the column title
   (e.g. "Notice" on row 1, "â‰¥ 42 days" chip on row 2) from
   OB_GetMetricTargetsExtended via MetricTargetsService. Pastel
   accent-soft pill, 9 px, no shadow. Sits inside the
   .col-header-trigger button so a hover-darken on the header still
   wraps it naturally. When the trigger has a chip, the layout flips
   to a centred 2-row stack so the title row stays predictable
   regardless of column width - Greg's call after the first pass had
   chips wrap-onto-row-2 only on narrow columns, which read as
   inconsistent across the header strip. */
.balance-grid.kpis-grid .col-header-trigger {
    flex-wrap: wrap;
    row-gap: 2px;
}
.balance-grid.kpis-grid .col-header-trigger:has(.kpi-target-chip-wrap) {
    justify-content: center;
    align-items: center;
    text-align: center;
    line-height: 1.15;
    padding: 2px 4px;
    column-gap: 0.25rem;
}
.balance-grid.kpis-grid .col-header-trigger:has(.kpi-target-chip-wrap) {
    overflow: hidden;
}
/* KPI col-header title wrap. Wrapped via GridSortLabel.Header's
   labelWrapperClass parameter; takes the full flex row so the chip
   (also flex-basis 100%) reliably drops to its own line below.
   Two layouts:
   - NON-chip headers: 1 line by default, wraps up to 2 lines if
     the title is too long for the column. height is content-driven.
   - CHIP-bearing headers: reserve EXACTLY 2 line slots so the chip
     always lands on row 3, regardless of whether the title takes 1
     or 2 lines. line-clamp keeps the title clipped at 2 lines so
     even a long string doesn't push the chip further down. The
     `title=` attribute on the th carries the full text for hover. */
.balance-grid.kpis-grid .kpi-col-header-label {
    flex: 0 0 100%;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    text-align: center;
    line-height: 1.1;
    word-break: break-word;
    overflow: hidden;
}
.balance-grid.kpis-grid .col-header-trigger:has(.kpi-target-chip-wrap) .kpi-col-header-label {
    /* Reserve 2 lines of vertical space even when the title is
       short, so chip always lives on visual row 3. */
    min-height: calc(1.1em * 2);
}
.balance-grid.kpis-grid thead th.num .col-header-trigger:not(:has(.kpi-target-chip-wrap)) .kpi-col-header-label {
    text-align: right;
}
/* Chip-wrap is a full-width flex line that the chip sits inside,
   centred. The wrap takes 100% of the trigger row so the chip
   drops to its own line via flex-wrap; the chip itself stays
   content-width inside the wrap (so the pill background is only
   as wide as the text, not the whole column). */
.kpi-target-chip-wrap {
    flex: 0 0 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    margin: 0;
    padding: 0;
    line-height: 1;
}
.kpi-target-chip {
    display: inline-flex;
    align-items: center;
    background: transparent;
    color: var(--ink-soft);
    border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
    border-radius: 8px;
    padding: 1px 5px;
    font-size: 9px;
    font-weight: 600;
    letter-spacing: 0.01em;
    line-height: 1.3;
    white-space: nowrap;
    cursor: pointer;
    font-family: inherit;
    transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
/* Hover bumps the chip back to its earlier accent-soft fill so the
   tooltip target stays glanceable when the user reaches for it,
   even though the resting state is intentionally quiet. */
.kpi-target-chip:hover,
.kpi-target-chip:focus-visible {
    background: var(--accent-soft);
    border-color: color-mix(in srgb, var(--accent) 55%, transparent);
    color: var(--ink);
    outline: none;
}
/* G32: when a session override is active on the chip, paint it
   solid accent (same dark-blue the Ask a question primary button
   uses) with white text so the chip reads at glance distance from
   the canonical chips around it. The earlier 18% accent-mix was
   too quiet to spot mid-grid. */
.kpi-target-chip.is-override {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}
.kpi-target-chip.is-override:hover,
.kpi-target-chip.is-override:focus-visible {
    background: var(--accent-hover);
    border-color: var(--accent-hover);
    color: #fff;
}
@media (prefers-reduced-motion: reduce) {
    .kpi-target-chip { transition: none; }
}

/* G32: KPI target editor dialog. Reuses the col-filter-dialog frame
   so it inherits the centred-modal chrome + Esc-closes behaviour.
   The body just stacks rows of label + control with the unit
   suffix sitting inline so "â‰¥ 42 days" reads naturally. */
.kpi-target-editor-body {
    display: flex;
    flex-direction: column;
    gap: 12px;
    min-width: 320px;
}
.kpi-target-editor-metric-label {
    font-weight: 600;
    font-size: 13px;
    color: var(--ink);
    padding-bottom: 4px;
    border-bottom: 1px solid var(--border-soft);
}
.kpi-target-editor-row {
    display: flex;
    align-items: end;
    gap: 10px;
    flex-wrap: wrap;
}
.kpi-target-editor-fld {
    display: flex;
    flex-direction: column;
    gap: 4px;
    font-size: 11px;
    color: var(--ink-soft);
}
.kpi-target-editor-fld input,
.kpi-target-editor-fld select {
    font-size: 13px;
    padding: 4px 6px;
    border: 1px solid var(--border);
    border-radius: 2px;
    background: var(--surface);
    color: var(--ink);
    font-family: inherit;
}
.kpi-target-editor-fld input { width: 110px; }
.kpi-target-editor-fld select { min-width: 170px; }
.kpi-target-editor-unit {
    padding: 4px 0;
    font-size: 13px;
    color: var(--ink-soft);
    align-self: end;
}
.kpi-target-editor-hint-inline {
    font-size: 11px;
    color: var(--ink-soft);
    align-self: end;
    padding-bottom: 6px;
}
.kpi-target-editor-tolerance-row[hidden] { display: none; }
/* Revert row sits at the bottom of the editor form with the button
   left-aligned so the user reads it as a secondary action distinct
   from the toolbar's Save tick. Padded-top border separates it
   from the Totals-row picker above. */
.kpi-target-editor-revert-row {
    padding-top: 10px;
    margin-top: 4px;
    border-top: 1px solid var(--border-soft);
}
.kpi-target-editor-revert-row[hidden] { display: none; }
.kpi-target-editor-revert {
    padding: 6px 12px;
    font-size: 12px;
}
/* Right-aligned numeric headers - default justify-end for headers
   without a chip. Chip-bearing headers override back to centre via
   the :has(.kpi-target-chip) rule above. */
.balance-grid.kpis-grid thead th.num .col-header-trigger:not(:has(.kpi-target-chip)) {
    justify-content: flex-end;
}

/* G31 (revised): aggregator chrome retired - the always-visible Î£ /
   âŒ€ badge and the totals-cell click toggle were both replaced by a
   "Totals row" select inside the Change Target modal that opens on
   chip click. The per-user override still persists in
   ob_user_kpi_aggregators; only the entry-point changed.
   .kpi-agg-compliance stays because totals + subtotal cells still
   render a "% pass" string in the accent colour when a column is in
   compliance mode - it's a render-state class, not a click target. */
.balance-grid.kpis-grid .kpi-agg-compliance {
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}

/* KPI profile picker + Manage Profiles modal. Picker sized to
   match other status-bar controls (36 px) to avoid visual jump. */
.kpi-profile-picker {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
}
.kpi-profile-select {
    /* Pinned to 31 px to match .group-by-status and every other
       in-grid-status-bar select - one chrome height across the row
       so the dropdowns read as a single row of controls. */
    height: 31px;
    min-width: 134px;
    padding: 0 0.5rem;
    font-size: 10px;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    color: var(--ink);
}
.kpi-profile-manage-btn {
    height: 24px;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0 0.7rem;
    font-size: 9px;
}

.kpi-profiles-modal { width: min(590px, 96vw); }
.kpi-profiles-grid {
    display: grid;
    grid-template-columns: 161px 1fr;
    gap: 1rem;
    margin-top: 0.6rem;
}
.kpi-profiles-list {
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.6rem;
    background: var(--bg);
}
.kpi-profiles-new-btn {
    width: 100%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    margin-bottom: 0.6rem;
}
.kpi-profiles-section-header {
    font-size: 9px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--ink-soft);
    margin: 0.7rem 0 0.3rem;
}
.kpi-profiles-listitems { display: flex; flex-direction: column; gap: 0.2rem; }
.kpi-profiles-list-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.4rem;
    text-align: left;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.5rem 0.7rem;
    font-size: 10px;
    font-weight: 500;
    color: var(--ink);
    cursor: pointer;
    transition: background 140ms ease, border-color 140ms ease;
}
.kpi-profiles-list-item:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--ink); }
/* Profile row with the Oceansblue-only Copy OrgUnitIds icon: the
   list-item button stretches; the export icon sits flush to the right
   with the same height for a clean grid edge. */
.kpi-profiles-list-row { display: flex; align-items: stretch; gap: 0.25rem; }
.kpi-profiles-list-row .kpi-profiles-list-item { flex: 1 1 auto; }
.unit-profile-export-btn { align-self: stretch; }
.kpi-profiles-trust-tag {
    background: var(--accent);
    color: white;
    font-size: 7px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    /* Square-rectangle button family (see design-brief). All clickable
       buttons across the product are sharp 2 px rectangles. */
    border-radius: 2px;
    padding: 1px 5px;
}

.kpi-profiles-editor {
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 1rem;
    background: var(--panel);
}
.kpi-profiles-visibility {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}
.kpi-profiles-radio {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 10px;
    color: var(--ink);
    cursor: pointer;
}
.kpi-profiles-radio input[type=radio] { width: 12px; height: 12px; accent-color: var(--accent); }
.kpi-profiles-categories {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.4rem 0.9rem;
}
.kpi-profiles-cat-check {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 10px;
    color: var(--ink);
    cursor: pointer;
}
.kpi-profiles-cat-check input[type=checkbox] {
    width: 12px;
    height: 12px;
    accent-color: var(--accent);
    cursor: pointer;
}
.kpi-profiles-cat-actions {
    margin-top: 0.5rem;
    font-size: 9px;
    color: var(--ink-soft);
}
.link-action {
    background: transparent;
    border: none;
    color: var(--accent);
    font: inherit;
    font-size: 9px;
    cursor: pointer;
    padding: 0;
}
.link-action:hover { text-decoration: underline; }

/* Unit profile picker - sized like other toolbar parm inputs
   (46 px) since it sits in the filter-row, not a status bar. */
.unit-profile-picker .kpi-profile-select { height: 31px; min-width: 121px; }
.unit-profile-picker .kpi-profile-manage-btn { height: 31px; }
.unit-profile-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    padding: 0.4rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--bg);
    min-height: 29px;
    max-height: 134px;
    overflow-y: auto;
}
/* Roster-end picker in the main toolbar (KPI mode only). Sits in
   its own .filter-group so it flows with the rest of the parm row;
   the inner label keeps a 'Roster end' caption next to the input. */
.kpi-roster-end-toolbar {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    font-size: 10px;
    color: var(--ink-soft);
}
.kpi-roster-end-toolbar-label { font-weight: 600; color: var(--ink); }
.kpi-roster-end-input {
    /* WG Verbosity + Show (Data/Compliance) selects. Pinned to
       31 px to match every other in-grid-status-bar dropdown. */
    height: 31px;
    padding: 0 0.5rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    color: var(--ink);
    font: inherit;
    font-size: 10px;
    cursor: pointer;
    transition: border-color 140ms ease, box-shadow 140ms ease;
}
.kpi-roster-end-input:hover { border-color: var(--accent); }
.kpi-roster-end-input:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

/* â”€â”€ Welcome tour â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Seven-step popover walk-through that auto-runs once on a user's
   first sign-in. .tour-overlay is the root container (rendered
   hidden in _Layout.cshtml). .tour-backdrop is the soft scrim;
   .tour-card is the floating step bubble positioned by JS relative
   to the target element named in TOUR_STEPS[i].selector. */
.tour-overlay {
    position: fixed;
    inset: 0;
    z-index: 4000;
}
.tour-overlay[hidden] { display: none; }
.tour-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(16, 42, 67, 0.32);
    backdrop-filter: blur(1px);
}
.tour-card {
    position: fixed;
    width: min(255px, calc(100vw - 21px));
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 5px;
    box-shadow: 0 8px 21px rgba(16, 42, 67, 0.28);
    padding: 1.1rem 1.25rem 1rem;
    z-index: 1;
}
.tour-step-pill {
    display: inline-block;
    font-size: 8px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--accent);
    background: var(--accent-soft);
    /* Square-rectangle button family (see design-brief). All clickable
       buttons across the product are sharp 2 px rectangles. */
    border-radius: 2px;
    padding: 2px 7px;
    margin-bottom: 0.55rem;
}
.tour-title {
    margin: 0 0 0.5rem;
    font-size: 13px;
    font-weight: 700;
    color: var(--ink);
}
.tour-body {
    margin: 0 0 0.9rem;
    font-size: 11px;
    line-height: 1.45;
    color: var(--ink);
}
.tour-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.tour-spacer { flex: 1 1 auto; }
.tour-skip {
    background: transparent;
    border: none;
    color: var(--ink-soft);
    font: inherit;
    font-size: 10px;
    cursor: pointer;
    padding: 4px 3px;
}
.tour-skip:hover { color: var(--accent); text-decoration: underline; }
.tour-back,
.tour-next {
    min-height: 24px;
    padding: 0 1rem;
    font-size: 10px;
}
.tour-back[disabled] { opacity: 0.45; cursor: not-allowed; }
/* Highlight ring on the active target. Sits above other chrome
   without obstructing it; the slight outline + pulsing accent
   draws the eye without yelling. */
.tour-highlight {
    position: relative;
    z-index: 4001;
    box-shadow: 0 0 0 3px var(--accent), 0 0 0 5px rgba(111, 149, 187, 0.35);
    border-radius: 2px;
    transition: box-shadow 200ms ease;
}
@media (prefers-reduced-motion: reduce) {
    .tour-highlight { transition: none; }
}

/* Fade-in for the balance grid when its swap completes. HTMX adds
   the swap-in animation via #balance-grid, but we want the inner
   grid container to fade gently too. */
.balance-grid-container { animation: balance-fade-in 200ms ease; }
@keyframes balance-fade-in {
    from { opacity: 0; transform: translateY(1px); }
    to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
    .balance-grid-container { animation: none; }
}

/* ESR Division column - compact by default but the user can
   drag it wider to reveal the full name. Long division names
   ("Surgery, Critical Care and Anaesthetics") ellipsis at the
   default width; native title= on the cell carries the full
   text on hover. width: <px> is the preferred-width nudge
   auto-layout honours; max-width is a soft ceiling on the
   natural pass that the manual resizer can override via an
   explicit inline width. */
.balance-grid .division-col {
    width: 150px;
    min-width: 100px;
    max-width: 280px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Notes column - AI commentary. Given more headroom than the
   other cells because populated commentary runs multi-paragraph;
   the column is the natural place for the eye to rest at the
   end of the row. Bumped up to absorb the width released by the
   narrower Staff and Unit columns. */
.notes-cell {
    min-width: 429px;
    max-width: 549px;
}

/* Staff column - compact by default, but grows when the user
   drags the column edge. width: <px> is the preferred-width
   nudge auto-layout honours (max-width alone leaves the column
   at the longest-name width because nowrap content has no
   wrap point). The inner drill-link uses max-width: 100% so
   it expands with whatever width the column ends up at - a
   manual resize reveals more of the name instead of staying
   pegged to a hard px cap. Names ellipsis when narrower than
   the content; native title= on the link gives the full name
   on hover, Staff Details modal one click away. */
.balance-grid .staff-col {
    width: 110px;
    min-width: 60px;
    max-width: 220px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.balance-grid .staff-col .drill-link {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: middle;
}

/* Unit column - compact by default, but grows when the user
   drags the column edge. width: <px> is the preferred-width
   nudge auto-layout honours; max-width is a soft ceiling on
   the natural-fit pass but a manual column-resize via the JS
   resizer (barnaclesFitColumns / drag) overrides it via an
   explicit width style. The inner drill-link uses max-width:
   100% so it tracks whatever width the column ends up at -
   dragging wider reveals more of the unit name instead of
   leaving it pegged to a hard px cap. WG shield + pay-
   enhancement / leave-only flag icons stay inline. */
.balance-grid .unit-col {
    width: 130px;
    min-width: 80px;
    max-width: 260px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.balance-grid .unit-col .drill-link {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: middle;
}

/* Contract column - shortened header ('Contract') so the column
   is much narrower than the old 'CONTRACTED HRS' width. Single
   tabular number, right-aligned. */
.balance-grid .contract-col {
    max-width: 54px;
    min-width: 47px;
}

/* Per-row "Generate notes" lozenge sitting in the Staff grid's Notes
   column. Same shape as the canonical .secondary-button.toolbar-action
   family (G12) so the row-level affordance reads as the same family
   as the status-bar lozenges above it: 24 px, accent text on rule
   border, accent-soft hover wash. Stays as its own class because
   the row-level call sites are wired by data-href + JS handlers, not
   straight <button class="secondary-button toolbar-action"> markup. */
.generate-notes {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    height: 24px;
    background: var(--panel);
    border: 1px solid var(--rule);
    color: var(--accent);
    cursor: pointer;
    font-size: 9px;
    font-weight: 600;
    padding: 0 0.95rem;
    border-radius: 2px;
    transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}

.generate-notes:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
}
.generate-notes:disabled { cursor: progress; opacity: 0.85; }

/* Loading swap: HTMX adds .htmx-request to the button while the
   commentary request is in flight, so the user sees an explicit
   "Analysing..." label + spinner instead of the silent press
   the previous Generate flow had. */
/* Slot containing either the sparkle icon (idle) or the spinner
   (loading). Same width either way so the button doesn't reflow
   on click. */
.gen-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 9px;
    height: 9px;
    flex: 0 0 auto;
}
.generate-notes .gen-icon-loading { display: none; }
.generate-notes.htmx-request .gen-icon-idle { display: none; }
.generate-notes.htmx-request .gen-icon-loading { display: inline-block; }

/* Loading state swaps the "Analyse" label for "Analysing..." text.
   The spinner itself lives in .gen-icon (handled above), so
   .gen-loading carries text only - no inline spinner here. Hidden
   until HTMX adds .htmx-request to the button so the button doesn't
   render "Analysing..." from first paint. */
.generate-notes .gen-loading { display: none; }
.generate-notes.htmx-request .gen-label { display: none; }
.generate-notes.htmx-request .gen-loading { display: inline; }
.gen-loading-spinner {
    display: inline-block;
    width: 8px;
    height: 8px;
    border: 1px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    animation: spinner-spin 0.8s linear infinite;
    flex: 0 0 auto;
}

.commentary-placeholder {
    color: var(--ink-soft);
    font-style: italic;
    font-size: 9px;
}

/* Rendered AI commentary cell - small body text, soft accent bar on
   the left so the eye picks it out as a meta annotation rather than
   another grid value. White-space pre-wrap so single-paragraph text
   wraps naturally inside the cell. */
.commentary-text {
    display: block;
    font-size: 9px;
    line-height: 1.4;
    color: var(--ink);
    border-left: 2px solid var(--accent);
    background: var(--accent-soft);
    padding: 0.45rem 0.6rem;
    border-radius: 1px;
    /* One flowing paragraph - any newlines from the AI collapse to
       spaces so the text wraps naturally within the column width. */
    white-space: normal;
}

/* Commentary wrapper holds the prose + a small actions row beneath.
   Default state shows the prose with a labeled "Hide Notes" pill in
   the actions row; the .is-collapsed state (or .all-notes-hidden on
   the grid) hides both and reveals a "Show Notes" pill the user can
   click to expand the note again. Both states are reached without
   touching the cached commentary, so re-opening costs nothing. */
.commentary-wrap {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.commentary-actions {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 4px;
}
.commentary-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    border: 1px solid var(--rule);
    background: var(--panel);
    color: var(--ink-soft);
    cursor: pointer;
    font: inherit;
    font-size: 9px;
    padding: 0.2rem 0.65rem;
    /* Square-rectangle button family (see design-brief). All clickable
       buttons across the product are sharp 2 px rectangles. */
    border-radius: 2px;
    line-height: 1;
    transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.commentary-toggle:hover {
    background: var(--accent-soft);
    color: var(--accent);
    border-color: var(--accent);
}
.commentary-toggle-show {
    display: none;
    align-self: flex-start;
    color: var(--accent);
}

/* Collapsed state - per-row (.is-collapsed) or grid-wide
   (.balance-grid.all-notes-hidden). Same effect either way: hide
   the prose + the actions row, reveal the Show Notes pill. */
.commentary-wrap.is-collapsed .commentary-text,
.commentary-wrap.is-collapsed .commentary-actions,
.balance-grid.all-notes-hidden .commentary-wrap .commentary-text,
.balance-grid.all-notes-hidden .commentary-wrap .commentary-actions {
    display: none;
}
.commentary-wrap.is-collapsed .commentary-toggle-show,
.balance-grid.all-notes-hidden .commentary-wrap .commentary-toggle-show {
    display: inline-flex;
}

/* Open link in the Staff grid â†’ goes to /staff/{personId}. */
a.drill-link {
    text-decoration: none;
}

/* Plain status-bar - text-only. Block layout so the inline content
   (a mix of <strong> elements and text runs from the Razor template)
   reads as one paragraph, not as scattered flex items. */
.status-bar {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    padding: 0.5rem 0.7rem;
    color: var(--ink-soft);
    font-size: 10px;
    border: 1px solid var(--rule);
    background: var(--panel);
    border-radius: 4px 4px 0 0;
    border-bottom: none;
    margin-top: 1rem;
    line-height: 1.4;
}

/* Status-bar variant - when the template wraps the text in
   .status-text alongside another action (e.g. the Staff grid's
   "+ Generate all" button), switch to a two-column flex layout so
   the button anchors right. */
.status-bar:has(> .status-text) {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}
.status-text { flex: 1 1 auto; }

.status-bar strong { color: var(--ink); font-weight: 600; }

/* .status-bar-export + .generate-all-btn deleted in the legacy-CSS
   tidy sweep (post-G12). Status-bar action lozenges now use the
   canonical .secondary-button.toolbar-action / .primary-button.toolbar-action
   family per Section 0 table 4 + G12. */

.balance-grid-container {
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 0 0 4px 4px;
    /* overflow: visible (NOT hidden) so the inner scroll wrapper's
       horizontal scrollbar isn't clipped at the bottom corner. The
       corner-radius is preserved via the inner wrappers; never let
       a wide grid lose columns off the right edge invisibly. */
    overflow: visible;
}

/* "Matched staff" chips on /sign-offs rows - shown when the
   current search hit a captured staff member rather than the
   unit name. Compact pill list with hover-title for the full
   name; '+N more' for events that matched lots of people. */
.signoff-matched-cell {
    max-width: 174px;
    line-height: 1.6;
}
.signoff-matched-chip {
    display: inline-block;
    margin: 0 0.25rem 0.15rem 0;
    padding: 0.05rem 0.55rem;
    border: 1px solid var(--accent);
    background: var(--accent-soft);
    color: var(--ink);
    /* Square-rectangle button family (see design-brief). All clickable
       buttons across the product are sharp 2 px rectangles. */
    border-radius: 2px;
    font-size: 8px;
    white-space: nowrap;
}
.signoff-matched-more {
    display: inline-block;
    margin-left: 0.25rem;
    font-size: 8px;
    color: var(--ink-soft);
    font-style: italic;
}

/* Retraction (Unsigned) rows on /sign-offs - styled subtly so the
   row is clearly NOT a fresh sign-off but stays scannable. The
   small chip in the actions column flags it as a retraction. */
.balance-grid tr.signoff-unsigned td {
    color: var(--ink-soft);
    background: #fbfcfe;
}
.balance-grid tr.signoff-unsigned td .drill-link {
    text-decoration: line-through;
    text-decoration-color: var(--ink-soft);
}
/* .signoff-unsigned-tag replaced by canonical .audit-badge.audit-badge-warn
   - one text-badge family across the app. */

/* Window-slide sentinels - the loading-placeholder rows at the
   top and bottom of the rendered 2000-row slice. IntersectionObserver
   fires when one scrolls into view; htmx-morph swaps the grid with
   the next/prev window. Visual is a soft accent-soft strip + spinner
   so the user sees the load happen rather than wonder why scroll
   suddenly snapped to a new page. */
.window-sentinel-cell {
    text-align: center;
    padding: 0.55rem 0.85rem;
    color: var(--ink-soft);
    background: var(--accent-soft);
    font-size: 9px;
    font-style: italic;
}
.window-sentinel-spinner {
    display: inline-block;
    width: 7px;
    height: 7px;
    margin-right: 0.4rem;
    border: 1px solid var(--accent);
    border-top-color: transparent;
    border-radius: 50%;
    animation: spinner-spin 0.8s linear infinite;
    vertical-align: -1px;
}
@media (prefers-reduced-motion: reduce) {
    .window-sentinel-spinner { animation: none; }
}

/* Scroll budget subtracts --activity-strip-h so the sticky <tfoot>
   never slides under the fixed activity strip at the foot of the
   viewport. .top-movers variants reserve extra space when the
   strip is rendered above the grid.
   Horizontal scroll: when the table's natural content width
   exceeds the container (narrow Trust laptops, many columns,
   long unit names), the scroll-wrapper grows a horizontal
   scrollbar instead of crushing columns below their min-width.
   Sticky thead + tfoot still work because position:sticky is
   per-axis. */
.balance-grid-scroll {
    /* overflow-x: scroll (not auto) so the horizontal scrollbar is
       always visible at the bottom of the grid, making it obvious
       that wide grids continue past the right edge. The design-brief
       "every grid scrolls horizontally" rule plus user-reported
       confusion when the bar appears only on overflow. */
    overflow-x: scroll;
    overflow-y: auto;
    /* Pin the scroll wrapper to its parent's width so a wide table
       inside it triggers overflow-x. Without these, the wrapper
       grows to fit the table's natural width (.balance-grid's
       min-width: max-content) and overflow-x never fires - so wide
       grids silently lose columns off the right edge.
       Codified in docs/design-brief.md "every grid scrolls
       horizontally" rule. */
    max-width: 100%;
    width: 100%;
    min-width: 0;
    /* Tightened: was 340 px deduction which left ~70 px of empty
       space between the grid's last row and the bottom-fixed
       activity strip. 270 px matches the actual chrome above the
       grid (tab row + toolbar + status bar + chips strip).
       Using `height` (not `max-height`) so the wrapper always
       extends to the available vertical space - if the table
       is short, the wrapper is still tall and the horizontal
       scrollbar reliably sits at the foot of the viewport
       instead of floating somewhere mid-page. */
    height: calc(100vh - 181px - var(--activity-strip-h) - 16px);
    min-height: 134px;
    /* Pale-blue scrollbars matched between Firefox (scrollbar-*) and
       WebKit (::-webkit-scrollbar-* below). The track has its own
       pale fill so the scroll lane is visible at the bottom of the
       grid even when there's no thumb (content fits in viewport). */
    scrollbar-color: color-mix(in srgb, var(--accent) 55%, var(--panel)) var(--accent-soft);
    scrollbar-width: auto;
    scrollbar-gutter: stable;
    /* No bottom margin - the wrapper's scrollbar lane rests flush
       on top of the fixed .activity-panel below it. The activity
       strip's upward box-shadow has been removed so it no longer
       paints over the scrollbar; a single 1 px border-top on the
       strip is the visual separator. */
}
/* Capped-height modifiers for in-modal / in-tile grids that should
   stop at a fixed height instead of filling the viewport. Replaces
   the previous one-off inline `style="max-height: NNNpx;"` declarations
   scattered across approvals / sign-off modal partials. Four canonical
   caps - if a new caller needs a fifth, raise it to Section 0 first. */
.balance-grid-scroll--cap-xs { height: auto; max-height: 160px; min-height: 0; }
.balance-grid-scroll--cap-sm { height: auto; max-height: 200px; min-height: 0; }
.balance-grid-scroll--cap-md { height: auto; max-height: 320px; min-height: 0; }
.balance-grid-scroll--cap-lg { height: auto; max-height: 480px; min-height: 0; }
/* Viewport-relative cap for in-modal grids that should scale with the
   browser window (taller monitors get a longer grid). 60 vh leaves
   ~40 % of the viewport for the modal chrome around it. */
.balance-grid-scroll--cap-vh { height: auto; max-height: 60vh; min-height: 0; }

.balance-grid-scroll::-webkit-scrollbar {
    /* Forced visible by !important - some browser configurations
       (Edge "Show scrollbars only when scrolling" / Windows
       "auto-hide" setting) collapse the standard 14 px scrollbar.
       18 px + the deeper colour pair makes the lane obvious. */
    width: 18px !important;
    height: 18px !important;
    background: var(--accent-soft) !important;
}
.balance-grid-scroll::-webkit-scrollbar-track {
    background: var(--accent-soft) !important;
    border-top: 1px solid var(--accent) !important;
    box-shadow: inset 0 1px 0 var(--rule);
}
.balance-grid-scroll::-webkit-scrollbar-thumb {
    background: var(--accent) !important;
    border-radius: 2px;
    border: 2px solid var(--accent-soft);
    min-width: 40px;
    min-height: 40px;
}
.balance-grid-scroll::-webkit-scrollbar-thumb:hover {
    background: var(--accent);
}
.balance-grid-scroll::-webkit-scrollbar-corner {
    background: var(--accent-soft);
}

/* Cached AI commentary rendered inline on the Staff grid - means
   Generate-all-Notes skips the row entirely. The â†» regen icon sits
   alongside Hide Notes in the actions row so a fresh and a cached
   cell read identically; only the small â†» marks the cached path. */
.commentary-regen {
    flex: 0 0 auto;
    background: transparent;
    border: 1px solid var(--rule);
    color: var(--ink-soft);
    width: 18px;
    height: 18px;
    border-radius: 50%;
    cursor: pointer;
    font-size: 11px;
    line-height: 1;
    transition: background 140ms ease, color 140ms ease, border-color 140ms ease;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.commentary-regen:hover {
    background: var(--accent-soft);
    color: var(--accent);
    border-color: var(--accent);
}

/* Stop button that interrupts a Generate-all run. Red tint so it's
   visibly distinct from the green-ish Generate / Hide buttons in
   the same pill group. Hidden until a Generate-all is in flight. */
.mode-toggle-button-stop {
    color: var(--rag-red, #b91c1c);
}
.mode-toggle-button-stop:hover {
    background: var(--rag-red-soft-bg, #fdecea);
    color: var(--rag-red, #8a1f1f);
}
.mode-toggle-button-stop[hidden] { display: none; }

/* Per-unit pattern badges. Same visual recipe as .wg-badge so a
   row reads as a small horizontal strip of glyphs after the unit
   name (shield, coin, moon). Two flags surface a unit's 90-day
   operational signature:
   - .unit-flag-payenh   (coin) : >60% of worked shifts in unsocial hours
   - .unit-flag-leaveonly (moon) : >70% of rows are NERG (sickness / leave)
   No background or border by default; the icon stroke colour
   carries the meaning. Hover gets a soft pastel wash so the
   button affordance stays obvious without competing with the
   row text. */
.unit-flag {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    margin-left: 0.3rem;
    padding: 0;
    background: transparent;
    border: 0;
    border-radius: 50%;
    vertical-align: -3px;
    line-height: 1;
    cursor: pointer;
    font-family: inherit;
    transition: background-color 140ms ease, filter 140ms ease;
}
.unit-flag .icon {
    width: 12px;
    height: 12px;
    stroke-width: 2;
}
.unit-flag:hover { filter: brightness(0.9); }

/* Patterns popover trigger - sits on the toolbar next to the
   Units / Grade / Band pickers. G11 converted the popover itself
   from <details> to <dialog>, so the wrapper / popover / footer
   classes are gone; only the trigger button + dot live here.
   Padding + font + focus ring match the picker-trigger family
   per Section 0 Table 2 (consistency-sweep 2026-06-09). */
.patterns-trigger {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    height: 31px;
    padding: 0.55rem 0.8rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    color: var(--ink-soft);
    cursor: pointer;
    font: inherit;
    transition: background 140ms ease, border-color 140ms ease, color 140ms ease, box-shadow 140ms ease;
}
.patterns-trigger:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--ink);
}
.patterns-trigger:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.patterns-trigger.is-active {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--ink);
    font-weight: 600;
}
.patterns-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--accent);
    display: none;
}
.patterns-dot.is-active {
    display: inline-block;
}
.patterns-section { margin: 0 0 0.7rem 0; }
.patterns-section:last-child { margin-bottom: 0; }
.patterns-section-label {
    font-weight: 600;
    font-size: 11px;
    color: var(--ink);
    margin-bottom: 0.15rem;
}
.patterns-section-hint {
    color: var(--ink-soft);
    font-weight: 400;
    font-size: 9px;
}
.patterns-section-sub {
    font-size: 9px;
    color: var(--ink-soft);
    margin-bottom: 0.35rem;
    line-height: 1.3;
}
.patterns-radio-row {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
    font-size: 10px;
}
.patterns-radio-row label {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    cursor: pointer;
    color: var(--ink);
}
.patterns-radio-row input[type="radio"] {
    accent-color: var(--accent);
    cursor: pointer;
}
/* Amber coin = pay-enhancement. Reuses --rag-amber for stroke
   so the colour story stays inside the variance palette. */
.unit-flag-payenh { color: var(--rag-amber); }
.unit-flag-payenh:hover { background: #fef3c7; }

/* Violet moon = leave-only. No matching palette token (NERG /
   away isn't an R/A/G state) so the hex pair stays inline. */
.unit-flag-leaveonly { color: #6d28d9; }
.unit-flag-leaveonly:hover { background: #ede9fe; }

/* Small leading glyph on each Patterns dropdown section so the
   user sees the same icon in the popover that they see on the
   row. Inline-flex + tiny inline-block size mirrors .wg-badge so
   the icons don't disturb the section-label baseline. */
.patterns-section-glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 14px;
    height: 14px;
    margin-right: 0.25rem;
    vertical-align: -2px;
}
.patterns-section-glyph .icon {
    width: 12px;
    height: 12px;
    stroke-width: 2;
}
.patterns-section-glyph-payenh { color: var(--rag-amber); }
.patterns-section-glyph-leaveonly { color: #6d28d9; }
.patterns-section-glyph-wg { color: var(--rag-green-strong); }
.patterns-section-glyph-signoff { color: var(--accent); }

/* Footer for the Unit Type popover - two icon buttons (cross +
   tick) sharing the .unit-picker-action chrome with the Units /
   Band / Grade pickers. The tick stays disabled until a radio
   differs from the snapshot taken on popover open; the greyed-out
   state is the user's "you've already applied this" cue and lives
   on .unit-picker-apply:disabled so every apply tick in the app
   greys the same way. */
.top-movers ~ .balance-grid-container .balance-grid-scroll {
    max-height: calc(100vh - 208px - var(--activity-strip-h));
}
.top-movers[open] ~ .balance-grid-container .balance-grid-scroll {
    max-height: calc(100vh - 288px - var(--activity-strip-h));
}

/* Transient class applied to a .balance-grid while barnaclesFitColumns
   is measuring natural column widths. Releases per-cell width caps
   (max-width, overflow:hidden, ellipsis, wrap) so each th's
   getBoundingClientRect width reflects the widest single-line cell.
   The class is removed before the next frame - it is never the
   long-lived steady-state look of the grid. */
.balance-grid.grid-measuring th,
.balance-grid.grid-measuring td {
    max-width: none !important;
    min-width: 0 !important;
    width: auto !important;
    overflow: visible !important;
    text-overflow: clip !important;
    white-space: nowrap !important;
}

/* Grids inside modals + detail panels don't have the rich status-bar
   the main grid tabs carry, so the Fit-columns affordance gets a tiny
   leftmost toolbar of its own. .grid-block is display:contents so the
   data-grid-key wrapper has no layout effect; .grid-mini-toolbar is a
   one-row flex strip that sits directly above the grid container. */
.grid-block { display: contents; }
.grid-mini-toolbar {
    display: flex;
    align-items: center;
    margin: 0.4rem 0 0.3rem;
}

.balance-grid {
    /* width: max-content means the table is its natural width -
       columns size to their widest content, and the table anchors
       to the left of .balance-grid-scroll instead of stretching to
       fill the wrapper. min-width: 100% was the previous default
       (stretch to fill when content was narrow) but stretched-out
       cells made narrow grids harder to scan; left-anchored natural
       width reads as one compact block instead of widely-spaced
       cells with empty padding to the right. Horizontal-scroll
       behaviour for wide grids is unchanged (overflow-x: scroll on
       the scroll wrapper still fires when content exceeds viewport). */
    width: max-content;
    border-collapse: collapse;
    /* Bumped body font so Ward Managers on small / low-res screens
       can scan rows without leaning in. Headers + sticky-totals
       inherit unless they override below. */
    font-size: 11px;
}

.balance-grid thead th {
    position: sticky;
    top: 0;
    background: var(--panel);
    border-bottom: 1px solid var(--rule);
    text-align: left;
    /* Padding lives on the <th> itself, not on the inner sort-link
       button, so every header cell (sortable or not, button or
       plain text) shares the same chrome. Trimmed in lockstep with
       the body cell padding so header and body share the same
       column width budget. */
    padding: 0.2rem 0.55rem;
    color: var(--ink);
    font-weight: 700;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    z-index: 1;
}
/* Every visible child of a header inherits the bold-uppercase
   treatment so static labels (Trend, Notes), button-wrapped
   sort-links, and bare text-node children render identically. */
.balance-grid thead th,
.balance-grid thead th * {
    font-weight: 700;
}

/* Drag handle on the right edge of each grid header cell. The
   shaded full-height strip is the grab target; the thin centred
   line is the visual indicator the user aims for. Hover highlights
   the line, indicating it's interactive. */
.col-resizer {
    position: absolute;
    top: 0;
    right: 0;
    width: 5px;
    height: 100%;
    cursor: col-resize;
    user-select: none;
    -webkit-user-select: none;
    touch-action: none;
    background: linear-gradient(
        to right,
        transparent 0%,
        transparent 42%,
        var(--rule) 42%,
        var(--rule) 58%,
        transparent 58%);
    opacity: 0.55;
    transition: opacity 120ms ease, background 120ms ease;
    z-index: 3;
}
.col-resizer:hover,
.col-resizer.is-dragging {
    opacity: 1;
    background: linear-gradient(
        to right,
        rgba(111, 149, 187, 0.10) 0%,
        rgba(111, 149, 187, 0.10) 35%,
        var(--accent) 35%,
        var(--accent) 65%,
        rgba(111, 149, 187, 0.10) 65%,
        rgba(111, 149, 187, 0.10) 100%);
}
body.is-col-resizing,
body.is-col-resizing * { cursor: col-resize !important; user-select: none !important; }

/* .sort-link rules deleted in the legacy-CSS tidy sweep. The
   GridSortLabel.Render method that emitted <button class='sort-link'>
   was deleted alongside (no call sites after the G8 spread closed on
   Leave grids); every grid now uses .col-header-trigger via
   GridSortLabel.Header(). */

/* Column-header hover/focus: a SEMI-TRANSPARENT darken overlay on
   the WHOLE <th> via :has(), not just the inner button. The family
   tint (light-blue identity, white metric) stays visible underneath -
   the eye sees "this column got darker" instead of "every column
   went blue". The :has() form fixes the narrow-column gutter:
   hovering Contract or Roster Hrs (47-54 px wide, of which ~27 px is
   th padding) now darkens the full cell, not a tiny strip in the
   middle. Codified in design-brief.md "Hover darkens the existing
   background, never replaces it" rule. */
.balance-grid thead th:has(.col-header-trigger:hover),
.balance-grid thead th:has(.col-header-trigger:focus-visible) {
    background-image: linear-gradient(rgba(0, 0, 0, 0.10), rgba(0, 0, 0, 0.10));
}

.balance-grid thead th.num { text-align: right; }

/* Sort arrow: a single â–´ glyph in a span, rotated by class. Always
   rendered (so the sortable affordance is visible on every column)
   but at reduced opacity until the column is active. With htmx-
   morph the span persists across grid swaps, so a click that flips
   asc -> desc smoothly transitions the existing arrow's transform
   instead of swapping in a fresh DOM node. */
.sort-arrow {
    display: inline-block;
    margin-left: 0.4rem;
    font-size: 16px;
    font-weight: 700;
    line-height: 1;
    /* Hidden until the column is the active sort. Keeps inactive
       headers clean and ensures the active arrow stands out. */
    opacity: 0;
    transform-origin: 50% 55%;
    transition: transform 140ms ease, opacity 140ms ease;
}
.sort-arrow.is-active { opacity: 1; }
.sort-arrow-asc.is-active  { transform: rotate(0deg); }
.sort-arrow-desc.is-active { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
    .sort-arrow { transition: none; }
}

.balance-grid tbody tr {
    border-bottom: 1px solid var(--rule);
    /* content-visibility: auto previously skipped style/paint for
       off-screen rows, but Chrome's hover-state recalc on a row
       that just came into the visible region paid a measurable
       cost - the user saw the row colour "catch up" behind the
       cursor when sweeping down the grid. Dropped for now;
       reintroduce only with a guard that disables it during hover
       (e.g. via `:has(tr:hover)` or a JS class on the table). */
}
.balance-grid tbody tr:last-child { border-bottom: none; }
/* Row hover background restored - user prefers the whole-row tint
   over the left-edge-only cue. Accepting the paint cost on the
   visible-row count (~50 rows in view at a time, not the full
   2000) for the better visual feedback. */
.balance-grid tbody tr:hover { background: var(--accent-soft); }
/* Baseline row height. Staff / Units / Leave grids each have a
   24 x 24 row-action icon in their drill-col which sets the
   intrinsic row height; Divisions has no drill-col (rows are
   text-only) so its rows were rendering 6-8 px shorter. Setting
   a min-height on every tbody cell normalises the row height
   across all grids so flipping between Staff -> Units ->
   Divisions feels consistent rather than the eye seeing each
   row shrink. */
.balance-grid tbody td { height: 28px; }

.balance-grid td {
    padding: 0.2rem 0.55rem;
    color: var(--ink);
    font-size: 11px;
    line-height: 1.2;
}

/* Right-align + tabular figures on every numeric cell. The monospace
   family is reserved for body and footer cells - column headers
   inherit the standard sans-serif so 'CONTRACTED HRS' reads as the
   same family as 'STAFF', 'UNIT', etc. */
.balance-grid .num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}
/* Numeric cells take an even tighter horizontal padding than the
   default - tabular nums are intrinsically narrow and right-
   aligned, so the breathing room on either side was mostly
   whitespace. Saves ~5 px per numeric column - across ~8 numerics
   on the Staff grid that's the cost of a whole extra column worth
   of width. Header / body / footer all kept in lockstep so the
   column edges line up. Explicit selectors for each row family
   so specificity beats the .balance-grid thead th / td defaults. */
.balance-grid thead th.num,
.balance-grid tbody td.num,
.balance-grid tfoot td.num {
    padding-left: 0.4rem;
    padding-right: 0.4rem;
}
.balance-grid tbody .num,
.balance-grid tfoot .num {
    font-family: var(--mono);
    font-size: 11px;
}

/* Date column: mirrors .num for the date `d MMM yy` cells (variable
   length so they need to line up on the year edge). Sans-serif rather
   than monospace - dates don't share .num's mono body treatment. */
.balance-grid .col-date,
.balance-grid thead th.col-date {
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.balance-grid .balance-red   { color: var(--rag-red); font-weight: 600; }
.balance-grid .balance-amber { color: var(--rag-amber); font-weight: 600; }
.balance-grid .balance-green { color: var(--rag-green); font-weight: 600; }

/* Totals row pinned to the bottom of the scroll container.

   Sticky lives on the <td> rather than the <tr> because border-
   collapse tables don't honour sticky on rows in WebKit / older
   Chromium, so the row would scroll out of view on long tables.
   Cells share a solid background + a top border so the row reads
   as one strip even though the sticky is per-cell.

   The wrapping <tfoot> separately needs a higher z-index than the
   sticky <thead> (which has z-index: 1), otherwise body rows can
   paint OVER the totals during fast scroll - exactly the "totals
   missing" symptom. */
.balance-grid tfoot {
    position: relative;
    z-index: 3;
}
.balance-grid tfoot tr.totals-row td {
    position: sticky;
    /* Flush with the bottom of the scrollport (just above the
       horizontal scrollbar lane). The previous `bottom: 4px` left
       a 4 px lane between the totals row and the scrollbar where
       data rows were briefly visible while scrolling. */
    bottom: 0;
    background-color: var(--panel);
    background-clip: padding-box;
    padding: 0.3rem 0.85rem;
    font-weight: 600;
    color: var(--ink);
    border-top: 1px solid var(--rule);
    box-shadow: 0 -1px 0 var(--rule);
    z-index: 3;
}

/* â”€â”€ Mode toggle pill (Net Hours / Annual Leave) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

/* Sits between the tab strip and the toolbar. Visually a pill switch
   with two equal halves; the active half is filled in with the panel
   surface and bolded. Distinct from the tab bar above (which switches
   what data the grid is showing) - this toggle switches the lens the
   grid is using to look at it. */

/* â”€â”€ Health card (tiles inside the site-header) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

/* Three small tiles sit in the centre of the very top header bar -
   .site-header is flex with space-between, so the tiles take the
   middle slot between the brand on the left and the Trust selector
   / account menu on the right. HTMX-refreshes on balances-refresh
   so the counts track the toolbar filters. */
.site-header .health-card {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    flex: 0 1 auto;
}

.health-tile {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.3rem 0.7rem;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-radius: 2px;
    color: var(--ink);
    text-decoration: none;
    line-height: 1.1;
    transition: border-color 140ms ease, background 140ms ease;
}
.health-tile:hover { background: var(--accent-soft); border-color: var(--accent); }
/* Inset focus ring so the tile's footprint stays exactly the same
   between idle, hover, focused and active states. Outer shadow
   would have grown the tile by 2 px each side - made the clicked
   tile visibly different size to its neighbours. */
.health-tile:focus,
.health-tile:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: inset 0 0 0 2px var(--accent-soft);
}
/* Single-select toggle: the tile whose focus is currently applied
   to the grid carries this class. Solid accent fill with white text
   and icon so it reads unmistakably as "on" against the idle
   neighbours; clicking it clears the focus (no chip in Filtering
   by, the tile itself is the undo). */
.health-tile.is-active {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
    box-shadow: inset 0 0 0 1px var(--accent);
}
.health-tile.is-active .icon-sm,
.health-tile.is-active .health-tile-number,
.health-tile.is-active .health-tile-label { color: #fff; }
.health-tile.is-active .health-tile-number-alert { color: #fff; }
.health-tile.is-active:hover {
    background: var(--accent-hover);
    border-color: var(--accent-hover);
}

.health-tile .icon-sm { color: var(--ink-soft); flex: 0 0 auto; }

.health-tile-body {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-start;
    line-height: 1.1;
}

.health-tile-number {
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--ink);
}
.health-tile-number-alert { color: var(--rag-red-strong); }

.health-tile-label {
    font-size: 7px;
    color: var(--ink-soft);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Action tile (Top movers) shares the chrome with the counter tiles
   but has no number to display - just a prominent label. Match the
   counter font-size so the row's vertical centring lines up with
   the other three. */
.health-tile-label-prominent {
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--ink);
    text-transform: none;
    letter-spacing: 0;
}
.health-tile-action .health-tile-body {
    justify-content: center;
}

/* Skeleton placeholder while the real tiles load. Same chrome
   (border, radius, padding height) so the layout doesn't shift
   when the real partial swaps in. */
.health-tile-skeleton {
    width: 95px;
    height: 25px;
    background: linear-gradient(90deg, var(--rule) 0%, var(--panel) 50%, var(--rule) 100%);
    background-size: 200% 100%;
    animation: health-skeleton-shimmer 1200ms ease-in-out infinite;
    border: 1px solid var(--rule);
    border-radius: 2px;
}
@keyframes health-skeleton-shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
    .health-tile-skeleton { animation: none; }
}
.health-card-loading { gap: 0.5rem; display: inline-flex; }

/* Inline ? button - same visual weight as any other SVG icon inline
   beside a control. No border, no chip background; just the SVG with
   a subtle ink-soft colour that shifts to the accent on hover. The
   <button> wrapper exists for keyboard accessibility but is invisible
   chrome. */
/* Help icons match the modal-corner Close-X chrome: 36 x 36 round
   button with a subtle hover lift. Big enough to read as a real
   click target on the toolbar - the previous small inline icon
   looked like ornament. */
.help-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    margin-left: 0.25rem;
    border: 1px solid transparent;
    background: transparent;
    color: var(--ink-soft);
    cursor: pointer;
    vertical-align: middle;
    border-radius: 50%;
    transition: background 140ms ease, color 140ms ease, border-color 140ms ease;
}
.help-icon .icon { width: 16px; height: 16px; }
.help-icon:hover { background: var(--accent-soft); color: var(--ink); }
.help-icon:focus-visible { outline: none; color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.help-icon-page { margin-left: auto; }

/* Help modal - readable prose card sized for short explanations.
   Reuses the shared .staff-modal chrome so the close button + scroll
   layout match every other detail dialog. */
.help-modal { width: min(429px, 92vw); }
.help-modal .staff-modal-scroll { padding: 1.25rem 1.5rem 1.5rem 1.5rem; }
.help-modal h2 { margin: 0 0 0.5rem 0; font-size: 1.25rem; color: var(--ink); }
.help-modal h3 { margin: 1rem 0 0.35rem 0; font-size: 1rem; color: var(--ink); }
.help-modal p { margin: 0 0 0.65rem 0; color: var(--ink); line-height: 1.5; font-size: 9px; }
.help-modal ul { margin: 0 0 0.65rem 1.2rem; padding: 0; color: var(--ink); font-size: 9px; line-height: 1.5; }
.help-modal li { margin-bottom: 0.2rem; }
.help-modal strong { color: var(--ink); font-weight: 600; }
.help-modal em { font-style: italic; color: var(--ink-soft); }

/* Tab strip + mode toggle share one row so the toggle doesn't claim
   its own near-empty strip above the toolbar. The tab row is flex with
   the toggle pushed to the right edge. */
.tab-row {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 0.75rem;
    /* Zero horizontal padding so the tab-bar's outer edge is flush
       with the parent edge, matching the sub-menu blue strip
       directly below. Was 0 0.25rem on the right which left a 4px
       mismatch with the sub-menu bar. */
    padding: 0;
}

.mode-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0;
    margin: 0 0 0.25rem 0;
    padding: 2px;
    background: var(--accent-soft);
    border: 1px solid var(--rule);
    /* Square-rectangle button family (see design-brief). All clickable
       buttons across the product are sharp 2 px rectangles. */
    border-radius: 2px;
    width: fit-content;
    position: relative;
    z-index: 1;
}

.mode-toggle-button {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.4rem 1.1rem;
    background: transparent;
    border: 1px solid transparent;
    /* Square-rectangle button family (see design-brief). All clickable
       buttons across the product are sharp 2 px rectangles. */
    border-radius: 2px;
    color: var(--ink-soft);
    cursor: pointer;
    font: inherit;
    font-size: 9px;
    font-weight: 500;
    line-height: 1.1;
    transition: background 140ms ease, color 140ms ease, font-weight 140ms ease;
    min-height: 21px;
}

.mode-toggle-button:hover {
    color: var(--ink);
    background: var(--panel);
}

.mode-toggle-button.active {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
    font-weight: 700;
}
.mode-toggle-button.active:hover {
    background: var(--accent-hover);
    border-color: var(--accent-hover);
    color: #fff;
}

.mode-toggle-button .icon-sm {
    width: 9px;
    height: 9px;
}

/* â”€â”€ Leave-mode grid columns â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

/* Header family tints across every grid - pastel-blue / white
   alternation by group of similar columns, matching the KPI
   category-row scheme. The eye reads "this is the same family of
   columns" without four different colours competing.
   Pure CSS via the classes the views already emit, so no per-view
   edit is required to apply the banding. */

/* Default header background - white (the "off" band of the
   alternation). Identity + trail families pick up the pastel blue
   "on" band below. */
.balance-grid thead th { background: var(--panel); }

/* Identity columns (name, unit, division, trust) - pastel blue
   group, anchoring the left of every grid. */
.balance-grid thead th.staff-col,
.balance-grid thead th.unit-col,
.balance-grid thead th.division-col,
.balance-grid thead th.trust-cell {
    background: color-mix(in srgb, var(--accent) 10%, var(--panel));
}

/* Trail / sparkline / checkpoint - pastel blue group, second
   "on" band further along the row. */
.balance-grid thead th.trail-col,
.balance-grid thead th.checkpoint-col {
    background: color-mix(in srgb, var(--accent) 10%, var(--panel));
}

/* Drill / row-action / lozenge columns stay on the white "off"
   band by default - no override needed. */
/* Numeric metric columns also inherit the white band - the right-
   aligned tabular-numeric font already separates them visually. */

/* Leave-grid header families - blue / white alternation only (no
   yellow, no grey). Explicit override wins over the .num default
   above. Total + Remaining keep a slightly deeper blue (22%) so the
   totals still read as the emphasis columns; bold weight is already
   on every thead th and doesn't need re-asserting. */
.leave-grid thead th.leave-ent {
    background: color-mix(in srgb, var(--accent) 10%, var(--panel));
}
.leave-grid thead th.leave-total {
    background: color-mix(in srgb, var(--accent) 22%, var(--panel));
}
.leave-grid thead th.leave-use {
    background: var(--panel);
}
.leave-grid thead th.leave-remaining {
    background: color-mix(in srgb, var(--accent) 22%, var(--panel));
}
.leave-grid thead th.leave-pace {
    background: var(--panel);
}
.leave-grid tbody td.leave-total,
.leave-grid tbody td.leave-remaining { font-weight: 600; }

/* KPI grid keeps its own category-row tints (kpi-cat-*) - the
   two-row header already does the family grouping there. */

/* Leave-grid family rules moved earlier in this file alongside the
   global .balance-grid thead family tints - kept together so the
   precedence story is in one place. */

/* Leave-grid uses the same row spacing as the Staff grid - the
   previous narrower-padding override was added before horizontal-
   scrolling on all grids landed. Now wide Leave grids scroll
   sideways instead of squashing the columns. */

/* Top mover cards in leave mode get the leave-yellow accent on the
   delta number, matching the Used column band. */
.top-mover-card.top-mover-leave .top-mover-delta {
    color: var(--ink);
}
.top-mover-card.top-mover-leave {
    border-left: 2px solid var(--duty-annual);
    /* Square aspect for the "Most leave yet to take" cards - same
       width as height so they tile cleanly. The default top-mover
       wide rectangle suits a single big delta on Staff / Units; the
       Leave variant carries four lines of equally-weighted content
       and reads better as a square. */
    flex: 0 0 101px;
    width: 101px;
    min-width: 101px;
    max-width: 101px;
    height: 101px;
    min-height: 101px;
    justify-content: space-between;
}

/* â”€â”€ Flying Top Movers strip (site-header) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */

/* Single "Top movers" button in the flying header (next to the
   health tiles). Click sorts the Staff grid by |variance| DESC. */
.top-movers-flying-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.35rem 0.85rem;
    border: 1px solid var(--rule);
    background: var(--panel);
    color: var(--ink);
    text-decoration: none;
    font-size: 9px;
    line-height: 1.2;
    transition: background 140ms ease, border-color 140ms ease;
    flex: 0 0 auto;
}
.top-movers-flying-btn:hover { background: var(--accent-soft); border-color: var(--accent); }
.top-movers-flying-btn:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.top-movers-flying-btn .icon-sm { color: var(--ink-soft); }

/* â”€â”€ Button shape: lozenges across the product â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Single override block at the end of the file so every button-
   style control reads as a pill regardless of where the original
   rule set its radius. Cards, modals, inputs, tabs and the table
   chrome keep their existing slight rounding - those aren't buttons
   so the lozenge convention shouldn't claim them. */
.primary-button,
.secondary-button,
.ask-button,
.link-action,
.generate-notes,
.generate-all-btn,
.signoff-button,
.status-bar-export,
.login-submit,
.unit-picker-action,
.balance-band-clear,
.nl-example,
.lozenge-button,
.health-tile,
.top-mover-card,
.top-movers-flying-btn,
.mode-toggle-button {
    /* Square-rectangle button family - see design-brief Shapes. Every
       clickable button is a sharp 2 px rectangle. */
    border-radius: 2px;
}

/* "Clear" affordance inside grid status-banner sentences (e.g.
   "Balance outside +/- 11.5 h tolerance. [clear]"). Promoted to a
   lozenge button - small but unambiguous - rather than a flat inline
   link which read as part of the sentence. */
.balance-band-clear {
    display: inline-flex;
    align-items: center;
    padding: 0.15rem 0.7rem;
    margin-left: 0.4rem;
    border: 1px solid var(--rule);
    background: var(--panel);
    color: var(--accent);
    font-size: 9px;
    text-decoration: none;
    line-height: 1.4;
    transition: background 140ms ease, border-color 140ms ease;
}
.balance-band-clear:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
    text-decoration: none;
}
.balance-band-clear:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

/* The Units toolbar chip stays square - it sits in the same row as the
   square Name search and Grade picker, so a lozenge there would clash. */
.unit-picker-trigger { border-radius: 2px; }

/* Tooltips inside the balance grid: barnaclesUpgradeTooltips() already
   skips title-promotion under .balance-grid for performance on long
   grids, so .barnacles-tip never fires there. Any per-cell hint that
   stays as native title= remains a quiet browser tooltip. */

/* â”€â”€ Sign-off modal: per-staff review grid â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   The pre-sign-off modal now lists every staff member that's about
   to be captured, with a per-row textarea for an optional manual
   note + a Generate button that asks Oceansblue AI for a suggestion
   the manager can copy into the textarea. */
/* Compact one-page sign-off modal - widens the form so the staff grid
   breathes and tightens vertical rhythm so the whole flow lives in a
   single viewport. The internal grid scrolls; the action footer +
   attestation stay anchored at the bottom of the visible area. */
.signoff-form-compact {
    padding: 1rem 1.2rem 0.9rem;
    max-width: min(780px, 95vw);
    min-width: min(560px, 92vw);
}
.signoff-header-compact {
    margin-bottom: 0.55rem;
    align-items: center;
    /* Leave room for the absolute .staff-modal-close in the top-right
       corner - without this the status badge collides with the Ã— glyph. */
    padding-right: 2rem;
}
/* One-line metrics strip - replaces the bordered preview card so the
   whole modal earns its vertical space. Each .signoff-metric is a
   self-contained label / value / meta cluster. */
.signoff-metrics {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 1.2rem;
    padding: 0.45rem 0.7rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--bg);
    margin-bottom: 0.65rem;
}
.signoff-metric {
    display: inline-flex;
    align-items: baseline;
    gap: 0.35rem;
}
.signoff-metric-label {
    font-size: 9px;
    color: var(--ink-soft);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.signoff-metric-value { font-size: 13px; font-weight: 600; }
.signoff-metric-meta  { font-size: 9px; color: var(--ink-soft); }

/* Date + Note share one row to compact the form. The date input
   keeps the standard filter-input height so it sits flush with the
   note. */
.signoff-inline-row {
    display: flex;
    gap: 0.65rem;
    margin-bottom: 0.65rem;
    align-items: flex-end;
}
.signoff-inline-field {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
}
.signoff-inline-field-grow { flex: 1 1 auto; min-width: 0; }
.signoff-inline-label {
    font-size: 9px;
    color: var(--ink-soft);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.signoff-date-input { width: 9rem; }

/* Attestation tightened to a single banner + inline tick / typed-name
   row. The hint sits underneath so the row stays narrow. */
.signoff-attestation-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.8rem 1.1rem;
    margin-top: 0.4rem;
}
.signoff-typed-name-inline {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 10px;
    color: var(--ink-soft);
}
.signoff-typed-name-inline .signoff-typed-name-input {
    width: 11rem;
}

.signoff-staff-review {
    margin: 0 0 0.7rem;
}
.signoff-staff-review-head {
    margin-bottom: 0.5rem;
}
.signoff-staff-review-title {
    font-size: 11px;
    margin: 0 0 0.2rem;
    color: var(--ink);
}
.signoff-staff-review-sub {
    font-size: 10px;
    color: var(--ink-soft);
    margin: 0;
}
.signoff-staff-review-scroll {
    max-height: 241px;
}
.signoff-staff-review-grid td {
    vertical-align: top;
}
.signoff-staff-note-cell {
    min-width: 188px;
}
.signoff-staff-note-input {
    width: 100%;
    min-height: 29px;
    font: inherit;
    font-size: 10px;
    padding: 0.35rem 0.5rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--bg);
    resize: vertical;
}
.signoff-staff-note-actions {
    margin-top: 0.3rem;
    display: flex;
    gap: 0.3rem;
}
/* Empty placeholder (no Generate yet, or Dismiss clicked) takes no
   vertical space - keeps the row compact until the manager asks
   for an AI suggestion. */
.signoff-staff-note-suggestion[data-empty] {
    display: none;
}
.signoff-staff-note-suggestion {
    margin-top: 0.4rem;
    padding: 0.5rem 0.6rem;
    border: 1px dashed var(--accent);
    border-radius: 2px;
    background: color-mix(in srgb, var(--accent) 6%, transparent);
}
.signoff-staff-note-suggestion-label {
    font-size: 9px;
    color: var(--accent);
    font-weight: 600;
    margin-bottom: 0.25rem;
}
.signoff-staff-note-suggestion-text {
    font-size: 10px;
    color: var(--ink);
    white-space: pre-wrap;
    margin-bottom: 0.4rem;
}
.signoff-staff-note-suggestion-actions {
    display: flex;
    gap: 0.3rem;
}

/* Sign-off snapshot - per-staff Manual note column.
   Only rendered when at least one staff member in the snapshot
   carries a note (the head/cell are gated by anyManualNotes in
   the view). Wraps long notes onto multiple lines so the column
   doesn't blow the table width on a verbose entry. */
.signoff-snapshot-note {
    white-space: pre-wrap;
    max-width: 241px;
    font-size: 9px;
    color: var(--ink);
}

/* Approval-role badges on the Admin Users grid. Distinct from the
   strong admin-flags (red) - these are quieter pastel blue pills
   for HRD / CNO / FD assignments. */
.admin-flag-role {
    background: color-mix(in srgb, var(--accent) 12%, #fff);
    border: 1px solid color-mix(in srgb, var(--accent) 35%, #fff);
    color: var(--accent);
}

/* â”€â”€ Approvals stub page (phase 1) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Roadmap landing page for the Approvals tab. Replaced by the
   triangulation grids + workflow in subsequent phases. */
.approvals-stub {
    padding: 1.5rem 0;
    max-width: 616px;
}
.approvals-stub-header h1 {
    margin: 0 0 0.4rem;
    font-size: 15px;
    color: var(--ink);
}
.approvals-stub-sub {
    font-size: 10px;
    color: var(--ink-soft);
    margin: 0 0 1.4rem;
}
.approvals-stub-roles, .approvals-stub-roadmap {
    background: #fff;
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 1rem 1.2rem;
    margin: 0 0 1rem;
}
.approvals-stub-roles h2, .approvals-stub-roadmap h2 {
    font-size: 11px;
    margin: 0 0 0.7rem;
    color: var(--ink);
}
.approvals-role-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin: 0 0 0.6rem;
}
/* .approvals-role-chip + .is-active replaced by .audit-badge +
   .audit-badge-info / .audit-badge-muted (active / inactive role chips
   for HRD / CNO / FD). One canonical text-badge family across the app. */
.approvals-stub-meta {
    font-size: 9px;
    color: var(--ink-soft);
    margin: 0.4rem 0 0;
}
.approvals-roadmap-list {
    margin: 0;
    padding-left: 1.2rem;
}
.approvals-roadmap-list li {
    font-size: 10px;
    color: var(--ink);
    margin: 0 0 0.6rem;
}
.approvals-roadmap-list li:last-child { margin-bottom: 0; }

/* â”€â”€ Finance Data admin page (Approvals phase 2) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Header navigation + per-Trust upload form + headline-summary
   cards + sample-rows preview table. */
/* Admin page-switcher restyled as a tab strip (subtab pattern) so
   it matches the Net Hours / Annual Leave / KPI sub-tab chrome
   instead of looking like a row of generic buttons. Accent strip
   sits behind the row; each link styles as a subtab; the active
   page flips to the white panel fill the same way is-active does
   on the sub-tab bar. */
.admin-nav {
    display: flex;
    align-items: stretch;
    gap: 2px;
    margin: 0.8rem 0 0;
    padding: 0.2rem 0.4rem 0 0.4rem;
    background: var(--accent);
    border-radius: 4px 4px 0 0;
}
.admin-nav .secondary-button {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.5rem 0.95rem 0.55rem 0.95rem;
    font: inherit;
    font-size: 0.8rem;
    font-weight: 500;
    color: #fff;
    text-decoration: none;
    background: transparent;
    border: 1px solid transparent;
    border-bottom: none;
    border-radius: 4px 4px 0 0;
    margin-bottom: -1px;
    transition: none;
    white-space: nowrap;
    cursor: pointer;
}
.admin-nav .secondary-button:hover {
    background: var(--panel);
    color: var(--ink);
    border-color: var(--rule);
    box-shadow: inset 0 1px 0 color-mix(in srgb, var(--accent-soft) 60%, white);
}
.admin-nav .secondary-button.is-current {
    background: var(--panel);
    color: var(--ink);
    border-color: var(--rule);
    font-weight: 600;
    z-index: 2;
}
/* Back link sits flush-left and reads as "leave admin" rather than
   a peer page tab. Stays text-style on the accent strip with a
   subtle hover lift; pushes the rest of the tabs away with auto
   right margin so the page tabs cluster together. */
.admin-nav .admin-nav-back {
    background: transparent;
    border-color: transparent;
    color: #fff;
    font-weight: 500;
    opacity: 0.92;
    margin-right: auto;
    padding-left: 0.55rem;
    padding-right: 0.55rem;
}
.admin-nav .admin-nav-back:hover {
    background: color-mix(in srgb, #fff 12%, transparent);
    border-color: transparent;
    color: #fff;
    box-shadow: none;
    opacity: 1;
}
/* Banner semantic variants - sit on top of the .admin-banner base.
   Same shape (padding / radius / margin / font), different colour
   family per state. The base info-blue style is defined alongside
   .admin-banner higher up in this file. */
.admin-banner-success {
    background: color-mix(in srgb, var(--rag-green) 12%, #fff);
    border-color: color-mix(in srgb, var(--rag-green) 35%, #fff);
    color: var(--ink);
}
.admin-banner-error {
    background: var(--rag-red-soft-bg);
    border-color: var(--rag-red-soft-border);
    color: var(--rag-red);
}
/* â”€â”€ Shared stat-card tiles â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Headline tiles used on Finance Data (.finance-stat) and the
   Approvals headline (.approvals-headline-stat). Same shape, same
   typography - the two view-specific class names share these
   rules so the visual idiom is identical across the app. New
   places that want a "label + big number + meta" tile should
   adopt these class names too.
*/
.finance-summary,
.approvals-headline-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    margin: 0 0 1.2rem;
}
.finance-stat,
.approvals-headline-stat {
    flex: 1;
    min-width: 107px;
    background: #fff;
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.7rem 0.9rem;
}
.finance-stat-label,
.approvals-headline-label {
    display: block;
    font-size: 9px;
    color: var(--ink-soft);
    text-transform: uppercase;
    letter-spacing: 0.4px;
    margin-bottom: 0.25rem;
}
.finance-stat-value,
.approvals-headline-value {
    display: block;
    font-size: 15px;
    font-weight: 600;
    color: var(--ink);
}
.finance-stat-meta,
.approvals-headline-meta {
    display: block;
    font-size: 9px;
    color: var(--ink-soft);
    margin-top: 0.2rem;
}
.finance-stat-meta-line {
    display: block;
    font-size: 9px;
    color: var(--ink-soft);
    margin-top: 0.2rem;
}
.finance-upload {
    background: #fff;
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 1rem 1.2rem;
    margin: 0 0 1.2rem;
}
.finance-upload h2 {
    margin: 0 0 0.5rem;
    font-size: 11px;
}
.finance-upload-hint {
    font-size: 10px;
    color: var(--ink-soft);
    margin: 0 0 0.8rem;
}
.finance-format-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 9px;
    margin: 0 0 1rem;
}
.finance-format-table th,
.finance-format-table td {
    text-align: left;
    border: 1px solid var(--rule);
    padding: 0.4rem 0.6rem;
}
.finance-format-table th {
    background: var(--accent-soft);
    color: var(--ink);
}
.finance-upload-form {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
}
.finance-upload-form input[type=file] {
    flex: 1;
    min-width: 161px;
    height: 24px;
    font-size: 10px;
}
.finance-preview {
    background: #fff;
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 1rem 1.2rem;
    margin: 0 0 1.2rem;
}
.finance-preview h2 {
    margin: 0 0 0.6rem;
    font-size: 11px;
}

/* â”€â”€ Approvals tab phase 3 - triangulation + alert grids â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   The Finance / Estab / Demand / Actual / In Post triangulation
   landing page. Headline stat strip + three alert cards (Estab /
   Demand / Actual exceed Finance) + the full detail grid. */
.approvals-header {
    padding: 1.2rem 0 0.6rem;
}
.approvals-header h1 {
    margin: 0 0 0.35rem;
    /* Was 15px; bumped to the same 1.25rem the Budgets + Leave
       Entitlements dashboards use so the three governance tabs
       have the same title scale at a glance. The body font on
       this page stays on the original tighter pixel scale by
       intent (Approvals is a dense triangulation table), so
       the h1 sits proportionally larger here than on the
       Budgets dashboard but matches absolutely. */
    font-size: 1.25rem;
    font-weight: 600;
    color: var(--ink);
}
.approvals-stub-sub {
    margin: 0;
    font-size: 0.78rem;
    color: var(--ink-soft);
    max-width: 75ch;
    line-height: 1.45;
}
/* Banner-card family - shared shape across Approvals and Sign-off
   contexts. Same padding / radius / margin / font-size; the
   colour-modifier classes (-error red, -no-alerts green, default
   neutral white) override only background + border + color. Same
   shape rules as .admin-banner above but with a card-style 4px
   radius and rule-grey border by default. */
/* .approvals-empty / -no-changes / -error replaced by _EmptyState
   partial + canonical .empty-state class. The bespoke dashed-border
   slate card + red-tint variants here drifted from every other empty
   state in the app - G6 sweep closed in 2026-06-02. */
.approvals-summary {
    margin: 0 0 1rem;
}
.approvals-summary-meta {
    font-size: 9px;
    color: var(--ink-soft);
    margin: 0 0 0.6rem;
}
/* .approvals-headline-stats / -stat / -label / -value / -meta
   defined alongside .finance-stat higher up - same shape, shared
   rules. Block kept empty so the comment-anchor stays for future
   editors. */
.approvals-alerts {
    margin: 1.5rem 0;
}
.approvals-alerts h2,
.approvals-detail h2,
.approvals-stub-roadmap h2 {
    font-size: 11px;
    margin: 0 0 0.6rem;
    color: var(--ink);
}
.approvals-alert-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(228px, 1fr));
    gap: 0.8rem;
}
.approvals-alert-card {
    background: #fff;
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.9rem 1rem;
}
.approvals-alert-header {
    position: relative;
    margin: 0 0 0.6rem;
    padding-right: 4rem;
}
.approvals-alert-header h3 {
    margin: 0 0 0.25rem;
    font-size: 10px;
    color: var(--rag-red);
}
.approvals-alert-header p {
    margin: 0;
    font-size: 9px;
    color: var(--ink-soft);
}
.approvals-alert-count {
    position: absolute;
    top: 0;
    right: 0;
    background: var(--rag-red-soft-bg);
    border: 1px solid var(--rag-red-soft-border);
    color: var(--rag-red);
    font-size: 8px;
    padding: 0.15rem 0.5rem;
    border-radius: 669px;
}
.approvals-alert-empty {
    margin: 0;
    padding: 0.8rem 0;
    font-size: 9px;
    color: var(--rag-green);
}
/* .approvals-no-alerts replaced by canonical .empty-state - G6 sweep. */
/* Approvals alert table inherits the .balance-grid 17 px body font
   and row padding - no override needed for visual consistency. */

/* "View by band â€º" link inside each alert card's header. Sits below
   the count badge as a quiet text-button so it doesn't compete with
   the card's title + subtitle. Hover lifts the underline only - no
   colour shift (the card itself is already coded red for breaches). */
.band-pivot-open-link {
    margin-top: 0.4rem;
    padding: 0;
    background: none;
    border: 0;
    font-size: 9px;
    color: var(--ink);
    cursor: pointer;
    text-decoration: none;
}
.band-pivot-open-link:hover,
.band-pivot-open-link:focus-visible {
    text-decoration: underline;
}

/* Band-mix alert card. Same chrome as the over-Finance siblings but
   the header carries an inline threshold form so the user can tune
   the cancellation cut-off from the card itself. */
.approvals-alert-mix .approvals-alert-header {
    padding-right: 4rem;
}
.band-mix-threshold-form {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    margin-top: 0.4rem;
    font-size: 9px;
    color: var(--ink-soft);
}
.band-mix-threshold-form label {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    margin: 0;
}
.band-mix-threshold-form input[type=number] {
    width: 6.2rem;
    font-size: 10px;
    padding: 0.15rem 0.3rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
}

/* Band-pivot modal. Wider than the propose dialog because the matrix
   has one column per band - the .balance-grid-scroll inside provides
   horizontal scroll on smaller viewports. */
.band-pivot-dialog::backdrop {
    background: rgba(0, 0, 0, 0.18);
}
.band-pivot-dialog {
    width: min(96vw, 1100px);
    max-width: 96vw;
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0;
    background: #fff;
}
.band-pivot-table th,
.band-pivot-table td {
    white-space: nowrap;
}
.band-pivot-empty {
    color: var(--ink-soft);
    opacity: 0.3;
}
.band-pivot-action-col {
    text-align: right;
    width: 1%;
    white-space: nowrap;
}

.approvals-detail {
    margin: 1.5rem 0;
}
.approvals-detail-hint {
    font-size: 9px;
    color: var(--ink-soft);
    margin: 0 0 0.5rem;
}
/* "No Finance baseline" placeholder in the per-unit summary's
   vs-Finance Â£ cell + its sibling footer cell. Reads as muted /
   absent rather than a Â£0 variance so the user can tell apart
   "matches Finance exactly" (Â£0 painted black) from "we have
   nothing to compare against" (hyphen, faded). */
.approvals-no-baseline {
    color: var(--ink-soft);
    opacity: 0.5;
}
/* Approvals detail grid uses the standard .balance-grid row spacing
   and 17 px body font - no font-size / padding overrides. The grid
   scrolls horizontally if the column count exceeds the viewport. */
.approvals-detail-grid th.num {
    text-align: right;
}
.approvals-group-head {
    text-align: center !important;
    font-weight: 600;
    border-bottom: 1px solid var(--rule);
}
/* Group-band heads alternate light-blue / white - the only two
   tints permitted on grid headers across the app. Five groups means
   blue / white / blue / white / blue. Status colour (RAG) belongs in
   the body cells, never on the header chrome. */
.approvals-group-fin    { background: color-mix(in srgb, var(--accent) 10%, #fff); }
.approvals-group-estab  { background: #fff; }
.approvals-group-demand { background: color-mix(in srgb, var(--accent) 10%, #fff); }
.approvals-group-actual { background: #fff; }
.approvals-group-inpost { background: color-mix(in srgb, var(--accent) 10%, #fff); }
.approvals-row-sub td {
    background: color-mix(in srgb, var(--accent) 8%, #fff);
    font-weight: 600;
}
.approvals-row-grand td {
    background: color-mix(in srgb, var(--ink) 8%, #fff);
    font-weight: 700;
    color: var(--ink);
}

/* â”€â”€ Approvals tab phase 4 - budget-change workflow â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Three-way HRD/CNO/FD approval cards + a Propose form + the
   recently-decided list. */
.approvals-changes {
    margin: 1.5rem 0;
}
.approvals-changes h2 {
    font-size: 11px;
    margin: 0 0 0.6rem;
    color: var(--ink);
}
.approvals-changes-sub {
    font-size: 9px;
    color: var(--ink-soft);
    margin: 0 0 0.8rem;
}
/* .approvals-no-changes - shape merged into the banner-card family
   block higher up (alongside .approvals-empty / .approvals-error).
   Anchor comment kept for searchability. */
.budget-change-card {
    background: #fff;
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.9rem 1rem;
    margin: 0 0 0.7rem;
}
.budget-change-card.budget-change-status-approved {
    border-left: 3px solid var(--rag-green);
}
.budget-change-card.budget-change-status-rejected {
    border-left: 3px solid var(--rag-red);
    opacity: 0.92;
}
.budget-change-card.budget-change-status-pending {
    border-left: 3px solid var(--accent);
}
.budget-change-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin: 0 0 0.5rem;
}
.budget-change-title {
    display: flex;
    align-items: baseline;
    gap: 0.4rem;
    font-size: 10px;
}
.budget-change-band {
    background: var(--accent-soft);
    color: var(--accent);
    padding: 0.1rem 0.4rem;
    border-radius: 2px;
    font-size: 9px;
}
.budget-change-type {
    font-size: 9px;
    color: var(--ink-soft);
}
/* .budget-change-status pill replaced by canonical .audit-badge family
   (.audit-badge-ok / -err / -info for Approved / Rejected / Pending).
   The .budget-change-card.budget-change-status-* left-border tint is
   still defined above - that one paints the whole card border, distinct
   from the status badge inside it. */
.budget-change-body {
    margin: 0 0 0.6rem;
}
.budget-change-wte {
    display: flex;
    align-items: baseline;
    gap: 0.4rem;
    margin: 0 0 0.3rem;
    font-size: 9px;
}
.budget-change-wte-label { color: var(--ink-soft); font-size: 9px; }
.budget-change-wte-value { font-weight: 600; font-size: 10px; }
.budget-change-wte-arrow { color: var(--ink-soft); }
.budget-change-wte-delta { font-size: 9px; margin-left: 0.4rem; }
.budget-change-rationale {
    margin: 0 0 0.3rem;
    font-size: 9px;
    color: var(--ink);
    background: color-mix(in srgb, var(--accent) 6%, #fff);
    padding: 0.45rem 0.6rem;
    border-radius: 2px;
}
.budget-change-meta {
    font-size: 9px;
    color: var(--ink-soft);
    margin: 0;
}
.budget-change-meta .sep { margin: 0 0.4rem; }
.budget-change-slots {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.4rem;
    margin: 0.5rem 0 0;
}
.budget-change-slot {
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.5rem 0.6rem;
    background: #fafafa;
}
.budget-change-slot-role {
    font-size: 8px;
    color: var(--ink-soft);
    text-transform: uppercase;
    letter-spacing: 0.4px;
    margin: 0 0 0.3rem;
    font-weight: 600;
}
.budget-change-slot-voted {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    font-size: 9px;
    color: var(--ink-soft);
}
.budget-change-slot-voted strong { font-size: 9px; }
.budget-change-slot-approved strong { color: var(--rag-green); }
.budget-change-slot-rejected strong { color: var(--rag-red); }
.budget-change-slot-voted em { font-style: italic; color: var(--ink); }
.budget-change-slot-waiting {
    font-size: 9px;
    color: var(--ink-soft);
    font-style: italic;
}
.budget-change-vote-form {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}
/* Vote slot input + buttons. Approve / Reject are the most
   consequential interactions on the page so they hit the
   primary-button floor: 36 px tall, 15 px font, full-width
   inside the slot. */
.budget-change-vote-note {
    width: 100%;
    height: 24px;
    font-size: 10px;
    padding: 0.35rem 0.5rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
}
.budget-change-vote-buttons {
    display: flex;
    gap: 0.3rem;
}
.budget-change-vote-buttons button { flex: 1; }
.budget-change-vote-reject {
    background: var(--rag-red-soft-bg);
    color: var(--rag-red);
    border-color: var(--rag-red-soft-border);
}
/* B8 - .approvals-propose-details + summary rules deleted; the
   propose form moved out of a <details> drawer into a modal
   <dialog id="approvals-propose">. The trigger button + the
   dialog chrome use the standard .primary-button + .staff-modal
   families respectively; no bespoke dialog rules needed here. */
.approvals-propose-trigger-row {
    margin: 1rem 0 0.5rem;
}
.approvals-propose-form {
    margin: 0.6rem 0 0;
}
.approvals-propose-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin: 0 0 0.5rem;
}
.approvals-propose-row label {
    flex: 1;
    min-width: 87px;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    font-size: 9px;
    color: var(--ink-soft);
}
.approvals-propose-row input, .approvals-propose-row select {
    height: 24px;
    padding: 0 0.4rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    font-size: 10px;
    background: #fff;
}
.approvals-propose-rationale {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    font-size: 9px;
    color: var(--ink-soft);
    margin: 0 0 0.5rem;
}
.approvals-propose-rationale textarea {
    font: inherit;
    font-size: 10px;
    padding: 0.4rem 0.5rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: #fff;
    resize: vertical;
    min-height: 47px;
}
.approvals-propose-actions {
    display: flex;
    justify-content: flex-end;
}
.approvals-decided-details {
    background: #fff;
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.5rem 1rem;
    margin: 1rem 0;
}
.approvals-decided-details summary {
    cursor: pointer;
    font-weight: 600;
    font-size: 10px;
    padding: 0.3rem 0;
}
.approvals-changes-decided {
    margin: 0.5rem 0 0;
}

/* Inline Propose buttons on the Approvals detail grid. Lives in
   a narrow column at the right of each row; subtotal rows propose
   against the whole unit, detail rows pre-fill the band too. */
.approvals-propose-col {
    width: 1%;
    white-space: nowrap;
    text-align: right;
    padding: 3px 5px;
}
/* Inline Propose buttons on the Approvals detail grid. Sit inside
   per-row narrow column; markup now uses .secondary-button which
   already meets the Ward-Manager 36 px / 15 px floor - no per-button
   overrides needed. */

/* Post-hoc note editor on the Sign-off snapshot modal. CanEditSignedNotes
   gates the edit pencil; every edit lands in the audit table with
   previous + new text + editor + IP. The pencil button itself is
   a .row-icon-btn (markup) for the standard 36 x 36 hit target; the
   inline form uses .primary-button / .secondary-button Save / Cancel
   to match every other modal action row. */
.signoff-snapshot-note-view {
    display: flex;
    gap: 0.4rem;
    align-items: flex-start;
}
.signoff-snapshot-note-text {
    flex: 1;
    white-space: pre-wrap;
}
.signoff-snapshot-note-form {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}
.signoff-snapshot-note-input {
    width: 100%;
    min-height: 34px;
    font: inherit;
    font-size: 10px;
    padding: 0.4rem 0.5rem;
    border: 1px solid var(--rule);
    border-radius: 2px;
    resize: vertical;
}
.signoff-snapshot-note-actions {
    display: flex;
    gap: 0.4rem;
}
.signoff-snapshot-note-meta {
    margin-top: 0.3rem;
    font-size: 9px;
    color: var(--ink-soft);
    font-style: italic;
}

/* Triangulation expand row - shared between the Budgets matrix and
   the Approvals triangulation grid. Parent row carries a .tri-toggle
   button (â–¸); the next <tr class="tri-expand"> with the matching
   data-tri-parent is shown/hidden by barnaclesToggleTri(). Visual
   intent: subtle, indented, framed so the expansion reads as a
   "drawer" rather than a continuation of the main grid. */
.tri-toggle {
    background: transparent;
    border: 1px solid var(--rule);
    cursor: pointer;
    padding: 0;
    margin-right: 0.35rem;
    color: var(--ink-soft);
    font-size: 13px;
    line-height: 1;
    transition: color 140ms ease, border-color 140ms ease, background 140ms ease;
    width: 22px;
    height: 22px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 2px;
    vertical-align: middle;
}
.tri-toggle:hover { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
.tri-toggle:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--accent-soft);
}
.tri-toggle-arrow {
    display: inline-block;
    transition: transform 140ms ease;
    font-size: 14px;
    line-height: 1;
    transform-origin: 50% 50%;
}
/* Open state - the row's expand drawer is visible. CSS rotation
   carries the visual cue; JS no longer swaps the glyph (â–¸ stays â–¸,
   it just turns) so the transition can animate smoothly. Honour
   prefers-reduced-motion by stripping the transition + snap straight
   to the rotated position. */
.tri-toggle[aria-expanded="true"] .tri-toggle-arrow {
    transform: rotate(90deg);
}
@media (prefers-reduced-motion: reduce) {
    .tri-toggle-arrow { transition: none; }
}
.tri-expand > td.tri-expand-cell {
    padding: 0;
    background: var(--bg);
    border-top: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule);
}
.tri-expand-table {
    width: auto;
    border-collapse: collapse;
    margin: 0 auto 0 0;       /* anchor to the left of the cell */
    font-size: 11px;
}
.tri-expand-table thead th {
    background: transparent;
    color: var(--ink-soft);
    font-weight: 600;
    font-size: 10px;
    text-transform: none;
    letter-spacing: 0;
    padding: 0.35rem 0.6rem;
    border-bottom: 1px solid var(--rule);
    text-align: left;
    white-space: nowrap;
}
.tri-expand-table thead th.num { text-align: right; }
.tri-expand-table tbody td {
    padding: 0.4rem 0.6rem;
    border-bottom: 1px solid var(--rule);
    vertical-align: middle;
}
.tri-expand-table tbody td.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.tri-expand-table tbody tr:last-child td { border-bottom: none; }
.tri-expand-table .tri-source-col {
    width: auto;
    padding-left: 2.2rem;
    display: flex;
    align-items: center;
    gap: 0.55rem;
}
.tri-expand-table thead th.tri-source-col {
    display: table-cell;
    padding-left: 2.2rem;
}
.tri-source-stripe {
    display: inline-block;
    width: 3px;
    height: 14px;
    border-radius: 2px;
    flex-shrink: 0;
}
.tri-source-stripe-fin    { background: var(--accent); }
.tri-source-stripe-estab  { background: #6f95bb; }
.tri-source-stripe-demand { background: #d49a3e; }
.tri-source-stripe-actual { background: #76a585; }
.tri-source-label { font-weight: 600; color: var(--ink); }
.tri-source-row.tri-source-fin .tri-source-label { color: var(--ink); }

/* When the expand row is open we want a discreet visual hint on the
   parent row so the eye keeps its place. Hosts add .tri-row-open via
   the toggle handler if they want the highlight. */
tr.tri-row-open { background: var(--accent-soft); }

}


/* Skeleton placeholder for status-bar buttons that don't render until
   the grid HTMX swap arrives (Fit, Group, Export, Generate). Same
   24 px x ~95 px shape as the real toolbar-action buttons; soft grey
   shimmer matches the data-row .skeleton-cell. */
.status-bar-skeleton {
    /* Inherits from .status-bar - just used as a hook for CSS rules. */
}
.skeleton-button-placeholder {
    display: inline-block;
    width: 88px;
    height: 24px;
    border-radius: 2px;
    background: linear-gradient(
        90deg,
        rgba(200, 210, 225, 0.45) 0%,
        rgba(220, 230, 240, 0.65) 50%,
        rgba(200, 210, 225, 0.45) 100%
    );
    background-size: 200% 100%;
    animation: skeleton-shimmer 1.2s ease-in-out infinite;
    flex: 0 0 auto;
}
.skeleton-button-placeholder.is-wide { width: 130px; }
@keyframes skeleton-shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
    .skeleton-button-placeholder { animation: none; }
}
/* Last login + Logins-30d columns on the Admin Users grid - left-aligned,
   min-width so the date / count never wraps onto two lines. Tabular nums
   on the count cell for clean column alignment. */
.admin-users-grid .admin-login-col {
    text-align: left;
    min-width: 92px;
    white-space: nowrap;
}
.admin-users-grid .admin-logins-30d {
    font-variant-numeric: tabular-nums;
    min-width: 64px;
}
.admin-users-grid .admin-last-login {
    color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
}