/* ── Standard UX column classes — THE FRONT DOOR for list tables ──────────
   Put `class="ux-date-col"` (etc.) on the <th> and it flows to the cells.

   THESE CLASSES AND THE `--col-*` VARIABLES BELOW ARE ONE SYSTEM, not two.
   I added the variable ladder without noticing this family already existed and
   did the same job — Clayton hit the collision converting his tables (2026-07-28).
   Rather than pick a winner and leave a stale copy, the classes now READ the
   variables: the vars are the single source of the NUMBERS, the classes add the
   behaviour a bare width can't (nowrap + ellipsis, so a long name truncates
   instead of blowing the column open).

   WHICH TO USE: reach for the CLASS on any list table — it's the complete
   package. Use `style="width:var(--col-…)"` only where a class doesn't fit
   (a one-off inline width, or a column that must wrap). Never a literal for a
   recurring kind of column; a genuinely one-off width stays literal WITH a
   comment saying why (see the note on the ladder below). */
.ux-date-col    { width: var(--col-date);    white-space: nowrap; }
.ux-name-col    { width: var(--col-name);    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ux-email-col   { width: var(--col-email);   white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ux-project-col { width: var(--col-project); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ux-role-col    { width: var(--col-role);    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ── Growable list tables ─────────────────────────────────────────────────
   Cap the height so the Undo/Redo row + inline add form BELOW the list stay
   reachable without scrolling past every row (Skyline scale — 200 users made
   the add form unreachable, sticky-footer or not). Header sticks to the top of
   the scroll box. Pair with `form-sticky-footer` on the form's button row.
   Tune the height HERE and every list follows. */
.ux-list-scroll { max-height: 60vh; overflow: auto; }
.ux-list-scroll > table thead th { position: sticky; top: 0; background: #f1f3f5; z-index: 1; }

/* ── Fit-to-viewport editor page (PREFERRED for full-page editors) ─────────
   The page fills the space under the top bar and the list flex-fills the exact
   leftover, becoming the ONLY scrollbar — no window scroll. Supersedes the
   fixed .ux-list-scroll 60vh guess (which lands a few px off on real screens —
   George saw the sliver). Structure: .ux-fit-page > [header flex:none] +
   [.ux-list-grow list] + [compact footer flex:none]. The 100vh - 5rem (top bar
   3.5rem + padding + slack) is the ONE tunable — change it HERE, never per page. */
.ux-fit-page  { display: flex; flex-direction: column; height: var(--fit-page-height); overflow: hidden; }
.ux-list-grow { flex: 1 1 auto; min-height: 0; overflow: auto; }
.ux-list-grow > table thead th { position: sticky; top: 0; background: #f1f3f5; z-index: 1; }

:root {
    --sidebar-width: 250px;
    --scrollbar-width: 17px;
    /* Fit-to-viewport height (top bar 3.5rem + padding + slack). The ONE tunable for any full-height
       page — .ux-fit-page AND the Kanban board. Change it HERE, never per page. */
    --fit-page-height: calc(100vh - 5rem);

    /* Standard READING WIDTH for a form/record page — one column of fields at a comfortable measure, NOT the
       whole viewport. Value is Escalation's Title-box / field-grid width (George 2026-08-14); every record page
       cascades to it so they line up instead of each picking its own. Change it HERE, never per page. */
    --content-width: 900px;

    /* ── TABLE COLUMN WIDTHS (single source of truth) ─────────────────────
       Same reasoning as the z-index ladder below: a width chosen on one page
       must be comparable to the same kind of column on another, or every list
       drifts a few pixels apart and nothing lines up. Pick the semantic name
       that fits the CONTENT, not a pixel value — `var(--col-hours)`, never
       `width:78px`. If a column genuinely doesn't fit a tier, add a tier here
       with a note rather than dropping a literal in the page.

       One column per table is deliberately left UNSIZED so it absorbs the
       slack — normally the name/description. Sizing every column makes the
       table fight the viewport. (Loading Report bug: "By function" was the
       only unsized column, so it soaked up all the space.)  */
    --col-name:     220px;  /* person / project / any primary identifier */
    --col-name-wide:280px;  /* identifiers that routinely wrap (project + subtitle) */
    --col-hours:     78px;  /* an hours figure, incl. 1 decimal ("1234.5") */
    --col-hours-sm:  64px;  /* hours where space is tight (deltas, per-week cells) */
    --col-num:       70px;  /* a plain count or short number */
    --col-pct:       56px;  /* a percentage ("120%") */
    --col-date:     121px;  /* DateFormats.Standard ("15-May-2026"). Was 110px, +10% (George
                               2026-08-08). 110 had never been BINDING anywhere — every table using
                               it was auto-layout, where a width is only a floor and the browser
                               silently grew the column — so it was never tested against a real
                               date. On Open Project at 1rem it wrapped "27-May-2026" onto two
                               lines. Nothing else moves: 110 was already only a floor elsewhere. */
    --col-date-sm:   92px;  /* DateFormats.ShortYear ("15-May-26") */
    /* Backing the .ux-*-col classes above. Carried over at their existing rem
       widths (16px root) so the admin screens don't shift: 18rem / 14rem / 7rem. */
    --col-email:    288px;  /* an email address */
    --col-project:  224px;  /* a project name in a list */
    --col-role:     112px;  /* a role / short enum label */
    --col-label-sm:  84px;  /* a short enum label in a DENSE list, where the header word is the widest
                               thing in the column ("Status", "Your Role"). 75% of --col-role. Earns a
                               tier rather than a literal because two columns in the Open-Project list
                               already share it and any future status column is the same shape; if it
                               never picks up a third caller, collapse it back into --col-role. */
    /* NOTE — a tier earns its place only if the SAME KIND of column recurs across pages. There was a
       `--col-split` here for "a breakdown list in one cell"; it was dropped (George 2026-07-28) because
       that isn't a real category — its width is whatever that one table needs. A name that means
       nothing is worse than a literal: it implies a shared rule that doesn't exist. One-off widths stay
       literal, with a comment saying why. */
    --col-actions:   96px;  /* row action buttons (edit / × delete) */

    /* ── FONT-SIZE LADDER (single source of truth) ────────────────────────
       Same reasoning as the column and z-index ladders: a size chosen on one
       page must be comparable to the same KIND of text on another, or the app
       drifts into a dozen near-identical sizes that read as sloppy. Pick the
       semantic name — var(--font-small) — never a literal.

       --font-min is a FLOOR, not a tier: nothing in the app may be smaller,
       anywhere, for any reason. It exists because small text is where
       readability quietly dies — nobody notices 0.6rem in review, and the
       people it fails are the ones least likely to report it. If something
       "needs" to be smaller than the floor to fit, the layout is wrong, not
       the font.

       Set from the left-panel nav rows, which George judged to sit just ABOVE
       the acceptable minimum (2026-07-28) — so the floor is about half a point
       below them. Sizes found below it at that time and needing a sweep:
       0.75, 0.72, 0.68, 0.6rem, and raw 11px/12px.

       ── --font-body IS THE DEFAULT. --font-small and --font-min MUST BE EARNED.
       Added 2026-08-09 after George looked at the new Project Overview and said
       "I see lots of tiny text". He was right, and NOTHING on that page broke a
       rule: every declaration used a ladder variable and none was below the
       floor. The page was simply written entirely AT the floor — --font-min for
       the score label, the captions and the digest lines, --font-small for the
       rest, and not one line at --font-body.

       A floor prevents the worst case. It does not produce HIERARCHY, and when
       every line is the smallest legal size the page has none: nothing stands
       out, and all of it reads as fine print. So the ladder needs a default as
       well as a floor.

         --font-body   content. The thing the reader came for. START HERE.
         --font-small  dense secondary text next to content that is already body
                       size — table meta, nav rows.
         --font-min    a genuine CAPTION ("not built yet"), and nothing else.

       The guard test cannot catch this, which is worth knowing before trusting
       it: it asserts nothing is BELOW the floor, so a page rendered entirely at
       the floor passes green. A test reporting "clean" about a page nobody can
       read is the same succeeded-did-nothing shape this codebase keeps meeting.
       The floor is machine-checkable; the default is not. */
    --font-min:     0.8rem;   /* HARD FLOOR — never go below this, anywhere */
    --font-small:   0.85rem;  /* dense UI: left-panel nav rows, table meta text */
    --font-body:    1rem;     /* default body copy */
    --font-h2:      1.5rem;   /* page title, e.g. "Open Project" */

    /* ── RADIUS + SHADOW LADDER (single source of truth) ──────────────────
       Same reasoning as the ladders above: cards, panels and modals all round
       corners and cast shadows, and without a ladder every component hand-rolls
       its own (37 box-shadow / border-radius declarations across 13 files at
       last count, none of them shared). The Kanban card is the FIRST consumer;
       the other 13 converge here over time. Pick the semantic name, never a
       literal. */
    --radius-sm:  4px;    /* inputs, chips, small controls */
    --radius:     8px;    /* cards, panels — the default */
    --radius-lg:  12px;   /* large surfaces, modals */
    --shadow-sm:  0 1px 2px rgba(0, 0, 0, 0.08);    /* a resting card */
    --shadow:     0 2px 6px rgba(0, 0, 0, 0.12);    /* a raised panel */
    --shadow-lg:  0 8px 24px rgba(0, 0, 0, 0.20);   /* a dragged card / modal */

    /* ── Z-INDEX LADDER (single source of truth) ──────────────────────────
       Governs CROSS-COMPONENT stacking only. The whole point is that a number
       chosen in one component must be comparable to a number chosen in another
       — so anything that can overlap something from a *different* component
       reads its tier from here instead of inventing a literal.

       LOCAL stacking is deliberately NOT on this ladder: a sticky <thead>
       pinned inside its own scroll box, or the Gantt's sticky left columns,
       compete only with their own siblings, so they keep small local values
       (1, 2, …). Don't promote those to ladder tiers.

       Ordering rationale (low → high):
         content     in-flow page content, the baseline
         sticky      page form submit bars pinned to the viewport bottom
         status      the fixed Avg/queries status cluster (bottom-left)
         sidebar     the left panel AND its Miller-column flyout. MUST sit
                     above page sticky bars (that was the bug: a z:10 submit
                     footer painted over the z:2 sidebar, burying the flyout)
                     and below modals/menus/overlays.
         blazor-err  framework's #blazor-error-ui strip
         modal*      app modals + their backdrops
         messaging   the messaging / my-messages slide-over panels
         overlay     full-screen blocking overlays (wiring, import busy)
         context     right-click context menus + their click-away backdrops */
    --z-content:         1;
    --z-sticky-footer:   10;
    --z-status-cluster:  20;
    --z-sidebar:        100;
    --z-blazor-error:  1000;
    --z-modal-backdrop:1050;
    --z-modal:         1060;
    --z-messaging:     1065;
    --z-overlay:       3000;
    --z-context-menu: 10000;

    /* The ONE "pink" used app-wide to flag a required field that is blank and
       therefore blocking a modal's Create/Save. Single source of truth — every
       edit modal references these vars (and the .field-required-blank class
       below), never a literal, so the shade stays consistent everywhere. */
    --field-required-border: #dc3545;
    --field-required-bg:     #f8d7da;

    /* ── SIGNAL COLOURS — one palette for SignalLevel, app-wide (K20, r4 §3).
       The first shared rendering of a NodeSignal, so this is where the three
       levels get their colours ONCE. Eight dash rows are about to draw them.

       Colour comes from the LEVEL, never from a number: thresholds live with
       the node that knows what its own scale means, so the same amber means
       the same thing on a gate dash and on a KPI dash. If these were picked
       per dash instead, "amber" would quietly become eight different claims.

       -bg is the row/badge fill, -fg its text, -edge its border. Ok is
       deliberately NEUTRAL rather than green: a leaderboard that paints every
       healthy row green makes the two that are not green harder to find, not
       easier — the colour is there to draw the eye to the exception. */
    --signal-ok-bg:     #ffffff;
    --signal-ok-fg:     #1f2328;
    --signal-ok-edge:   #d7dbe0;
    --signal-warn-bg:   #fff4d6;
    --signal-warn-fg:   #6b4e00;
    --signal-warn-edge: #e0b23c;
    --signal-bad-bg:    #fbe3e5;
    --signal-bad-fg:    #8a1c26;
    --signal-bad-edge:  #d3576a;
}

/* Paint a form control pink while it's required-but-blank. Shared by all edit
   modals; the per-field condition (which mirrors the matching IsModalValid
   term) decides WHEN to add the class, this decides how it looks. */
.field-required-blank,
.field-required-blank:focus {
    border-color: var(--field-required-border) !important;
    background-color: var(--field-required-bg) !important;
}

/* Shared page form submit bar — pinned to the bottom of a scrolling form so
   Save/Cancel stay reachable. Replaces a dozen identical inline copies; rides
   the --z-sticky-footer tier so the left-panel Miller flyout (--z-sidebar)
   always wins over it. Compose with Bootstrap's col-12 where the form is a
   grid row: class="col-12 form-sticky-footer".

   PUT THE RESULT/ERROR ALERT INSIDE THIS DIV, not after the form. When the alert
   sits outside, the buttons stay pinned but their own feedback renders below the
   fold — the user presses Save and sees nothing happen (George, Resource Periods).
   Use "mt-2 mb-0 py-2" on the alert so the pinned bar stays compact. */
.form-sticky-footer {
    position: sticky;
    bottom: 0;
    background: #fff;
    padding: 12px 0;
    border-top: 1px solid #dee2e6;
    z-index: var(--z-sticky-footer);
}

/* ── THE busy/loading look (Components/Shared/BusyPanel.razor) ─────────────
   Defined once so every wait state in the app is identical. Six hand-rolled
   spinners had drifted apart before this existed; if the look needs to change,
   it changes HERE and everywhere follows.

   TOP-LEFT, not viewport-centred (George's preference, 2026-07-30). A centred
   panel also moves as content height changes, so the spinner appears to jump
   between screens that are actually doing the same thing. */
.ux-busy {
    display: flex;
    padding: 1.25rem 0 0 0;
}

.ux-busy-box {
    display: flex;
    align-items: center;
    gap: 1rem;
    max-width: 32rem;
}

.ux-busy-title {
    font-size: var(--font-body);
    font-weight: 600;
    color: #495057;
}

.ux-busy-detail {
    font-size: var(--font-small);
    color: #6c757d;
    margin-top: 0.15rem;
}

/* Blocking variant — a scrim over everything, same panel. Used only while a WRITE
   is in flight, where interacting mid-save would be genuinely harmful. */
.ux-busy-blocking {
    position: fixed;
    inset: 0;
    background: rgba(255, 255, 255, 0.94);
    z-index: var(--z-overlay);
    padding: 18vh 0 0 3rem;
}

html, body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

a, .btn-link {
    color: #006bb7;
}

.btn-primary {
    color: #fff;
    background-color: #1b6ec2;
    border-color: #1861ac;
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
  box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}

.content {
    padding-top: 1.1rem;
}

h1:focus {
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: #e50000;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}

/* ── Miller-column cascade (shared: My Messages §3.9, Profile §3.6.1) ──
   Extracted from MyMessagesPanel so both share one column engine. Global (not
   scoped) so a caller's <TrailingColumn> can reuse .cascade-col /
   .cascade-col-head for a seamless last column. The flex container is sized by
   its parent (e.g. the My Messages flyout body); these rules style the columns
   and rows only. */
.cascade-cols {
    flex: 1 1 auto;
    display: flex;
    min-height: 0;
}
.cascade-col {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    border-right: 1px solid #e9ecef;
}
.cascade-col:last-child {
    border-right: none;
}
.cascade-col-head {
    position: sticky;
    top: 0;
    background: #f1f3f5;
    border-bottom: 1px solid #dee2e6;
    padding: 0.35rem 0.6rem;
    font-size: var(--font-min);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: #6c757d;
}
.cascade-empty {
    padding: 0.6rem;
    color: #6c757d;
    font-size: 0.85rem;
}
/* Row — a label that ellipsizes, with a count/alarm pill pinned right.
   Selected row tinted with a blue left accent. */
.cascade-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.4rem;
    width: 100%;
    text-align: left;
    background: none;
    border: none;
    border-bottom: 1px solid #f1f3f5;
    padding: 0.4rem 0.6rem;
    font-size: 0.85rem;
    cursor: pointer;
}
.cascade-item:hover {
    background: #e7f1ff;
}
.cascade-item.sel {
    background: #cfe2ff;
    box-shadow: inset 3px 0 0 0 #0d6efd;
    font-weight: 600;
}
.cascade-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* Red status dot (late / unread) — same red as the count pill, used where a
   boolean state needs a marker instead of a number. Shared by the cascade
   alarm and the My Messages unread indicator. */
.cascade-dot {
    flex: 0 0 auto;
    width: 10px;
    height: 10px;
    border-radius: 999px;
    background: #dc3545;
}

/* ═══ K20 · DASH CHROME — scope picker + leaderboard + score badge ═══════════
   ProjectNotebookSpec r4 §3.1/§3.2/§3.3. ONE position and ONE appearance on
   every dash, so a reader who has learned the block on one dash has learned it
   on all eight. A dash does not restyle it — that is the whole point of the
   block, and it is why these rules live here rather than in eight .razor.css
   files where they would drift apart quietly.

   FONT LADDER: the rows are CONTENT, so they are --font-body. This block is the
   counter-example to the Project Overview George looked at on 2026-08-09 and
   called "lots of tiny text" — that page used a ladder variable everywhere and
   still had no hierarchy, because every line sat at the floor. --font-small
   appears here exactly twice, on the two labels that sit NEXT TO body-size
   content, and --font-min not at all. */
.dash-chrome {
    margin-bottom: 1rem;
}
.dash-chrome-bar {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    margin-bottom: 0.5rem;
}
.dash-scope {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin: 0;
}
.dash-scope-label {
    font-size: var(--font-small);
    color: #6c757d;
}
.dash-scope-select {
    font-size: var(--font-body);
    padding: 0.25rem 0.5rem;
    border: 1px solid #ced4da;
    border-radius: var(--radius-sm);
    background: #fff;
    min-width: var(--col-project);
}

/* THE RESERVED CORNER (§3.3). It is a fixed slot, not a flex item that grows —
   the badge must land in the same place whether or not the picker beside it is
   wide, because the one position the reader has learned is the one thing here
   that must never move. Nothing else may occupy it: no close button, no
   overflow menu, no action control, on any page in this design. */
.dash-chrome-corner {
    flex: 0 0 auto;
    min-height: 1px;   /* holds the corner open when the badge renders nothing */
}
/* Page-supplied bar content where the leaderboard's summary was (scope-picker-stays, 2026-08-16) — sits
   between the scope picker and the reserved corner; the page styles its own inner content. */
.dash-chrome-extra {
    flex: 0 1 auto;
    align-self: center;
    margin-right: auto;
    font-size: var(--font-small, .85rem);
}
.score-badge {
    display: inline-flex;
    align-items: baseline;
    gap: 0.3rem;
    padding: 0.25rem 0.6rem;
    border: 1px solid var(--signal-ok-edge);
    border-radius: var(--radius-sm);
    background: var(--signal-ok-bg);
    color: var(--signal-ok-fg);
}
.score-badge.score-warn {
    border-color: var(--signal-warn-edge);
    background: var(--signal-warn-bg);
    color: var(--signal-warn-fg);
}
.score-badge.score-bad {
    border-color: var(--signal-bad-edge);
    background: var(--signal-bad-bg);
    color: var(--signal-bad-fg);
}
.score-badge-value {
    font-size: var(--font-h2);
    font-weight: 600;
    line-height: 1;
}
.score-badge-unit {
    font-size: var(--font-small);
}

/* WIDTH VARIES, COLUMNS DO NOT MOVE (§3.2). The table sizes to its content
   rather than filling the row, so a dash with no score is VISIBLY narrower —
   that narrowness is the signal "this dash has no score", and it only reads
   that way if the block does not stretch to the same width regardless. */
.dash-lb {
    border-collapse: collapse;
    font-size: var(--font-body);
    width: auto;
}
.dash-lb th {
    text-align: left;
    font-size: var(--font-small);
    font-weight: 600;
    color: #6c757d;
    border-bottom: 1px solid #dee2e6;
    padding: 0.3rem 0.75rem 0.3rem 0.5rem;
}
.dash-lb td {
    padding: 0.35rem 0.75rem 0.35rem 0.5rem;
    border-bottom: 1px solid #f1f3f5;
}
.dash-lb .dash-lb-subject { min-width: var(--col-name); }
.dash-lb .dash-lb-n       { width: var(--col-num); text-align: right; }
.dash-lb .dash-lb-due     { width: var(--col-date); white-space: nowrap; }
.dash-lb .dash-lb-status  { width: var(--col-name); white-space: nowrap; }

/* The heading's right entry is a FIGURE, not a column label — "Total Late  3".
   Right-aligned and heavier so it reads as the answer to the heading beside it
   rather than as a header for the Status cells below. */
.dash-lb .dash-lb-latecount {
    text-align: right;
    font-weight: 700;
}
.dash-lb .dash-lb-score   { width: var(--col-name); white-space: nowrap; }

/* ── ROW TONES (George 2026-08-15) ────────────────────────────────────────────
   Late red · Hold/Cancelled gray · otherwise green, all in bold white.

   SOLID FILLS, not the pale signal-* tints the level classes use. These say what
   a row IS rather than tinting how worried to be, and George asked for white bold
   lettering — which needs a saturated ground to stay legible. The three colours are
   the canonical ones (SkylineDimensions Red / Green, and the gray that file reserves
   for de-emphasis and explicitly NOT for health), so a dash row and a Skyline brick
   never disagree about what red means.

   Gray for Hold/Cancelled is the point of having three: those rows are outside every
   "what is stuck" count and outside the score's denominator, so they must read as
   neither good nor bad. Painting them green would say "fine"; red would say "act". */
.dash-lb-row.tone-late,
.dash-lb-row.tone-ok,
.dash-lb-row.tone-muted {
    color: #fff;
    font-weight: 700;
}

.dash-lb-row.tone-late  { background: #d64545; }
.dash-lb-row.tone-ok    { background: #3a9d4e; }
.dash-lb-row.tone-muted { background: #9e9e9e; }

/* The subject cell is a link on a clickable row; on a toned row it must stay white
   rather than falling back to link blue on a saturated ground. */
.dash-lb-row.tone-late .dash-lb-link,
.dash-lb-row.tone-ok .dash-lb-link,
.dash-lb-row.tone-muted .dash-lb-link {
    color: #fff;
}

.dash-lb-row.lvl-warn {
    background: var(--signal-warn-bg);
    color: var(--signal-warn-fg);
}
.dash-lb-row.lvl-bad {
    background: var(--signal-bad-bg);
    color: var(--signal-bad-fg);
}
/* Only a row that DOES something gets the pointer. A row with neither a scope
   id nor a link is not clickable and must not look it — a cursor that promises
   a drill-down which never arrives is a worse lie than a plain row. */
.dash-lb-click {
    cursor: pointer;
}
.dash-lb-click:hover {
    outline: 2px solid #0d6efd;
    outline-offset: -2px;
}
.dash-lb-avg {
    font-weight: 600;
}
/* The worst case is the reason anybody opened the dash, so it is not shrunk
   into fine print beside the average — same size, lighter weight. */
.dash-lb-low {
    margin-left: 0.3rem;
    color: inherit;
    opacity: 0.85;
}
.dash-lb-unit {
    margin-left: 0.3rem;
    font-size: var(--font-small);
    color: #6c757d;
}
/* A data problem, not a missing figure — it says what is wrong in words rather
   than leaving a gap the reader would read as a failed load. */
.dash-lb-mixed {
    font-style: italic;
    color: var(--signal-warn-fg);
}
.dash-lb-empty {
    font-size: var(--font-body);
    color: #6c757d;
    margin: 0.5rem 0;
}

/* ═══ USER DELETE — the reassignment dialog ═════════════════════════════════
   Refuses a delete that would leave a leaver's name on live work. The NAMES are
   the actionable content here, so they sit at --font-body: a list of affected
   projects the admin has to act on is not fine print. Counts and hints are the
   secondary text NEXT TO them, which is what --font-small is for. */
.udb-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: var(--z-modal-backdrop);
    display: flex;
    align-items: center;
    justify-content: center;
}
.udb-panel {
    background: #fff;
    border-radius: var(--radius);
    padding: 24px;
    width: 90%;
    max-width: 720px;
    max-height: 85vh;
    overflow-y: auto;
    box-shadow: var(--shadow-lg);
    z-index: var(--z-modal);
}
.udb-title {
    margin: 0 0 0.5rem 0;
}
.udb-lead {
    font-size: var(--font-body);
    color: #333;
    margin: 0 0 1rem 0;
}
.udb-cat {
    border: 1px solid #dee2e6;
    border-radius: var(--radius-sm);
    padding: 0.6rem 0.75rem;
    margin-bottom: 0.6rem;
}
.udb-cat-head {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
}
.udb-cat-name {
    font-size: var(--font-body);
    font-weight: 600;
}
.udb-cat-count {
    font-size: var(--font-small);
    color: #6c757d;
}
/* Leadership. Amber rather than red: this is a constraint on the CHOICE, not an
   error the admin has made. */
.udb-must {
    font-size: var(--font-small);
    color: var(--signal-warn-fg);
    background: var(--signal-warn-bg);
    border: 1px solid var(--signal-warn-edge);
    border-radius: var(--radius-sm);
    padding: 0 0.4rem;
}
.udb-names {
    font-size: var(--font-body);
    color: #212529;
    margin: 0.35rem 0 0.5rem 0;
    word-break: break-word;
}
.udb-actions {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
}
.udb-select {
    font-size: var(--font-body);
    padding: 0.25rem 0.5rem;
    border: 1px solid #ced4da;
    border-radius: var(--radius-sm);
    min-width: var(--col-name);
}
.udb-clear {
    font-size: var(--font-body);
    display: flex;
    align-items: center;
    gap: 0.3rem;
    margin: 0;
}
/* Says WHICH row is holding the delete, next to that row — a disabled button
   with no reason is a dead end the admin cannot debug. */
.udb-pending {
    font-size: var(--font-small);
    color: var(--signal-warn-fg);
}
.udb-foot {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5rem;
    margin-top: 1rem;
}
.udb-blocked {
    font-size: var(--font-small);
    color: #6c757d;
    margin-right: auto;
}