/* ── 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; }

/* ── FilteredPicker (Components/Shared) ──────────────────────────────────────
   The rows matching the typed text sit under the box in a bounded scroll box, so
   a long list never pushes the rest of the window off-screen; empty once a row
   is picked. Tune the height HERE and every picker follows. */
.filtered-picker-list { max-height: 10rem; overflow: auto; font-size: var(--font-small); }

/* ── 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.
         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
         blazor-err  framework's #blazor-error-ui strip — THE CEILING, see below */
    --z-content:         1;
    --z-sticky-footer:   10;
    --z-status-cluster:  20;
    --z-sidebar:        100;
    --z-modal-backdrop:1050;
    --z-modal:         1060;
    --z-messaging:     1065;
    --z-overlay:       3000;
    --z-context-menu: 10000;

    /* ⛔⛔ THE STRIP THAT SAYS THE APPLICATION IS DEAD OUTRANKS EVERYTHING, AND THAT IS NOT A
       PREFERENCE BETWEEN TIERS. #blazor-error-ui is unhidden by blazor.web.js only on an unhandled
       circuit exception. At that moment every control still on screen is non-functional — the circuit
       carrying their handlers is gone — so there is nothing behind it worth seeing, and the cost of
       putting it on top is nil. Any ordering that lets an app modal cover it hides the only remaining
       explanation behind something that can no longer respond.

       ⛔ IT USED TO SIT AT 1000, BELOW THE MODAL TIER, AND THAT WAS THE DEFECT (intake #70). A Notice
       whose buttons had died with the circuit was painted over the strip by its own full-viewport
       backdrop, which also INTERCEPTED the click meant for Reload and handed it to a Dismiss handler
       needing the dead circuit. The user pressed the one control that would have recovered the page and
       got the same nothing as every other button.

       ⛔ DO NOT "TIDY" THIS BACK INTO NUMERICAL ORDER WITH THE LIST ABOVE. It is deliberately last and
       deliberately far: a survey on 2026-09-08 found app z-indexes as high as 10050 (SplitTaskModal),
       10001 (VpmView), 10000 (TaskContextMenu) and 9999 (ErrorBanner), several of them literals that do
       not read this ladder at all. A value merely "above the modal tier" would have lost to six of them.
       This is the ceiling rather than a tier, so nothing can be added above it by accident.

       ⚠ The reconnect dialog still wins, correctly, and no number here can change that: ReconnectModal
       is a <dialog> opened with showModal(), which the browser promotes to the TOP LAYER above every
       z-index in the document. A merely-dropped connection should show "Rejoining" rather than this. */
    --z-blazor-error: 2147483647;

    /* 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;

    /* ── SPEC INTAKE COLOURS — the one place. The overview pill (six ordered
       cases, Services/SpecIntakePill) and the page's status cells both read
       these by name; retuning is this block and nothing else. Names match the
       spec tokens (SpecIntakeEmptyGray → --spec-intake-empty-gray, etc.).

       ⚠ STARTER VALUES chosen to match the names — George tunes them later. Do
       not hardcode a colour in component logic; add or change a shade HERE. */
    --spec-intake-empty-gray:          #ced4da;  /* no rows — muted, recedes */
    --spec-intake-blocked-red:         #dc3545;  /* a non-terminal row is blocked */
    --spec-intake-recommended-amber:   #f0ad4e;  /* a Conductor recommendation is queued */
    --spec-intake-next-up-pale-yellow: #ffe08a;  /* the PM moved a row to Next Up */
    --spec-intake-complete-green:      #198754;  /* every row terminal */
    --spec-intake-backlog-blue:        #0d6efd;  /* backlog remains */
    /* status-cell only (not a pill case) — the page's Status column reads these */
    --spec-intake-folded-gray:         #adb5bd;
    --spec-intake-not-now-gray:        #868e96;
    --spec-intake-converted-dark-green:#0f5132;
    /* Closed-Other — settled, nothing built, do not look again (220480). Its own gray, distinct from
       both Folded (#adb5bd) and Not Now (#868e96): all three are finished, and a reader comparing rows
       must be able to tell "covered elsewhere" from "deferred" from "settled with nothing built". */
    --spec-intake-closed-other-gray:   #6c757d;
    /* Recommended for NOT Now — its own amber (Beck's ruling 7, 2026-08-30). It and
       Recommended for Next Up sit at sort positions 3 and 4; sharing one amber, a user
       cannot tell "we should" from "we should not" at a glance, which is the entire
       difference between them. ⚠ The PILL still folds both into --spec-intake-recommended-amber
       (spec's pill table, unchanged) — a pill summarises a project, a cell compares rows. */
    --spec-intake-not-recommended-amber:#c77b1f;
    /* Next Up Rejected (3B) — state 1 handed the row back, and it is the PM's move.
       ⭐ ROSE RATHER THAN A GRAY BECAUSE IT IS ACTIONABLE, NOT FINISHED. The three grays above all
       mean "this row's life ended"; this one means the opposite — somebody has to look at it again,
       which is why it also sorts at rank 2 rather than into the finished block.
       ⚠ DELIBERATELY NOT --spec-intake-blocked-red (#dc3545). That red already has a job on this
       page and it is a different one: it paints BLOCKER TEXT, and a row can be blocked and rejected
       at once. Wearing one colour for both would make the two unreadable exactly where they overlap.
       Distinct from Next Up's pale yellow (#ffe08a) too — the pair a reader most needs to tell apart,
       since a rejected row is one PM decision away from being a Next Up row again.
       ⚠ STARTER VALUE, like every shade in this block. George tunes it; it is one value here and
       nothing else changes. */
    --spec-intake-next-up-rejected-rose:#d6336c;
    /* Deleted (319800) — the row is hidden and never painted; the token exists because every status
       must resolve to a defined colour (the enum-driven catalogue test), not because a cell shows it.
       A gray distinct from the three finished grays so a hidden row surfacing by fault is tellable. */
    --spec-intake-deleted-gray:        #495057;
    /* NOT a spec case — the fall-through colour for a status with no pill case (an 8th
       status added to the enum). Deliberately LOUD and off-palette so it reads as broken,
       not as one more state; the pill's job there is to make the gap seen, not hide it. */
    --spec-intake-unclassified:        #ff00ff;
}

/* ── SPEC INTAKE TABLE GEOMETRY (George, 2026-08-31) ──────────────────────────
   ⛔ THE WIDTHS ARE IN INCHES BECAUSE HE ASKED IN INCHES. Kept unconverted on
   purpose: when the next request says "make Spec/Use Case 3 inches" the number
   here is the number he said, and nobody has to trust a px conversion to check
   it. This is the one-off case the --col-* ladder's own note allows — a literal
   WITH a reason — not a new tier, because these widths describe THIS table and
   naming them would imply a shared rule that does not exist.

   ⛔⛔ `table-layout: fixed` IS LOad-BEARING, NOT TIDYING. Under the default
   auto layout a width is only a FLOOR — the browser silently grows a column to
   fit its content, which is exactly what these three long free-text columns
   would do. The ladder's --col-date note records the same trap being missed
   once already. Without this line the widths below render and do nothing, and
   the failure looks like the CSS never loaded.

   ⚠ AND `width: auto` GOES WITH IT. Bootstrap's .table sets width:100%; under
   fixed layout that scales every column up proportionally to fill the viewport,
   so the inches would be right relative to each other and wrong absolutely. */
.spec-intake-table            { table-layout: fixed; width: auto; }
.spec-intake-table td         { vertical-align: top; }
.spec-intake-table .si-num    { width: 0.5in; }
.spec-intake-table .si-name   { width: 1.8in; }
/* 165px (George, 2026-09-01), down from var(--col-name) = 220px. The 55px it gives up is
   most of what pays for the new Last Change column.

   ⚠ A LITERAL RATHER THAN A LADDER TIER, AND DELIBERATELY. The ladder rule at the top of this
   file says a genuinely one-off width stays literal WITH a comment saying why: this is his
   eye-measured value for THIS table's owner column, not a new opinion about how wide a display
   name should be everywhere. Moving --col-name would drag every other table with it. */
.spec-intake-table .si-owner  { width: 165px; }
/* "5 Blocker" is the widest cell (299213); the same width as the # column plus its word. */
.spec-intake-table .si-priority { width: 0.9in; }
/* ⛔ AFTER THIS RULE, EVERY COLUMN IN THIS TABLE IS SIZED — AND THAT IS INTENDED, NOT AN
   OVERSIGHT (George, 2026-09-01). The ladder note at the top of this file says one column per
   table is left UNSIZED so it absorbs the slack, and Status was that column here. It absorbs
   nothing now.

   He ruled it deliberately: Status could not give up the space, so he reversed his own opening
   constraint and ACCEPTED HORIZONTAL SCROLL rather than shave the three si-text columns.
   ⛔ Do not "fix" the scroll by narrowing si-text — that was proposed and ruled against.

   ⚠ The slack was never being stretched into anyway: `.spec-intake-table` is `width: auto`, so
   the sum of the columns IS the table width. Sizing the last one does not change the mechanism,
   only removes the last column that could quietly grow.

   ⚠ 176px is George's estimate off a screenshot, not a measurement. One word to change if it
   looks wrong to him. */
.spec-intake-table .si-status { width: 176px; }
/* 3.38in = 2.6in + 30% (George, 2026-08-31). Written as the result rather than a calc() so the
   number in the file is the number on the screen — the next request will be another absolute. */
.spec-intake-table .si-text   { width: 3.38in; }

/* The per-cell cap. A <td> does not honour max-height, so the scroller is this
   inner element; every cell that can overflow carries one. Cells that cannot
   (the number, the status badge) deliberately do not — a scrollbar that can
   never appear is still a box that can clip a badge. */
.spec-intake-table .si-cell {
    max-height: 2.2in;
    overflow-y: auto;
    overflow-x: hidden;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* 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;
}