/* ============================================================
   Design tokens — defined once, consumed by all components.
   Light theme by default; the dark theme is a token override on
   .app-shell.theme-dark. "Pro Tools density": 11–12px type,
   28–32px rows, 1px borders, no shadows, no rounded corners.
   ============================================================ */
:root {
    --color-bg: #f2f3f6;
    --color-surface: #ffffff;
    --color-surface-alt: #e8eaee;      /* weekend / off shading */
    --color-border: #d0d4da;
    --color-border-strong: #aab0b8;
    --color-text: #24262a;
    --color-text-muted: #6a6f77;
    --color-accent: #1f6fc4;
    /* Info strip band (Robert's pick 2026-07-21, was quiet-tint #e3edf8 light /
       #1c2634 dark): a solid cyan the strip keeps in BOTH themes — like the
       delete/assign mode strips it owns its ink colours (see .panel-info), so
       the theme's text tokens never touch it. */
    --color-info-bg: #71c0d9;

    /* Verdict states (used from the drag phase onward, part of the token vocabulary) */
    --color-warning: #a87404;
    --color-breach: #c22c20;

    /* Assign sub-panel + engaged-mode surfaces — solid green with WHITE text,
       the delete-mode strategy in green (round-6 gallery option D, chosen
       2026-07-21). Earlier tries: #e6f0e8 original quiet tint; #abdeb6;
       #7abb74 too dark for black text. */
    --color-assign-bg: #3e7c4d;
    --color-assign-border: #2b5836;

    /* Reorder mode's band (Robert, 2026-08-10): solid purple with white text —
       the delete/assign strategy in a third hue, so arranging the rows is as
       unmistakable as a live write mode. The SAME colour in both themes, like
       the info band's cyan: the strip owns its ink, so nothing themed touches it. */
    --color-reorder-bg: #6b3fa0;
    --color-reorder-border: #482a6e;

    /* Quiet well behind inputs and hovered ghost buttons */
    --color-field: #e9ecef;

    /* Unassigned (placeholder) rows' cell colour (2026-08-10; Robert picked
       antiquewhite the same day, replacing the first cut's light gray): the
       SAME colour in both themes, like the info strip's cyan band — which is
       why the name-cell inks below are fixed rather than themed. The name
       cell adds the stripe pattern — colour is never the only cue. */
    --color-placeholder-bg: #faebd7;

    /* Time-of-day gridlines (06:00/12:00/18:00), shown only when the grid is
       time-zoomed in far enough — see .roster-grid.time-ticks. Slightly stronger
       than a cell border so it reads as a deliberate marker, not chrome. */
    --tick-line: rgba(0, 0, 0, 0.16);

    /* Job block palette — muted, readable with light text */
    --job-color-1: #3d6fa8;
    --job-color-2: #4e8a5a;
    --job-color-3: #8a5aa0;
    --job-color-4: #a8763d;
    --job-color-5: #4d8f8f;
    --job-color-6: #96566a;

    /* The logo (2026-10-06; docs/logo is its place of truth, LogoSymbol draws it).
       --logo-ink fills the symbol's block and, on the public pages, the name beside
       it: the design's dark teal, turned white by the dark theme, which is the
       design's own on-dark version. --logo-accent is the cyan square in both themes.
       It is the info band's colour today but its own token, so the band can change
       colour without the logo following it. --font-logo sets the name: Archivo,
       served by the app itself (the @font-face beside .logo-name), with the system
       font standing in until it loads. */
    --logo-ink: #16323b;
    --logo-accent: #71c0d9;
    --font-logo: "Archivo Logo", var(--font-family);

    --font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
    --font-size-base: 12px;
    --font-size-small: 10px;
    --row-height: 30px;
    /* How far a block sits inside its cell, top and bottom. A TOKEN rather than a
       literal since 2026-09-08, because three other layers are positioned against it:
       the fill view's time ribbon, the tags-mode row height, and the Pivot View's faint
       "elsewhere" block. The heat map widens it (see [data-heat]) so the cell's colour
       shows as a band above and below each block instead of a 3px halo — in the default
       fill view a lone block tiles its whole cell. Move it here or they part company. */
    --block-inset: 3px;
    --panel-row-height: 36px;
    /* MINIMUM day-column width (2026-07-19, was fixed): short ranges stretch the
       columns to fill the viewport (minmax(--day-width, 1fr)); long ranges sit at
       this floor and the grid scrolls. Raise for time-axis zoom (post-v1). */
    --day-width: 100px;
    /* The employee-name column: content-sized at rest — fit-content(200px)
       with a 160px floor (min-width on the corner and name cells; fit-content
       can't nest in minmax()) — and FIXED at 290px in REORDER MODE ONLY (the
       .roster-grid[data-mode="reorder"] token override below). History: a
       content-sized track re-sizes whenever its content changes, and reorder
       mode changes it mid-use — hovering a guest row's ✕ reveals "Remove from
       department", which took the track from its floor to its cap and shifted
       every day column sideways — so on 2026-08-10 the track went fixed-290
       everywhere ("the column should already BE at its maximum"). Robert found
       290 too wide outside reorder the same day, so the pin is now scoped to
       the one mode whose content actually moves; every other mode's name-cell
       content is hover-stable, so content sizing is safe there. 290 is
       measured to fit a name, the "(guest)" marker, the drag handle AND that
       revealed button with no ellipsis. */
    --name-col-width: fit-content(200px);
}

/* Reorder mode pins the name column at its maximum — the revealed guest-✕
   label has nothing to grow into, so nothing shifts under the pointer. A
   token override, like themes and zoom; the grid's inline template reads
   var(--name-col-width), and the attribute rides the same element. */
.roster-grid[data-mode="reorder"] {
    --name-col-width: 290px;
}

*, *::before, *::after {
    box-sizing: border-box;
}

html, body {
    height: 100%;
    margin: 0;
}

body {
    background: var(--color-bg);
    color: var(--color-text);
    font-family: var(--font-family);
    font-size: var(--font-size-base);
    line-height: 1.3;
}

h1:focus {
    outline: none;
}

.blazor-error-boundary {
    background: #b32121;
    padding: 4px 10px;
    color: white;
}

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

/* ------------------------------------------------------------
   App shell: action panel on top, grid gets every remaining
   pixel. The light theme overrides tokens here.
   ------------------------------------------------------------ */
.app-shell {
    display: flex;
    flex-direction: column;
    height: 100vh;
    background: var(--color-bg);
    color: var(--color-text);
    color-scheme: light;
}

.app-shell.theme-dark {
    --color-bg: #131417;
    --color-surface: #1a1c20;
    --color-surface-alt: #17181c;
    --color-border: #2c2f36;
    --color-border-strong: #41454e;
    --color-text: #d6d8dc;
    --color-text-muted: #83878f;
    --color-accent: #4d9fec;
    --color-info-bg: #71c0d9;   /* same band both themes — the strip sets its own ink */
    --color-warning: #d9a021;
    --color-breach: #d94f43;
    --color-assign-bg: #35693f;
    --color-assign-border: #244a2c;
    --color-field: #24262c;
    /* --color-placeholder-bg deliberately NOT overridden: antiquewhite in both
       themes (Robert, 2026-08-10 — the info-strip precedent). */
    --tick-line: rgba(255, 255, 255, 0.16);
    /* The logo's block goes white on the dark bar, as the design draws it; its cyan
       square stays the same. */
    --logo-ink: #ffffff;
    color-scheme: dark;
}

.roster-area {
    flex: 1;
    overflow: auto;
}

/* ------------------------------------------------------------
   Action panel — two dense rows: controls + info strip.
   ------------------------------------------------------------ */
.action-panel {
    flex: none;
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border-strong);
}

/* ------------------------------------------------------------
   The system-wide banner (2026-08-19) — a System Admin's message
   across the very top of the app, for everyone signed in.

   ROBERT'S OWN FOUR DECLARATIONS, same day, replacing the amber
   first cut: 75px tall, red, 16px, white — and the "Notice" label
   removed with them, so the band carries the message and nothing
   else. The colour-is-never-alone rule is still kept, and by the
   strongest thing there is: the whole content IS the sentence
   somebody wrote, so a reader who cannot see the red reads the
   words.

   FIXED COLOURS IN BOTH THEMES. The info strip's cyan and the
   unassigned rows' antiquewhite set that precedent, and the reason
   is the same — a band everybody must read identically cannot
   inherit a palette that moves under it.

   Centred both ways: the message is a sentence or two, and one
   starting at the left edge of a 75px band reads as a caption
   rather than an announcement. The height is FIXED at Robert's
   number, so a long message scrolls inside it rather than eating
   the roster — box-sizing is border-box app-wide, so the padding
   is inside the 75px.

   It sits INSIDE .action-panel (see SystemBanner.razor for why),
   so it needs no grid placement and cannot disturb the docked
   layouts.
   ------------------------------------------------------------ */
.system-banner {
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 6px 12px;
    height: 75px;
    background: red;
    font-size: 16px;
    color: white;
    line-height: 1.35;
    overflow-y: auto;
}

/* min-width: 0 is what lets a long unbroken word wrap inside a flex item
   instead of pushing the band wider than the screen; max-width keeps a long
   message inside the band while justify-content still centres a short one. */
.system-banner .note-display {
    min-width: 0;
    max-width: 100%;
}

.panel-row {
    min-height: var(--panel-row-height);
    display: flex;
    align-items: center;
    flex-wrap: wrap;   /* narrow screens: the row grows instead of clipping */
    /* Wrapped controls hug the right edge instead of the left. Line 1 is
       unaffected — the flex:1 .panel-spacer eats all its slack, so there is
       nothing for justify-content to distribute there; only overflow lines
       (which carry no spacer) get right-aligned. */
    justify-content: flex-end;
    gap: 4px 8px;
    padding: 3px 10px;
}

/* Info strip — emphasised so the details don't get lost. Chosen look (round 3, "Larger
   type"): 14px body, 15px job title. Since 2026-07-19 (round-5 variant B + right edge):
   a fixed 80px band of three centred lines — job / who & when / note — centred on the
   full panel width, with the provenance block pinned to the right edge. Layout classes
   live in the markup (Bootstrap flex utilities); only look-and-feel stays here. */
/* On the #71c0d9 band every ink is fixed here rather than themed: the theme
   text tokens (light grey in dark mode) fail on a mid-cyan. All inks are
   teal-tinted darks ≥4.5:1 against the band. */
.panel-info {
    border-top: 1px solid var(--color-border);
    background: var(--color-info-bg);
    border-left: 3px solid #2f7a93;   /* darker same-hue bar, the mode strips' pattern */
    color: #16323b;
    font-size: 14px;
    height: 80px;
    padding: 3px 10px;
}

.panel-info .info-sep {
    height: 18px;
}

/* Delete mode: unmistakable. Declared after the base look so it always wins. */
.panel-info.delete-mode {
    background: var(--color-breach);
    border-left-color: #6e0f08;
    color: #ffffff;
}

.panel-info.delete-mode .muted,
.panel-info.delete-mode .notice,
.panel-info.delete-mode .prov-label,
.panel-info.delete-mode .info-note-right {
    color: #ffffff;
}

.panel-info.delete-mode .info-sep {
    background: rgba(255, 255, 255, 0.55);
}

/* Add and Notes: the engaged green, BACK on the band (Robert, 2026-08-13).
   The history, because this rule has been here before: the green painted the
   band for both modes until each mode's controls moved into a dock (Notes
   2026-08-10, Add 2026-08-12), and it came out with them — a coloured strip
   with none of the mode's content in it said "a mode is on" twice over. What
   that missed is that both modes make the NEXT CLICK write: clicking a cell
   adds a shift, clicking an entry stamps a note over whatever was there. That
   is the same problem Delete's red has always guarded against, so it gets the
   same guard, and the reasoning is now the other way round — the band is
   painted by every mode whose click writes, wherever its controls live.
   What did NOT come back is the old treatment's other half: the bold centred
   "Add Shift Mode" line that REPLACED the hover content. Hover details render
   on the green (a planner reads an entry's existing note before stamping over
   it), and the mode's name appears only in the placeholder, when nothing is
   hovered. The status bar stays unpainted too — it was the third piece of the
   2026-07-21 treatment and reads as a bug on its own.
   Colour mode paints too, but with its own striped surface — see
   .panel-info.colour-mode below.
   Ink is fixed white like delete's and reorder's — the whole band is repainted,
   so nothing themed may touch it. */
.panel-info.assign-mode {
    background: var(--color-assign-bg);
    border-left-color: var(--color-assign-border);
    color: #ffffff;
}

.panel-info.assign-mode .muted,
.panel-info.assign-mode .notice,
.panel-info.assign-mode .prov-label,
.panel-info.assign-mode .info-note-right {
    color: #ffffff;
}

.panel-info.assign-mode .info-sep {
    background: rgba(255, 255, 255, 0.55);
}

/* Colour mode (Robert, 2026-08-13 — round-11 gallery option B, "pastel
   stripes"): three broad diagonal tints, the flag geometry softened until the
   band's dark teal ink passes on every stripe. Stripes of several colours are
   the one surface that says "colour" without endorsing a hue — a single hue
   here would either collide with the mode vocabulary (green, red, purple) or
   read as an endorsement of one of the organisation's own palette colours,
   which is why this band never took the solid treatment. The old "no mode hue
   beside the swatches" objection belongs to the DOCK, where the picker lives —
   and since later that day .mode-dock-head is plain for EVERY dock, so it is
   satisfied everywhere; the band holds no swatch of its own. Saturated and
   deepened variants were mocked and rejected in the
   same round: dark ink is borderline on the saturated flag, and white ink
   forces the yellow to ochre. Every stripe is light, so the band keeps its
   base dark inks — no white overrides, unlike the solid mode bands. Fixed in
   both themes, like every band: the strip owns its colours. */
.panel-info.colour-mode {
    background: linear-gradient(112deg, #cfe4d0 0 34%, #f5e9c2 34% 66%, #f0d2cc 66% 100%);
    border-left-color: #8fb996;
}

/* The provenance block, the note and (2026-09-10) the Fatigue Index line occlude a
   wide centre line running under them with background: inherit — which on a GRADIENT
   re-renders the whole gradient inside the small box, a smeared patch (found building
   the round-11 mocks). On the striped band they go transparent instead; the imperfect
   occlusion is accepted, since the centre block rarely reaches either flank.
   Since .info-detail passes background-COLOR down rather than the shorthand, the
   gradient can no longer reach a child at all and these three are belt and braces —
   kept because they are where the intent is written. */
.panel-info.colour-mode .info-provenance,
.panel-info.colour-mode .info-note-right,
.panel-info.colour-mode .info-fatigue {
    background: transparent;
}

/* Reorder mode (2026-08-10): solid purple with white text, the delete-mode
   strategy in a third hue. Declared after the base look so it always wins;
   the ink overrides mirror delete's, since the whole band is repainted. */
.panel-info.reorder-mode {
    background: var(--color-reorder-bg);
    border-left-color: var(--color-reorder-border);
    color: #ffffff;
}

.panel-info.reorder-mode .muted,
.panel-info.reorder-mode .notice,
.panel-info.reorder-mode .prov-label,
.panel-info.reorder-mode .info-note-right {
    color: #ffffff;
}

.panel-info.reorder-mode .info-sep {
    background: rgba(255, 255, 255, 0.55);
}

.panel-info .info-mode-label {
    font-weight: 700;
}

/* The reorder band's one line, which replaces the hover prompt rather than
   sitting above it — bigger than any other strip text, because it is the
   whole message. */
.panel-info .info-mode-label-lg {
    font-size: 20px;
}

/* The exit button sitting under that line: outlined white on the purple, which
   is the ghost-button idea in a band that has taken the surface away — a
   transparent button with no border would read as text, not a control. Fills
   white on hover (dark ink flipping in), so it can never be the grey the base
   hover would give it.
   Every selector here is qualified with .btn deliberately: the shared `.btn,
   .select` block and `.btn:focus-visible` are declared LATER in this file, so a
   bare .reorder-exit would lose the colour and the border to them on source
   order alone. */
.btn.reorder-exit {
    border-color: rgba(255, 255, 255, 0.75);
    color: #ffffff;
}

.btn.reorder-exit:hover {
    background: #ffffff;
    border-color: #ffffff;
    color: var(--color-reorder-bg);
}

/* The app-wide focus ring is --color-accent, which all but disappears on the
   purple; white keeps the keyboard route visible. */
.btn.reorder-exit:focus-visible {
    outline-color: #ffffff;
}

.panel-info {
    display: flex;
}

/* THE BAND'S COLOUR HAS TO REACH THE OCCLUDERS, and until 2026-09-10 it did not.
   .info-provenance, .info-note-right and (since 2026-09-08) .info-fatigue all carry
   `background: inherit` so they paint the band's own colour and cut a wide centre line
   off cleanly where they cross it. But `inherit` takes the PARENT ELEMENT's value, and
   their parent is this wrapper, which had no background at all — so all three computed
   `rgba(0, 0, 0, 0)` and occluded nothing. Measured, not reasoned: the note's computed
   background-color was transparent on a live strip, and a long Fatigue Index line ran
   under a note with the two sets of glyphs interleaved, which is what Robert reported.
   It broke silently the day this wrapper was introduced between the band and its children.

   background-COLOR, never the shorthand, and that is what makes one line cover all five
   bands. A solid band (the cyan, delete's red, Add and Notes' green, reorder's purple)
   passes its colour down. Colour mode sets `background: linear-gradient(...)`, whose
   shorthand leaves background-color at its initial TRANSPARENT — so this inherits
   transparent there, the children inherit transparent from it, and the striped band keeps
   the "imperfect occlusion is accepted" behaviour its own rule already asks for, with no
   gradient ever re-rendered inside a small box. The explicit colour-mode overrides above
   stay as the statement of that intent. */
.panel-info .info-detail {
    background-color: inherit;
}

/* (The Add band's controls lived here from 2026-07-24 until 2026-08-12:
   .band-controls / .band-align / .assign-cols, labelled columns on the solid
   green the mode turned the band, indented by a hidden replica of the wordmark.
   They moved to the add DOCK — see .add-dock-form below — and the whole band is
   hover details again. The three classes and the .panel-info.assign-mode paint
   went with them; the pieces those columns were BUILT from (.fcol, .cap,
   .start-unit, .dn-seg, the chips, the tag row) all survive, re-scoped to the
   dock's form.) */

/* One labelled control: caption above, control below (dense-panel labels stay
   at --font-size-small, like the old inline panel-labels). */
.add-dock-form .fcol {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}

.add-dock-form .cap {
    font-size: var(--font-size-small);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-text-muted);   /* the band's white-on-green ink is gone with the band */
}

/* The Start value with its presets. In the band these stacked (a column with no
   width to spare); the dock's 420px puts them side by side — see
   .add-dock-form .start-unit. */
.start-unit {
    display: flex;
    flex-direction: column;
    width: max-content;
}

.dn-seg {
    display: flex;
    height: 22px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
}

.dn-seg button {
    flex: 1;
    min-width: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    background: transparent;
    color: var(--color-text);
    font-family: var(--font-family);
    font-size: var(--font-size-small);
    cursor: pointer;
    padding: 0 2px;
}

.dn-seg button + button {
    border-left: 1px solid var(--color-border);
}

.dn-seg button:hover {
    background: var(--color-field);
}

/* The add dock's body: the band's columns as ROWS down a 420px panel (2026-08-12).
   It SCROLLS as of 2026-08-13 (Robert: the controls ran into one another when the
   panel was short — the search window docked underneath, a laptop screen). Two
   halves make that work, and both are load-bearing:
   - flex-shrink: 0 on every row. A column flex container shrinks its items by
     default, and the rows are built of fixed-height controls that can't shrink
     with them, so the box squashed while its content stayed put and rows drew on
     top of each other. Nothing squashes now; what doesn't fit scrolls.
   - The two pickers' lists FLOAT (Typeahead's IsDropdownFloating). This form
     could never scroll before because an absolutely-positioned list is clipped
     invisible by any overflow ancestor (the 2026-08-08 defect); a fixed-position
     list placed by typeahead-dropdown.js has no such ancestor. That module is
     the sanctioned escape from the never-scroll-a-typeahead rule — a new picker
     in here needs it too. */
.add-dock-form {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
    padding: 10px;
    overflow-y: auto;
}

/* (No scrollbar styling here, deliberately: it uses the platform's, like every
   other scrolling box in the app — the tag chips, the search results, the note
   editor. Both ways of forcing a standing bar on a short panel were tried and
   MEASURED on 2026-08-13, and neither reserved a single pixel: macOS Chromium
   keeps its overlay scrollbar through `scrollbar-color`, and through
   `::-webkit-scrollbar` too. What says there is more below is the row the scroll
   box cuts in half, plus the overlay bar the moment you scroll.) */

.add-dock-form > * {
    flex-shrink: 0;
}

/* Hours and Break: two small steppers that read as a pair, so they keep the
   band's side-by-side relationship instead of taking a row each. */
.add-dock-pair {
    display: flex;
    gap: 22px;
    /* BOTTOM-aligned so the three chips sit on one line whatever height the captions
       take. Measured 2026-09-10 with the fuller captions ("Total break (min)",
       "Number of breaks"): none of them wraps at the dock's own width, so this is
       defence rather than a fix — it is what keeps the chips level if a caption ever
       does wrap, on a narrower dock or a longer word. */
    align-items: end;
}

/* What a break's two numbers mean, under the chips that hold them, in the edit
   dialog and under each break tier (Robert's ask, 2026-09-10). One rule for all
   three, so the sentence reads the same wherever it is said. */
.break-meaning {
    margin: 6px 0 0;
    font-size: var(--font-size-small);
    color: var(--color-text-muted);
    line-height: 1.35;
}

/* In the Add dock the same sentence is right-aligned and hugs the chips above it
   (Robert, 2026-09-11), the edit dialog's placement of the day before applied to the
   panel that arms the numbers.
   RIGHT MEANS THE CHIPS, NOT THE FORM, and the 43px is ROBERT'S OWN, typed into Chrome's
   dev tools and given as the fix: "it should be right aligned so the end of the line is
   near the end of the fields above". The first cut aligned to the form's right edge
   instead, reasoning that the job picker, the tag picker and the note editor all end
   there so three verticals already stand on it. That was wrong about which edge the
   sentence belongs to: it is about the two numbers, so it ends under them, exactly as it
   does in the edit dialog. Do not "tidy" it back to the form's edge.
   WHY IT TAKES A MEASURED NUMBER AT ALL, so the next reader does not hunt for the
   expression that would replace it: the chip ROW is full width (the form is
   align-items: stretch), and what stops short is the last .fcol inside it, whose width
   comes from its caption. Nothing in CSS can name that leftover from out here, and the
   sentence is a SIBLING of the row, not a child. Measured on 2026-09-11: the last chip
   ends 53px clear of the form's content edge, so 43px lands the line about 10px past it,
   which is the "near" Robert asked for and the reading he signed off; 53px would be
   dead flush. IT IS TIED TO THE CAPTIONS — "Number of breaks" is the widest of the three
   and sets that .fcol — so re-measure in a browser if a caption's wording changes, the
   dock's width changes, or a fourth chip joins the row. The 290px name column is the
   house precedent for a number like this.
   The negative top margin is .entity-form .locked-setting-note's trick and its reason:
   the form's 12px gap plus this class's own 6px read as a new row, where the sentence is
   the chips' own explanation and should sit closer to them than unrelated rows sit to
   each other. 18px to 8px. Two classes because a bare one would tie with the shared rule
   above and lose on source order. */
.add-dock-form .break-meaning {
    margin: -4px 43px 0 0;
    text-align: right;
}

/* Both pickers take the dock's full width — what the band could never give
   them (the job picker ran to 266px and the tag picker to 150px). */
.add-dock-form .typeahead,
.add-dock-form .typeahead-input {
    width: 100%;
}

/* The stepper beside its presets, not above them — which means matching the
   chip's height (the segment ran 2px shorter when it hung underneath, where
   nothing sat next to it) and spending some of the recovered width on padding,
   so "Day" and "Night" aren't jammed against their divider. */
.add-dock-form .start-unit {
    flex-direction: row;
    align-items: center;
    gap: 6px;
    width: auto;
}

.add-dock-form .dn-seg {
    height: 24px;
}

.add-dock-form .dn-seg button {
    padding: 0 10px;
}

/* The chips and the white stepper wells were drawn to POP off the band's green.
   On the dock's own surface they need the hairline every field carries here
   (the .edit-tags precedent, which solved this for the edit dialog). */
.add-dock-form .chip,
.add-dock-form .tag-chip {
    border: 1px solid var(--color-border-strong);
}

/* The tag row takes the rest of the dock: chips above, picker below, both full
   width. The scroll stays on the chips box and nowhere else. */
.add-dock-form .fcol-tags {
    max-width: none;
    min-height: 0;
}

.add-dock-form .tag-armed {
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
}

.add-dock-form .tag-chips {
    max-height: 96px;
}

/* (The Notes band's editor column — .notes-cols/.fcol-note — lived here for
   part of 2026-08-10: the rich editor moved to the notes DOCK the day it was
   built. See .mode-dock/.notes-dock by the search-window dock rules.) */

/* Stepper chips: ◂ [typeable value] ▸, stepping 15 min / 0.5 h. */
.chip {
    display: inline-flex;
    align-items: center;
    height: 24px;
    background: var(--color-surface);
}

.chip-step {
    width: 20px;
    height: 24px;
    border: 0;
    background: none;
    color: var(--color-text-muted);
    font-size: 11px;
    cursor: pointer;
}

.chip-step:hover {
    color: var(--color-text);
    background: var(--color-field);
}

.chip-input {
    width: 48px;
    height: 24px;
    padding: 0 2px;
    border: 0;
    background: transparent;
    color: var(--color-text);
    font-family: var(--font-family);
    font-size: var(--font-size-base);
    font-variant-numeric: tabular-nums;
    text-align: center;
}

/* The armed tag set (2026-08-08): removable chips + a compact typeahead.
   A long set wraps and then scrolls rather than growing its container — in the
   band because its 80px was fixed, in the add dock so the form's height stays
   bounded and never needs a scroll of its own — but ONLY the chips box may
   scroll: the
   typeahead's open list is absolutely positioned, and any overflow on an
   ancestor between it and the page clips it invisible (the 2026-08-08 defect —
   the picker looked dead while the list rendered behind the clip). So the
   container is a plain row and the scroll lives on .tag-chips. */
.fcol-tags {
    max-width: 320px;
}

.tag-armed {
    display: flex;
    align-items: flex-start;
    gap: 4px;
}

.tag-chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    min-width: 0;
    max-height: 52px;
    overflow-y: auto;
}

.tag-chip {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    height: 24px;
    padding: 0 2px 0 6px;
    background: var(--color-surface);
    color: var(--color-text);
    font-size: var(--font-size-small);
    white-space: nowrap;
}

.tag-chip-remove {
    border: 0;
    background: none;
    color: var(--color-text-muted);
    font-size: 10px;
    cursor: pointer;
    padding: 0 4px;
    height: 100%;
}

.tag-chip-remove:hover {
    color: var(--color-text);
    background: var(--color-field);
}

/* The selected job's DEFAULT tags in the Add band (2026-08-08): dimmed and
   unremovable — they belong to the job, not the armed set, and every entry
   added on it carries them. The dashes-free tone difference (opacity + no ✕)
   is backed by the title text, never colour alone. */
.tag-chip-default {
    opacity: 0.62;
    padding: 0 6px;
    font-style: italic;
}

/* (The band's two picker widths — 266px for the job, 150px for the tag one —
   died with the band on 2026-08-12: both take the dock's full width now, set
   once in .add-dock-form.) */

/* The edit dialog's version of the same row: label left (the .modal-form label
   look), chips right. On the modal's light surface the chips need a hairline —
   the band's white-on-green pop does nothing here. */
.edit-tags {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
}

.edit-tags > span {
    padding-top: 5px;
}

.edit-tags .tag-armed {
    flex-wrap: wrap;   /* the input may drop below a full chip row here */
    max-width: 300px;
}

.edit-tags .tag-chips {
    max-height: none;   /* the modal can grow; only the band is height-boxed */
    overflow-y: visible;
}

.edit-tags .tag-chip {
    border: 1px solid var(--color-border);
}

.edit-tags .typeahead-input {
    width: 170px;
}

.chip-input:focus {
    outline: 1px solid var(--color-accent);
    outline-offset: -1px;
}

.chip-input-hours {
    width: 34px;
}

/* Colour mode's picker: one option per line, down the colour dock (2026-08-12 —
   it was a wrapping row centred on the action panel's 80px band until Robert
   moved it into the dock; a palette of a couple of dozen colours never fitted
   there, and a vertical list has room to grow). The order is the palette's own
   (the Colours tab's DisplayOrder), which is what every other colour gesture
   shows too. Scrolls inside the dock's body; the head above it stays put.
   Named .colour-dock- and not .colour-picker: that name belongs to the job
   page's and the Colours tab's swatch rows, and a flex-direction: column here
   leaked into both of them once (2026-08-07 — it stacked the modal's black/
   white text chips). Two different things must never share a class name
   because they are both pickers of colours. */
.colour-dock-options {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
    padding: 8px;
    overflow-y: auto;
}

/* One choice: its name written on its own colour, exactly as a coloured block
   shows it (the inline style carries the hex and the ink). Full width of the
   dock and left-aligned, so the names read as a list rather than a row of
   tags. The armed one gains a heavier ring AND a ✓ — never the ring alone. */
.colour-pick {
    display: flex;
    align-items: center;
    gap: 4px;
    flex: none;
    height: 26px;
    padding: 0 10px;
    border: 1px solid var(--color-border-strong);
    font-family: var(--font-family);
    font-size: var(--font-size-base);
    text-align: left;
    cursor: pointer;
}

/* "None" has no colour of its own, so it wears the band's — with the struck-
   through chip beside it saying "no colour", the same chip the right-click
   menu's None uses. */
.colour-pick-none {
    background: var(--color-surface);
    color: var(--color-text);
}

.colour-pick:hover {
    border-color: var(--color-text);
}

.colour-pick.selected {
    border: 2px solid var(--color-text);
    padding: 0 9px;   /* the extra border pixel comes back out of the padding */
}

.colour-pick-check {
    font-weight: 700;
}

/* ------------------------------------------------------------
   Roster search (2026-08-09, docs/specs/tag-search-and-block-display.md §3;
   unified over tags, jobs and note text the same day). The action-panel BAND
   that used to live here — .search-band / -controls / -foot / -results, the
   Pop out button, and the sideways column-wrap its results scrolled in — is
   gone (Robert, 2026-08-10): search left the panel entirely, and the window
   below is the whole surface. What remains here is shared by the window's
   two result forms (the docked one-liners and the undocked table). */
.search-count {
    font-size: var(--font-size-small);
    color: var(--color-text-muted);
}

.search-scope {
    color: var(--color-text-muted);
    margin: auto 0;
    flex-shrink: 0;
}

/* A result line is a reset button wearing the row-link look. flex-shrink: 0 is
   LOAD-BEARING (bug, 2026-08-09): flex children default to shrink-to-fit, so a
   long match list was squashed into its scroll box — 28 rows at ~2px each,
   humanly invisible. Both of the window's result forms are flex columns, so
   the guard still earns its place. */
.search-result {
    flex-shrink: 0;
    background: transparent;
    border: none;
    font-family: var(--font-family);
    font-size: var(--font-size-base);
    color: var(--color-text);
    text-align: left;
    padding: 1px 6px;
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.search-result:hover {
    background: var(--color-field);
}

/* The selected result (2026-08-09): the grid dims every entry but this one, and
   the line says so — ✓ + the accent fill, never colour alone. The fill holds
   under the cursor, like every active control. */
.search-result.selected,
.search-result.selected:hover {
    background: var(--color-accent);
    color: #ffffff;
}

.search-result-check {
    font-weight: 700;
}

/* A SCORE SEARCH's leading value on a DOCKED one-line row (Robert, 2026-09-10): the
   score first, then a hairline, then the who/when/what the line has always
   carried. The rule is the separation — the line's own dividers are " · ", so a third
   one would read as another field rather than as "this is the answer, and here is the
   entry it belongs to". Right-aligned in a fixed min-width so a column of figures lines
   up down the list even though the rows are not a grid; tabular-nums for the same reason.
   The border follows currentColor, so it stays visible when the selected row inverts to
   white on the accent fill rather than needing a second rule that could be forgotten.
   Named for the SCORE, not for an index (2026-09-12, when the Risk Index joined): the
   same cell carries either, and the field above it is what names which. Its own class,
   never the table cell's (.search-window-score): two different things never
   share a class name here. */
.search-result .search-result-score {
    display: inline-block;
    min-width: 32px;
    margin-right: 8px;
    padding-right: 8px;
    border-right: 1px solid color-mix(in srgb, currentColor 45%, transparent);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    text-align: right;
}

/* The search results WINDOW (2026-08-10 — was the Expand modal): a floating
   NON-modal tool window. No backdrop, so the grid and every control stay
   live around it; z-index 99 sits above the panel (95) and below the drag
   ghosts (108) and toasts. Deliberately NARROW — fit-content columns (below)
   let the table take only what its content needs, so the window covers as
   little roster as possible. Default position: centred under the panel; the
   drag module writes inline left/top over these (and clears the transform),
   which survive re-renders because the razor declares no inline style. */
.search-window {
    position: fixed;
    top: 88px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 99;
    width: min(680px, 94vw);
    max-height: 78vh;
    display: flex;
    flex-direction: column;
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
    padding: 8px 12px 12px;
}

/* Docked (2026-08-10, Robert's spec): the window joins the LAYOUT instead of
   floating over it. When a docked search window exists, the app shell's flex
   column becomes a grid — panel across the top, roster beside the dock,
   status bar across the bottom — so the dock's top is exactly the panel's
   bottom, its bottom exactly the status bar's top, and the roster area is
   GENUINELY narrower (nothing hides under the strip; the grid's columns
   already floor at 100px and scroll). No pixel constants to go stale.
   :has() is the one modern-CSS dependency; every browser this app targets
   ships it. --search-dock-width lives on :root, not .app-shell, because the
   drag preview strip is appended to <body> (never inside the Blazor-managed
   shell) and must read it too. */
:root {
    --search-dock-width: 420px;
}

/* Two dockables since the notes dock (2026-08-10): the search window, and the
   MODE dock — .mode-dock, the slot a write mode's furniture lives in while
   that mode is on (the notes editor today; the Add panel MAY follow, Robert
   2026-08-10, which is why the layout reads the generic class — modes are
   mutually exclusive, so at most one mode panel ever exists). The middle of
   the grid is TWO equal rows so the pair can stack: a lone dock panel spans
   both; both present, the MODE dock keeps the TOP slot and search sits
   UNDERNEATH (Robert's follow-up the same day, reversing his first order:
   search always appears below). */
.app-shell:has(> .search-window.docked),
.app-shell:has(> .mode-dock) {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr) minmax(0, 1fr) auto;
    grid-template-columns: minmax(0, 1fr) var(--search-dock-width);
}

.app-shell:has(> .search-window.docked) > .action-panel,
.app-shell:has(> .mode-dock) > .action-panel {
    grid-area: 1 / 1 / 2 / 3;
}

.app-shell:has(> .search-window.docked) > .roster-area,
.app-shell:has(> .mode-dock) > .roster-area {
    grid-area: 2 / 1 / 4 / 2;
}

.app-shell:has(> .search-window.docked) > .status-bar,
.app-shell:has(> .mode-dock) > .status-bar {
    grid-area: 4 / 1 / 5 / 3;
}

.search-window.docked {
    grid-area: 2 / 2 / 4 / 3;   /* alone: the whole column — see the stacked override below */
    /* z-index auto (2026-08-16): docked, this is a LAYOUT member with a column
       of its own, not a window floating over anything, so it has no business
       lifting itself above the rest of the app. (A grid item's z-index applies
       even at position: static, which is why the floating rule's 99 was still
       in force here.) At 99 it painted over every popup that overflows a
       neighbouring panel — the user menu, and the add dock's typeahead lists,
       which open downward into the search dock's half of the column. */
    z-index: auto;
    position: static;
    width: auto;
    max-height: none;
    min-height: 0;   /* the grid-blowout guard: results scroll inside, never grow the row */
    transform: none;
    border: 0;
    border-left: 1px solid var(--color-border-strong);
}

/* The mode dock's slot half: layout only — a mode panel's own chrome lives on
   its specific class (.notes-dock below). Flex column so the panel's header
   sits fixed and its body takes the rest; min-height 0 is the same grid-blowout
   guard the search window carries. */
.mode-dock {
    grid-area: 2 / 2 / 4 / 3;   /* alone: the whole column */
    min-height: 0;
    display: flex;
    flex-direction: column;
    background: var(--color-surface);
    border-left: 1px solid var(--color-border-strong);
}

/* Stacked: search docked AND a mode dock up — the mode dock on top, search
   below (Robert: search always appears below). These out-rank the lone
   placements above ((0,4,0) against (0,2,0)/(0,1,0)); the hairline between
   the two is the lower panel's border-top. */
.app-shell:has(> .mode-dock) > .search-window.docked {
    grid-area: 3 / 2 / 4 / 3;
    border-top: 1px solid var(--color-border-strong);
}

.app-shell:has(> .search-window.docked) > .mode-dock {
    grid-area: 2 / 2 / 3 / 3;
}

/* A mode dock's header strip, shared by all three (add, notes, colour — hence
   the generic name; naming it after one of them and dressing the others in it
   is exactly the class-collision trap): fixed above the body, title left and
   the ✕ right, and that ✕ is always the mode button's own SetMode toggle so
   there is one exit path per mode (the reorder-band rule).
   ONE look for every dock (Robert, 2026-08-13): the plain surface with a
   hairline and a muted cap-label title. Add and Notes wore the engaged green
   here until then, on the reasoning that the header carried the "a write mode
   is live" signal the info band had given up — but the band paints again since
   earlier the same day, so the header was saying it twice and saying it
   differently per mode. The signal lives on the band and the mode button; the
   dock is furniture, and furniture should not change colour by mode. This is
   Colour's old look promoted to the shared one — its "a tint beside a swatch
   changes how that swatch reads" reasoning still holds and now covers every
   dock for free, including whichever one is built next. Search is deliberately
   NOT part of this: its window is a tool, not mode furniture, and keeps its
   own header. */
/* The whole strip is the close gesture, not just the ✕ (Robert, 2026-08-13):
   the bar is a wide, easy target and the ✕ is a 20px one, so the pointer says
   the whole thing is clickable and a click anywhere on it runs the same
   SetMode toggle. The ✕ stays — it is what a user looks for — and stops the
   click propagating, or the toggle would fire twice and reopen the dock. */
.mode-dock-head {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 5px 10px;
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border-strong);
    cursor: pointer;
}

/* Hovering anywhere on the strip darkens both its parts, so the feedback
   matches the target: the ✕ alone darkening would understate what is live. */
.mode-dock-head:hover .mode-dock-title,
.mode-dock-head:hover .mode-dock-close {
    color: var(--color-text);
}

.mode-dock-title {
    font-size: var(--font-size-small);   /* the band's cap-label look */
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
}

.mode-dock-close {
    height: 20px;
    width: 20px;
    background: none;
    border: 0;
    font-size: var(--font-size-base);
    cursor: pointer;
    color: var(--color-text-muted);
}

.mode-dock-close:hover {
    color: var(--color-text);
}

/* The drag-to-dock landing preview (JS-created on <body> while a drag hovers
   the right edge): dashed accent over exactly the area the dock will take —
   top and height are set inline from the live .roster-area. */
.search-dock-preview {
    position: fixed;
    right: 0;
    width: var(--search-dock-width);
    z-index: 98;
    border: 2px dashed var(--color-accent);
    background: color-mix(in srgb, var(--color-accent) 10%, transparent);
    box-sizing: border-box;
    pointer-events: none;
}

/* The header strip is the drag handle — title row plus the scope line, both
   grabbable; the ✕ inside stays a click (the JS ignores buttons). */
.search-window-head {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin-bottom: 8px;
    cursor: grab;
    user-select: none;
    touch-action: none;
}

.search-window-head:active {
    cursor: grabbing;
}

.search-window-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.search-window-title-buttons {
    display: flex;
    align-items: center;
    gap: 6px;
}

/* The window's name, deliberately bigger than the chrome around it (Robert,
   2026-08-10): it is the one line that says what this floating thing is. */
.search-window-name {
    font-size: 15px;
}

/* The header buttons — "Clear search and close" and Dock/Undock: a small
   bordered ghost, discoverable where the gestures (drag to the right edge, the
   panel's own clear) are not. */
.search-window-head-btn {
    background: transparent;
    border: 1px solid var(--color-border-strong);
    color: var(--color-text);
    font-family: var(--font-family);
    font-size: var(--font-size-small);
    padding: 1px 8px;
    cursor: pointer;
}

.search-window-head-btn:hover {
    background: var(--color-field);
}

.search-window-close {
    height: 20px;
    width: 20px;
    background: none;
    border: 0;
    color: var(--color-text-muted);
    font-size: var(--font-size-base);
    cursor: pointer;
}

.search-window-close:hover {
    color: var(--color-text);
}

/* The scope line ("Searches what's showing…"): bigger than chrome and italic
   (Robert, 2026-08-10) — it is a standing caveat on every result below it, so
   it reads as an aside rather than as a label. */
.search-window-scope {
    color: var(--color-text-muted);
    font-size: 13px;
    font-style: italic;
}

.search-window-controls {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;
    flex-wrap: wrap;
}

.search-window-controls .typeahead-input {
    width: 190px;
}

/* The scroll box. Vertical is the point (the window caps at 78vh and long
   lists scroll); horizontal only exists on screens too narrow for the
   columns' minimums, where the grid overflows sideways rather than crushing. */
.search-window-results {
    flex: 1;
    min-height: 60px;
    overflow: auto;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 1px;
    border-top: 1px solid var(--color-border);
}

/* Header and rows share ONE template — that identity is what aligns the
   columns across rows (no subgrid needed). The tracks must stay LENGTH-ONLY
   (no auto/min-content/fit-content): header and rows are SEPARATE grid
   containers, and only content-independent tracks resolve identically in
   every one of them — an intrinsic track would size per row and the columns
   would go jagged. Tracks: ✓ | employee | date | job (soaks the slack) |
   times (wide enough for "06:00-14:00 (+1 day)" — clipped times were a real
   complaint) | colour | note. */
.search-window-head-row,
.search-window-result {
    display: grid;
    grid-template-columns: 16px 130px 84px minmax(120px, 1fr) 122px 110px 22px;
    column-gap: 8px;
    align-items: center;
}

/* A SCORE SEARCH gains a leading Score track (Robert, 2026-09-10). Written as a whole
   replacement template rather than as a first-track tweak, because the tracks are
   length-only BY RULE (header and rows are separate grid containers) and a partial
   override would be a second place the six other widths are written. 56px holds the
   widest figure either scale reaches beside a 10px "SCORE" heading — a Fatigue Index's
   "100.0" and a Risk Index's "2.13" are both well inside it; the job column keeps
   soaking the slack. */
.with-score .search-window-head-row,
.with-score .search-window-result {
    grid-template-columns: 16px 56px 130px 84px minmax(120px, 1fr) 122px 110px 22px;
}

/* The extra 64px (track plus gap) comes out of the WINDOW, not out of the job column:
   at 680px the seven tracks already fill the content box, so taking it from the columns
   would put the table into a permanent horizontal scroll for every score search. Scoped
   :not(.docked) because a docked panel's width is the layout's — and note this rule
   would otherwise OUTRANK .search-window.docked, whose width: auto is what makes the
   dock a column rather than a fixed panel. */
.search-window:not(.docked):has(.search-window-results.with-score) {
    width: min(744px, 94vw);
}

/* The house table-header chrome (the .admin-table th look, borderless), stuck
   to the top of the scroll box so long lists keep their labels. Sticky is
   vertical-only, so it rides along under any sideways scroll. */
.search-window-head-row {
    position: sticky;
    top: 0;
    z-index: 1;
    background: var(--color-surface);
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding: 6px 6px 4px;
    border-bottom: 1px solid var(--color-border);
    margin-bottom: 2px;
}

.search-window-result {
    flex-shrink: 0;   /* the band's squashed-rows bug, prevented here too */
    padding: 3px 6px;
}

/* Every cell clips independently — a long job or colour name ellipsizes in its
   own column instead of pushing the row out of alignment. */
.search-window-result > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.search-window-who {
    font-weight: 600;
}

/* The Score cell of an undocked score search: right-aligned tabular figures under a
   right-aligned heading, so the decimal points line up down the column. Bold like the
   docked row's, because it is the value the search was for. Its own class rather than
   the docked line's, per the never-share-a-class-name rule. */
.with-score .search-window-head-row > span:nth-child(2),
.search-window-result .search-window-score {
    text-align: right;
}

.search-window-result .search-window-score {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

/* The noted-block folded corner, quoted at result-line size (the same two
   triangles the block draws, in the muted ink; the title carries the words). */
.search-note-marker {
    position: relative;
    width: 8px;
    height: 8px;
    display: inline-block;
}

.search-note-marker::before {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    border-top: 8px solid var(--color-text-muted);
    border-left: 8px solid transparent;
    opacity: 0.35;
}

.search-note-marker::after {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    border-bottom: 7px solid var(--color-text-muted);
    border-right: 7px solid transparent;
}

.search-window-colour {
    display: flex;
    align-items: center;
    color: var(--color-text-muted);
}

/* The name ellipsizes on its own so the chip beside it never shrinks away. */
.search-window-colour-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* On the selected row's accent fill, the muted greys fall below contrast —
   the assign-band precedent: own the ink, don't inherit the theme's. */
.search-result.selected .search-window-colour {
    color: rgba(255, 255, 255, 0.88);
}

.search-result.selected .search-note-marker::before {
    border-top-color: #ffffff;
}

.search-result.selected .search-note-marker::after {
    border-bottom-color: #ffffff;
}

.btn.delete-active {
    background: var(--color-breach);
    border-color: var(--color-breach);
    color: #ffffff;
}

/* Colour (and Search, 2026-08-09) when active: the plain surface with a
   hairline, matching the band the mode turns plain — the same rule that pairs
   delete-active with the red band and engaged-active with the green one. Two
   classes sharing one declaration by grouping, never one class worn by two
   different things. */
.btn.colour-active,
.btn.search-active {
    background: var(--color-surface);
    border-color: var(--color-border-strong);
    color: var(--color-text);
}

/* Assign/Notes when active: the engaged green, matching the info band the mode
   paints — not the generic accent. Since 2026-08-13 the band and this button are
   the whole of the green: the dock header went plain with every other dock's,
   and the status bar stopped painting when the controls docked. */
.btn.engaged-active {
    background: var(--color-assign-bg);
    border-color: var(--color-assign-bg);
    color: #ffffff;
}

/* ------------------------------------------------------------
   The logo (2026-10-06): the Q.E.D. symbol (LogoSymbol) beside the
   name, which stays live text read from ProductInfo.Wordmark, so a
   rename is still one edit. docs/logo/README.md is the place of
   truth: the files, the colours, and the size each place uses.

   The name's font is a cut-down copy of Archivo (SIL Open Font
   License) that the app serves itself, made by docs/logo/make.sh:
   only the one width and weight the logo uses, and only the Latin
   characters, so it is about 12 KB. Its licence sits beside it in
   wwwroot/fonts. "swap" shows the system font for the moment the
   file takes to arrive, and App.razor preloads it, so that moment
   is short.
   ------------------------------------------------------------ */
@font-face {
    font-family: "Archivo Logo";
    src: url("fonts/archivo-semiexpanded-semibold.woff2") format("woff2");
    font-weight: 600;
    font-stretch: 112.5%;
    font-style: normal;
    font-display: swap;
}

.logo-symbol {
    flex: none;
    display: block;
}

.logo-symbol .logo-ink {
    fill: var(--logo-ink);
}

.logo-symbol .logo-accent {
    fill: var(--logo-accent);
}

/* The name as the design sets it: SemiBold, semi-expanded, spaced capitals.
   Each place that shows it sets only its size and its colour. */
.logo-name {
    font-family: var(--font-logo);
    font-weight: 600;
    font-stretch: 112.5%;
    letter-spacing: 0.14em;
    line-height: 1;
    white-space: nowrap;
}

/* The app's top-left corner: a 16px symbol and the name at 12px, in the quiet
   grey the name has always had there (the design keeps it: only the symbol
   carries colour), darkening on hover. */
.action-panel .app-title {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: var(--color-text-muted);
    margin-right: 8px;
    text-decoration: none;
}

.action-panel .app-title .logo-symbol {
    width: 16px;
    height: 16px;
}

.action-panel .app-title .logo-name {
    font-size: 12px;
}

.action-panel .app-title:hover {
    color: var(--color-text);
}

.panel-spacer {
    flex: 1;
}

.panel-label {
    display: flex;
    align-items: center;
    gap: 4px;
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
}

.panel-info .info-job {
    font-weight: 600;
    font-size: 15px;
}

.panel-info .info-emp {
    font-weight: 500;
}

.panel-info .info-swatch {
    flex: none;
    width: 12px;
    height: 12px;
    border: 1px solid rgba(0, 0, 0, 0.45);
}

.panel-info .info-sep {
    flex: none;
    width: 1px;
    height: 14px;
    background: rgba(22, 50, 59, 0.5);
}

.panel-info .muted {
    color: #274550;   /* 5:1 on the cyan band — the theme muted grey is ~2.5:1 */
}

/* Provenance rows (round 8, treatment A — adopted 2026-08-10): a fixed-width
   small-caps label column so the values align, roman values in the band's own
   ink. The label's muted teal is overridden white by the mode bands above,
   like .muted. */
.panel-info .prov-row {
    display: flex;
    align-items: baseline;
    gap: 8px;
    font-size: 12px;
}

.panel-info .prov-label {
    flex: none;
    width: 52px;
    font-size: 9.5px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #274550;
}

.panel-info .notice {
    color: #7d150d;   /* deepened breach red — the token red drops to ~2.8:1 on cyan */
}

.panel-info .dismissable {
    cursor: default;
    user-select: none;
}

.panel-info .info-potential {
    border: 1px dashed #5f4102;   /* deepened warning amber — the token amber is ~2:1 on cyan */
    color: #5f4102;
    padding: 0 4px;
    font-size: var(--font-size-small);
}

/* That amber was mixed for the cyan band and measures ~1.9:1 on the green and
   ~1.8:1 on the red — unreadable on both. On a repainted band it takes the
   band's own white ink, like .muted and .notice above. The badge's DASHED
   border is what tells it from externally-managed's solid one, so the
   distinction survives losing the hue (never colour alone). Delete's band has
   had this defect since it was written; it is fixed here alongside the green's
   because it is the same rule, and reorder needs no arm — that band shows no
   hover details at all. */
.panel-info.assign-mode .info-potential,
.panel-info.delete-mode .info-potential {
    border-color: #ffffff;
    color: #ffffff;
}

/* A faint block's entry on the Pivot View (2026-09-03): the strip's two centred
   lines go italic, like the faint block's own label, and the badge is DOTTED,
   the third border style beside POTENTIAL's dashed and externally-managed's
   solid. currentColor, so it follows the band's ink like the EM badge. */
.panel-info .info-from-elsewhere {
    font-style: italic;
}

.panel-info .info-elsewhere {
    border: 1px dotted currentColor;
    padding: 0 4px;
    font-size: var(--font-size-small);
    font-style: normal;
}

/* A coloured cell with a note under the pointer (2026-09-06): the badge that says the
   strip is describing a CELL COLOUR rather than an entry. Solid and currentColor,
   like the externally-managed badge, so it follows the band's ink. */
.panel-info .info-cell-colour {
    border: 1px solid currentColor;
    padding: 0 4px;
    font-size: var(--font-size-small);
}

/* Externally-managed: solid border to POTENTIAL's dashed, echoing the block edges.
   currentColor, so it reads as the strip's teal-dark ink on cyan and turns white
   with the text on the assign-green and delete-red strips. */
.panel-info .info-external {
    border: 1px solid currentColor;
    padding: 0 4px;
    font-size: var(--font-size-small);
}

/* The hovered entry's tags (2026-08-08): the same chip object planners
   arm in the Add band — reference on the chip, full name on its title — minus
   the remove button, so slightly shorter and evenly padded. Its own modifier:
   a bare .panel-info .tag-chip would restyle the armed chips too. */
.panel-info .info-tag-chip {
    height: 20px;
    padding: 0 6px;
}

.panel-info .info-line-small {
    font-size: 12px;
}

/* The hovered entry's Fatigue Index (2026-09-08): a third centred line under the two
   the strip has always had, in the workbook's own precision so a safety lead can
   reproduce the row in the official spreadsheet. Tabular figures so the components line
   up as the eye moves along them; .muted for the band's readable teal, because it is
   reference beside two lines that say what the entry IS. */
.panel-info .info-fatigue {
    font-variant-numeric: tabular-nums;
    /* A SOLID PLATE IN THE BAND'S OWN COLOUR (Robert, 2026-09-10): "the background will
       not be apparent for most of the time, but if it overlaps the notes text, it should
       be visually separated". `background: inherit` is the strip's one way of saying that
       — the same words the provenance block and the note use — and it works for all five
       bands now that .info-detail passes the colour down (see its rule above).

       WHICH SIDE WINS WHERE THEY CROSS, since both are opaque and only one can: THIS LINE
       (Robert, 2026-09-10, reversing the ruling he gave earlier the same day). His words:
       "the fatigue index details should be a single row, current height and be on top of
       the notes." The line was cut at the note's own 12px left padding before this, which
       is what he saw and reported. It stays exactly one row at the height it already had,
       so the note loses one of its four lines' width where they cross and nothing about
       the 80px band moves.

       The mechanism is position + z-index rather than DOM order, because the two occluders
       either side of this line must not both change hands: the note is z-index auto and
       loses, while .info-provenance at the LEFT edge is raised to 2 below and still wins.
       That asymmetry is deliberate. This line is centred, so it grows into both flanks at
       once — but not at the same width, because the two flanks are anchored differently: the
       note sits a fixed 260px right of centre, where the provenance block is a fixed ~270px
       from the LEFT EDGE. So this line crosses the note once it passes 520px wide whatever
       the window, and crosses provenance only once it passes the window's width minus about
       540 — measured at 638px for a seeded entry, which reaches provenance on a window
       between roughly 760px (below which provenance is hidden anyway) and 1180px. That is
       an ordinary window, so the raise below is not theoretical. Provenance wins there
       because CREATED and UPDATED are two short structural rows with nowhere else to be
       read, where the note's tail is a planner's own words the edit dialog shows in full.

       The padding is what stops the last character sitting flush against whatever cuts it. */
    background: inherit;
    padding: 0 4px;
    position: relative;
    z-index: 1;
    /* The band is a FIXED 80px, so this line may never wrap: the fully-rested and
       own-profile clauses can take it past 200 characters, which on a narrow window
       would push the stack out of the band and over the grid's sticky date header.
       One line, clipped with an ellipsis; the full sentence is the report's. */
    max-width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The heat map's legend, in the band's default (nothing hovered) state — a hovered
   entry's own score is the more specific answer to the same question, so the two never
   compete for the room. Swatch AND figure on every band: the scale is never read by
   colour alone. */
.panel-info .info-heat-legend {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 2px 10px;
    /* ONE LINE, like .info-fatigue above and for the same reason: the band is a fixed
       80px with no overflow of its own, so eight swatch-and-label pairs wrapping to three
       lines on a narrow window would push the stack out of the band and over the grid's
       sticky date header. The warm end clips first, which is the end a reader already has
       on the cells in front of them. */
    flex-wrap: nowrap;
    max-width: 100%;
    overflow: hidden;
}

.panel-info .info-heat-band {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}

.panel-info .info-heat-swatch {
    flex: none;
    width: 14px;
    height: 10px;
    border: 1px solid rgba(22, 50, 59, 0.45);
    /* A swatch wears exactly ONE band class, so exactly one of the two ramp variables is
       set on it; the fallback is what lets one rule serve both maps. It cannot read
       --heat-band like the grid's cells do, because that is assigned off the grid's own
       data-heat and this swatch lives up in the action panel. */
    background: var(--heat-fi-band, var(--heat-ri-band));
}

/* The hovered note's home since Robert's 2026-08-10 follow-ups: RIGHT of the
   centred details block. Absolutely positioned so the centred block's
   position and proportions never move; left-aligned near the block and
   running to the panel's right end. The left offset is a fixed distance past
   centre — the block is centred, so its right edge moves with content; 260px
   clears the widest centred lines (a long employee/times line, a tag-heavy
   job line) now that provenance lives at the LEFT edge instead of in the
   stack, and background: inherit occludes cleanly (the provenance trick) if
   a wider block ever runs under it. Clamps to what the 80px band holds —
   four lines, then an ellipsis: the strip is a glance; the dialog is where a
   long note is read — though since 2026-08-10 a wheel turn over the hovered
   entry scrolls a longer note in place (note-wheel.js, the state below). */
.panel-info .info-note-right {
    top: 50%;
    transform: translateY(-50%);
    left: calc(50% + 260px);
    right: 12px;
    font-size: 12px;
    background: inherit;
    padding: 0 0 0 12px;
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 4;
    line-clamp: 4;
}

/* The wheel-scrolled state (note-wheel.js, 2026-08-10): the clamp can't
   scroll — it truncates rendering whatever scrollTop says — so the module
   swaps to a plain block frozen at the clamped box's exact pixel height (an
   inline max-height measured at the swap; the 4-line height comes from real
   line boxes, which no CSS token can restate). The base rule's
   overflow: hidden is what makes it a script-scrollable box with no
   scrollbar — the pointer can never reach the strip while a note shows, so a
   scrollbar could never be dragged anyway. Scrolling back to the top
   restores the resting clamp and its ellipsis cue. */
.panel-info .info-note-right.note-scrolling {
    display: block;
    -webkit-line-clamp: none;
    line-clamp: none;
}

/* The note yields before it could crowd the centred block on a narrow window.
   The .note-scrolling arm must be named here: its display: block is more
   specific than this hide, so without it a mid-scroll note would survive the
   window narrowing past the breakpoint. */
@media (max-width: 1000px) {
    .panel-info .info-note-right,
    .panel-info .info-note-right.note-scrolling {
        display: none;
    }
}

/* The provenance block at the LEFT edge — labelled rows since the round-8
   gallery (treatment A's provenance, adopted 2026-08-10; the note was left
   exactly as it is, per Robert). Absolutely positioned so it never affects
   the centred lines; background: inherit means a wide centre slides cleanly
   UNDER it (delete-mode red included); vertically centred so it shares the
   details block's rhythm; below 760px it yields entirely, as it always did. */
.panel-info .info-provenance {
    top: 50%;
    transform: translateY(-50%);
    background: inherit;
    padding: 0 12px 0 10px;
    /* Above the Fatigue Index line, which took z-index 1 on 2026-09-10 so it would paint
       over the NOTE. Without this it would take these two rows with it: everything here is
       positioned, so the whole group stacks together and a raise aimed at one flank crosses
       the other. See the .info-fatigue rule for why the two flanks are answered differently. */
    z-index: 2;
    display: flex;
    flex-direction: column;
    gap: 3px;
}

@media (max-width: 760px) {
    .panel-info .info-provenance {
        display: none;
    }
}

/* ------------------------------------------------------------
   Hand-rolled controls (button, select, typeahead) —
   token-driven, square, flat.
   ------------------------------------------------------------ */
/* Ghost buttons (design round 2, variant G): commands are plain text until
   hovered; the active mode fills solid. Fields are outlined and white — see
   .select below. */
.btn,
.select {
    height: 24px;
    padding: 0 8px;
    color: var(--color-text);
    border: 1px solid transparent;
    border-radius: 0;
    font-family: var(--font-family);
    font-size: var(--font-size-base);
}

.btn {
    background: transparent;
    cursor: pointer;   /* every clickable button gets the hand */
}

/* THE HOUSE FIELD AFFORDANCE (Robert's rule, 2026-08-04): an editable field is
   an OUTLINED white box; a grey --color-field fill with no outline means LOCKED
   (.select:disabled below). Every field in the app carries this one class, so
   this pair of rules is the single place the look is set — the classes beside
   it (.typeahead-input, .rule-param-input, …) only set widths.
   This replaced the original quiet-grey-well inputs, which read as editable and
   locked at the same time. */
.select {
    background: var(--color-surface);
    border-color: var(--color-border-strong);
}

/* Active buttons keep their solid fill under the cursor. The reorder band's exit
   button is excluded for the assign sub-panel's reason: --color-field is a light
   grey, unreadable under the white text a coloured band gives its buttons, so it
   brings its own hover (see .reorder-exit). An override can't simply be written
   with more weight — every :not() argument here counts, so this chain is (0,9,0)
   and a plainly-nested rule would lose; naming the exception is the way in. */
.btn:hover:not(:disabled):not(.active):not(.delete-active):not(.engaged-active):not(.colour-active):not(.search-active):not(.unassigned-active):not(.reorder-exit):not(.btn-remove-highlight) {
    background: var(--color-field);
}

/* The unassigned-rows toggle shares the view buttons' engaged accent fill —
   its own class grouped here (the colour-active pattern: two classes, one
   declaration, never one class worn by two different things). */
.btn.active,
.btn.unassigned-active {
    background: var(--color-accent);
    border-color: var(--color-accent);
    color: #ffffff;
}

.btn:disabled {
    color: var(--color-text-muted);
    background: transparent;
    cursor: not-allowed;
}

/* The locked half of the pair. Disabled FIELDS stay readable (feedback
   2026-07-28): the value keeps full text contrast on the grey well — the FILL
   reads as "static", an OUTLINE as "editable" (Robert's flip of the first cut,
   which had them the other way around). opacity + -webkit-text-fill-color beat
   the browsers' own disabled dimming, which ignores plain `color`. */
.select:disabled {
    color: var(--color-text);
    -webkit-text-fill-color: var(--color-text);
    opacity: 1;
    background: var(--color-field);
    border-color: transparent;
    cursor: not-allowed;
}

/* Native select chevrons don't grey with the rest of the control — swap in a
   muted arrow of our own when disabled. (Longhand background-* AFTER the
   shorthand above, which would otherwise reset the image; #999 reads as grey
   on both themes' wells.) */
select.select:disabled {
    appearance: none;
    -webkit-appearance: none;
    padding-right: 26px;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6'><path d='M1 1l4 4 4-4' fill='none' stroke='%23999999' stroke-width='1.5'/></svg>");
    background-repeat: no-repeat;
    background-position: right 8px center;
}

/* Dropdowns you can open get the hand, like every clickable control. */
select.select:not(:disabled) {
    cursor: pointer;
}

/* Long option text must never run under the chevron (feedback 2026-07-29):
   only appearance:none makes padding-right reliable on a select, so enabled
   selects draw their own chevron too (the disabled grey one already did) and
   overflowing text ellipsises instead. These are longhand background-* rules
   and must stay below any background SHORTHAND on .select, which would
   otherwise wipe the chevron image. */
select.select {
    appearance: none;
    -webkit-appearance: none;
    padding-right: 26px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.app-shell select.select:not(:disabled) {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6'><path d='M1 1l4 4 4-4' fill='none' stroke='%23444444' stroke-width='1.5'/></svg>");
    background-repeat: no-repeat;
    background-position: right 8px center;
}

.app-shell.theme-dark select.select:not(:disabled) {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6'><path d='M1 1l4 4 4-4' fill='none' stroke='%23cccccc' stroke-width='1.5'/></svg>");
}

/* Buttons have no border, so keyboard focus must be unmistakable. */
.btn:focus-visible {
    outline: 1px solid var(--color-accent);
    outline-offset: -1px;
}

/* Focus turns a field's outline accent blue and thicker, so the field you are
   typing in is unmistakable. 2px, offset inward by its own width so the ring
   sits wholly inside the box (the 1px/-1px button rule is the same trick one
   pixel thinner). */
.select:focus {
    outline: 2px solid var(--color-accent);
    outline-offset: -2px;
}

/* Icon-only Pro Tools-style toggles (worked hours / gaps) left of the date
   range. Square ghost by default (muted icon), engaged via .btn.active's solid
   accent fill + white icon (SVGs use currentColor). */
.panel-icon-btn {
    width: 24px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--color-text-muted);
}

.panel-icon-btn svg {
    display: block;
}

.panel-icon-btn:hover:not(.active):not(.unassigned-active) {
    color: var(--color-text);
}

/* Thin dividers keep adjacent ghost buttons from merging into a phrase. */
.panel-sep {
    flex: none;
    width: 1px;
    height: 16px;
    background: var(--color-border-strong);
}

.typeahead {
    position: relative;
}

.typeahead-input {
    width: 200px;
    padding-right: 20px;   /* room for the ✕ */
}

.typeahead-clear {
    position: absolute;
    right: 2px;
    top: 50%;
    transform: translateY(-50%);
    padding: 0 4px;
    background: none;
    border: 0;
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
    cursor: pointer;
}

.typeahead-clear:hover {
    color: var(--color-text);
}

.typeahead-list {
    position: absolute;
    top: 100%;
    left: 0;
    /* Width is set inline by the component's DropdownWidth: Content grows to fit the
       widest option (min-width:100%), MatchInput re-adds right:0, Fixed sets width. */
    z-index: 20;
    margin: 0;
    padding: 0;
    list-style: none;
    background: var(--color-surface);
    /* Assert the surface's own text colour: the list can open inside the
       solid green assign sub-panel, whose white text would otherwise be
       inherited — white on white. */
    color: var(--color-text);
    border: 1px solid var(--color-border-strong);
    max-height: 240px;
    overflow: auto;
}

/* The floating variant (2026-08-13): fixed, so no scrolling ancestor can clip
   it. Everything but the border and the background comes from JS —
   typeahead-dropdown.js sets left/top (or bottom, opening upward near the
   screen's foot), width and max-height from the input's rect — so declare no
   position or size here: a CSS value would be the thing that fights it. */
.typeahead-list-floating {
    position: fixed;
}

.typeahead-list li {
    padding: 3px 8px;
    cursor: pointer;
}

.typeahead-list li:hover,
.typeahead-list li.active {
    background: var(--color-bg);
    color: var(--color-accent);
}

/* Muted context after an option (2026-08-07): spaced off the display text,
   small — findable, never the option itself. */
.typeahead-list .typeahead-context {
    margin-left: 10px;
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
}

/* The create-in-place row reads as an action, not a match. */
.typeahead-list li.typeahead-create {
    font-style: italic;
    color: var(--color-text-muted);
}

.typeahead-list li.typeahead-create:hover,
.typeahead-list li.typeahead-create.active {
    color: var(--color-accent);
}

/* The two STATEMENT rows (2026-08-18, the database-backed pickers): "more matches" and
   the failure line. Neither is an option — OptionCount counts matches and the create row
   only, so the keyboard highlight cannot reach either — so neither may wear the pointer
   or light up under the mouse, or it reads as something you can click. The hover
   overrides are needed because `.typeahead-list li:hover` is the same specificity and
   comes first; these must out-rank it, which the extra class does. */
.typeahead-list li.typeahead-notice {
    cursor: default;
    font-style: italic;
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
}

.typeahead-list li.typeahead-notice:hover {
    background: none;
    color: var(--color-text-muted);
}

/* Colour AND words: the sentence says it failed, so a reader who cannot tell the two
   rows apart by hue still reads which one this is. */
.typeahead-list li.typeahead-notice-error,
.typeahead-list li.typeahead-notice-error:hover {
    color: var(--color-breach);
}

/* ------------------------------------------------------------
   Options dropdown — hand-rolled, flat.
   ------------------------------------------------------------ */
.dropdown {
    position: relative;
}

/* z-index 101 — above the search window in BOTH its shapes (2026-08-16). The
   menu hangs off the action panel and overflows downward into the area the
   right-hand dock occupies, and the search window carries z-index 99, so at
   the old 30 the menu opened BEHIND it: docked, the bottom two thirds of the
   user menu simply vanished. Undocked it is the same story, since the window
   drags as far up as the top of the screen.
   Above the modal backdrop (100) as well, which is deliberate and costs
   nothing: the menu can never be open while a modal is, because every item
   that opens one closes the menu first (BeginResetDemo) and a backdrop covers
   the button that would open it. Below the interruption dialogs (102) all the
   same. */
.dropdown-menu {
    position: absolute;
    top: 100%;
    right: 0;
    z-index: 101;
    display: flex;
    flex-direction: column;
    min-width: 190px;
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
}

/* Section divider (2026-07-21): modes / links / sign out. */
.dropdown-sep {
    border-top: 1px solid var(--color-border);
    margin: 3px 0;
}

.dropdown-item {
    display: flex;
    align-items: center;
    padding: 5px 10px 5px 6px;
    background: none;
    border: 0;
    color: var(--color-text);
    font-family: var(--font-family);
    font-size: var(--font-size-base);
    text-align: left;
    text-decoration: none;
    cursor: pointer;
}

.dropdown-item:hover:not(:disabled) {
    background: var(--color-bg);
    color: var(--color-accent);
}

.dropdown-item:disabled {
    color: var(--color-text-muted);
    cursor: not-allowed;
}

.dropdown-check {
    width: 16px;
    flex: none;
    color: var(--color-accent);
}

/* ------------------------------------------------------------
   Roster grid: rows = employees (grouped), columns = dates.
   Column template is set inline (day count varies).
   ------------------------------------------------------------ */
.roster-grid {
    display: grid;
    min-width: max-content;
}

.grid-corner,
.grid-date-header {
    position: sticky;
    top: 0;
    z-index: 3;
    height: var(--row-height);
    display: flex;
    align-items: center;
    padding: 0 6px;
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border-strong);
    border-right: 1px solid var(--color-border);
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
    white-space: nowrap;
}

.grid-corner {
    left: 0;
    z-index: 4;
    gap: 4px;
    border-right: 1px solid var(--color-border-strong);
    /* The first grid track is content-sized (fit-content(200px), floored at
       160px via min-width here and on the name cells — fit-content can't nest
       in minmax()). The corner's typeahead has a large intrinsic width that
       would otherwise pin the track at the 200px cap — exclude it from track
       sizing (it lays out in whatever width the names produce; the typeahead
       flex-shrinks). In reorder mode the track is a fixed 290px instead — see
       --name-col-width in :root. */
    contain: inline-size;
    min-width: 160px;
}

.grid-corner .typeahead {
    flex: 1;
    min-width: 0;
}

.grid-corner .typeahead-input {
    width: 100%;
    height: 22px;
    font-size: var(--font-size-small);
}

.corner-toggle {
    display: flex;
    align-items: center;
    gap: 3px;
    flex: none;
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
    cursor: pointer;
}

.corner-toggle input[type="checkbox"] {
    margin: 0;
}

.grid-date-header.weekend {
    background: var(--color-surface-alt);
}

.week-no {
    margin-left: 3px;
    opacity: 0.75;
}

/* ------------------------------------------------------------
   Time-of-day markers (06:00 / 12:00 / 18:00). Hidden by default;
   the zoom module (roster-zoom.js) adds .time-ticks to .roster-grid
   once the time axis is zoomed in far enough that the day columns
   are wide enough to carry the labels legibly. Each marker is a small
   tick mark + label pinned to the bottom of the date header, centred
   on its time — no lines run down the cells.
   ------------------------------------------------------------ */
.time-tick {
    display: none;
}

.roster-grid.time-ticks .grid-date-header {
    /* Lift the date to the top so the tick marks get the bottom edge. */
    align-items: flex-start;
    padding-top: 3px;
}

.roster-grid.time-ticks .time-tick {
    display: block;
    position: absolute;
    bottom: 1px;
    transform: translateX(-50%);
    font-size: 9px;
    line-height: 1;
    font-weight: 400;
    color: var(--color-text-muted);
    text-align: center;
    pointer-events: none;
}

/* The tick mark itself: a short vertical line centred on the time, below
   the label. */
.roster-grid.time-ticks .time-tick::after {
    content: "";
    display: block;
    width: 1px;
    height: 4px;
    margin: 1px auto 0;
    background: var(--tick-line);
}

/* ------------------------------------------------------------
   Admin section.
   ------------------------------------------------------------ */
.admin {
    padding: 16px;
    max-width: 620px;
}

.admin h1 {
    font-size: 13px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin: 0 0 8px;
}

.admin .muted {
    color: var(--color-text-muted);
}

.admin .notice {
    color: var(--color-breach);
}

.admin-table {
    border-collapse: collapse;
    width: 100%;
    margin: 8px 0;
}

.admin-table th,
.admin-table td {
    border: 1px solid var(--color-border);
    padding: 4px 8px;
    text-align: left;
}

.admin-table th {
    background: var(--color-surface);
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.admin-form {
    display: flex;
    gap: 8px;
    align-items: center;
    margin: 8px 0;
}

.admin-link {
    color: var(--color-accent);
    text-decoration: none;
}

/* Sys-admin breadcrumb trail (Components/Shared/Breadcrumbs). Base font size
   — navigation users read is never shrunk (feedback 2026-07-22) — closed off
   by a hairline rule with clear air beneath it. */
.breadcrumbs {
    display: flex;
    align-items: center;
    gap: 6px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--color-border-strong);
    margin-bottom: 20px;
}

.breadcrumbs a {
    color: var(--color-accent);
    text-decoration: none;
}

.breadcrumbs a:hover {
    text-decoration: underline;
}

.breadcrumb-sep,
.breadcrumb-current {
    color: var(--color-text-muted);
}

/* The trail's opposite end: one click to the organisation's roster. */
.breadcrumbs .breadcrumb-roster {
    margin-left: auto;
}

/* The organisation pages (list + tabbed detail). The detail page hosts the
   rules component, so it takes the rules-page width, not .admin's 620px. */
.org-page {
    max-width: 1200px;
}

/* The sys-admin landing page. Tables lean WIDE (Robert, 2026-07-21): .admin's
   620px leaves table pages feeling cramped — when adding one, start from 980px+
   and give free-text columns room (the Changes page runs to 1400px). */
.sysadmin-page {
    max-width: 980px;
}

/* Tabs in the ghost-button language: transparent, hover fills the field tone,
   the active tab holds an accent underline (and keeps it while hovered). */
.org-tabs {
    display: flex;
    gap: 2px;
    border-bottom: 1px solid var(--color-border-strong);
    margin: 8px 0 12px;
}

.org-tab {
    background: transparent;
    border: none;
    border-bottom: 2px solid transparent;
    color: var(--color-text-muted);
    font: inherit;
    font-size: var(--font-size-small);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding: 6px 10px;
    cursor: pointer;
}

.org-tab:hover {
    background: var(--color-field);
}

.org-tab.active,
.org-tab.active:hover {
    color: var(--color-accent);
    border-bottom-color: var(--color-accent);
}

.org-details-table th {
    width: 180px;
}

/* Department/employee pages (2026-07-22): a small column of labelled fields —
   the modal-form's language, page-hosted and width-capped. */
.entity-form {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin: 10px 0 14px;
    max-width: 420px;
}

/* Labels stay at the base font size — form text users read is never shrunk
   (feedback 2026-07-22, a house rule). */
.entity-form label {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 8px;
    color: var(--color-text-muted);
}

.entity-form .select {
    width: 260px;
    box-sizing: border-box;
}

/* Several email addresses on one line need far more than the 260px default,
   or the list scrolls sideways while you type it (2026-08-15). */
.entity-form .select.error-recipients-input {
    width: 100%;
    max-width: 560px;
}

/* The job page's form (design pass 2026-08-09, Robert's ask): titled hairline
   section heads (the rule modal's pattern) group Details / Appearance / Tags /
   Settings, and every row sits on a two-column grid whose fixed label track
   lines the controls up. NOTHING here is small: fields and labels read at the
   base size (the house never-shrink rule) — the shared .edit-tags/.tag-chip
   chrome is small in the roster band, so this page re-asserts the size
   locally. Only the section heads keep the table-header chrome type. */
.job-form {
    max-width: 780px;
}

/* The roster's edit dialog shares these heads (round 12, 2026-08-13) rather
   than growing a third copy — same chrome type, same hairline, one rule. */
.job-form .form-section-head,
.roster-edit-body .form-section-head {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 14px 0 2px;
    font-size: var(--font-size-small);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
}

.job-form .form-section-head:first-child,
.roster-edit-body .form-section-head:first-child {
    margin-top: 0;
}

.job-form .form-section-head::after,
.roster-edit-body .form-section-head::after {
    content: "";
    flex: 1;
    border-top: 1px solid var(--color-border);
}

.job-form .form-row {
    display: grid;
    grid-template-columns: 250px minmax(0, 1fr);
    align-items: center;
    column-gap: 14px;
    margin: 0;
    color: var(--color-text);
    font-size: var(--font-size-base);
}

.job-form .form-row .form-label {
    font-weight: 600;
    color: var(--color-text);
    padding: 4px 0;
}

/* Rows whose control wraps (chips + a picker) top-align against their label. */
.job-form .form-row.edit-tags {
    align-items: start;
}

.job-form .form-row.edit-tags .form-label {
    padding-top: 6px;
}

.job-form .edit-tags .tag-armed {
    max-width: none;   /* the page is wider than the modal's 300px squeeze */
}

.job-form .tag-chip {
    font-size: var(--font-size-base);
    height: 26px;
    padding: 0 4px 0 8px;
}

.job-form .tag-chip-remove {
    font-size: var(--font-size-base);
}

.job-form .colour-swatch.colour-auto {
    font-size: var(--font-size-base);
}

/* Seven characters of hex never need the grid track's full width. */
.job-form .colour-hex {
    width: 90px;
}

/* Two digits of unassigned-row count need even less (Details tab, 2026-08-10). */
.job-form .row-count-input {
    width: 70px;
}

.job-form .form-row input[type="checkbox"] {
    justify-self: start;
    width: 15px;
    height: 15px;
    margin: 4px 0;
}

.entity-actions {
    display: flex;
    align-items: center;
    gap: 10px;
}

/* System settings (2026-07-22): a radio per policy with its plain-English
   explanation beneath the label — wider than the entity-form default so the
   explanations breathe (tables lean wide, the house feedback). */
.policy-options {
    max-width: 640px;
}

.policy-options .policy-option {
    justify-content: flex-start;
    align-items: flex-start;
    color: var(--color-text);
}

.policy-options .policy-option input {
    margin-top: 3px;
}

/* Page-hosted Save keeps button proportions (the rule-editor precedent) —
   the full-width .btn-primary look belongs to modals. */
.entity-actions .btn-primary {
    width: auto;
    padding: 0 14px;
}

/* The temporary CircuitResumeRecovery trial dropdown (2026-08-14): a select at
   its own readable width, with the three options' one-line explanations under
   it. Both go when the trial is settled and the dropdown is removed. */
.resume-recovery-select {
    max-width: 260px;
    margin-bottom: 8px;
}

.policy-options .resume-recovery-explain {
    margin: 4px 0 0;
}

/* Divides one system setting from the next (2026-07-23); one Save sits below. */
.settings-hr {
    max-width: 640px;
    margin: 18px 0;
    border: 0;
    border-top: 1px solid var(--color-border);
}

/* ---- Marketing settings (2026-08-03): the landing page's hero and the carousel
   slides. Three free-text columns, so it takes the wide treatment the house rule
   asks for rather than .sysadmin-page's 980px. ---- */
.marketing-page {
    max-width: 1400px;
}

/* The setting dividers run the page's width here, not the 640px the narrow
   System Settings column wants. */
.marketing-page .settings-hr {
    max-width: none;
}

.marketing-page .settings-note {
    max-width: 760px;
}

/* The tabs' "+ New …" buttons sit above their MBSearchableTable — button
   proportions, not the modal's full width (outranks .btn-primary's 100%). */
/* The house toolbar above a table (2026-08-07, Robert's rule): the create
   button sits at the TOP RIGHT, flush with the table's right edge, on every
   organisation tab. Right-aligned by default, so a lone button needs no
   spacer; a left-hand control (the rules tab's state filter) opts out with
   margin-right: auto. */
.table-toolbar {
    display: flex;
    align-items: center;
    gap: 12px;
    justify-content: flex-end;
    margin: 6px 0 8px;
}

.btn-primary.entity-add-btn {
    width: auto;
    padding: 0 14px;
    margin: 0;
}

/* Data import preview (road-to-live-data item 4, 2026-08-14). A problem is
   words first — the row's own error text — with breach red as the second cue
   (never colour alone). The summary opts out of the toolbar's right-alignment
   so the Import button keeps the house top-right spot. */
.import-problem {
    color: var(--color-breach);
}

.import-summary {
    margin-right: auto;
}

.import-kind-row {
    display: flex;
    gap: 24px;
}

/* The import form is prose and choices, not a 420px column of labelled
   fields, so it opts out of .entity-form's cap (2026-08-16 — the same
   "err generous" rule the tables follow). It is what gives the drop zone
   below a target worth aiming at. */
.entity-form.import-form {
    max-width: 620px;
}

/* Same opt-out for the banner message (2026-08-19): the editor is prose, so it
   takes the wider measure the settings page's own paragraphs have rather than
   the 420px column its short fields sit in. Its label goes above rather than
   beside the box — .entity-form label is a space-between row, which would park
   a one-word label opposite a full-width editor. */
.entity-form.banner-message-field {
    max-width: 620px;
}

.entity-form.banner-message-field label {
    justify-content: flex-start;
}

/* Stacked, not side by side: the drop zone wants the full width, and the
   template download is a separate errand rather than its neighbour. */
.import-file-row {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
    margin: 10px 0 4px;
}

/* The drop zone (2026-08-16). The file input is stretched invisibly across
   the whole box rather than hidden, which is what makes one element serve
   both gestures: a click anywhere opens the picker, a drop anywhere lands on
   the input. So the box needs no cursor or key handling of its own — the
   input carries both, and stays tabbable. Dashed because it is a target, not
   a field; the house no-shadows/no-corners rules hold. */
.import-drop {
    position: relative;
    min-height: 76px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 8px 16px;
    border: 1px dashed var(--color-border-strong);
    background-color: var(--color-surface);
    text-align: center;
}

.import-drop-input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
}

.import-drop-input:disabled {
    cursor: default;
}

/* Dragging over: the accent border thickens INWARD (outline-offset's trick —
   a border-width change would reflow the text under the pointer). */
.import-drop-over {
    border-color: var(--color-accent);
    border-style: solid;
    background-color: var(--color-field);
    outline: 1px solid var(--color-accent);
    outline-offset: -2px;
}

/* Under the input, so the input keeps every click and drop. */
.import-drop-text {
    pointer-events: none;
}

.import-drop-input:focus-visible + .import-drop-text {
    outline: 1px solid var(--color-accent);
    outline-offset: 4px;
}

.btn.import-template-btn {
    width: auto;
    padding: 0 10px;
    align-self: flex-start;   /* the stacked row stretches; a button shouldn't */
}

.admin-table td.import-row-number,
.admin-table th.import-row-number {
    width: 52px;
}

/* The Employees tab's Active column (2026-09-06): a Yes/No cell over a Yes/No
   dropdown, so the column takes only what those need and the two text columns
   keep the width. */
.admin-table td.employee-active,
.admin-table th.employee-active {
    width: 96px;
}

/* The new-organisation form's rule-group tick list (2026-08-14): a checkbox,
   the group's name, and how many rules ticking it would copy in. Deliberately
   NOT a .form-row — a group name ("Rail infrastructure (NR-typical)") is wider
   than that grid's 250px label track, and it reads better beside its own box.
   Named with .entity-form so it out-ranks that block's space-between labels. */
.entity-form .org-rule-choice {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 10px;
    padding: 2px 0;
    color: var(--color-text);
    font-size: var(--font-size-base);
    cursor: pointer;
}

.entity-form .org-rule-choice input {
    margin: 0;
}

/* The rule count sits at the right end of the row, away from the name. */
.entity-form .org-rule-choice .muted {
    margin-left: auto;
}

.jobs-identifier {
    width: 140px;
}

/* Non-label form rows (e.g. the job colour picker) share the label layout. */
.entity-form .entity-form-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 8px;
    color: var(--color-text-muted);
}

/* The job page's swatch picker: the six job-colour tokens + Automatic.
   Selection is an accent outline (visible focus ring style, not colour alone). */
.colour-picker {
    display: flex;
    align-items: center;
    gap: 6px;
}

.colour-swatch {
    width: 22px;
    height: 22px;
    padding: 0;
    border: 1px solid var(--color-border-strong);
    cursor: pointer;
}

.colour-swatch.colour-auto {
    width: auto;
    padding: 0 8px;
    background: var(--color-field);
    color: var(--color-text);
    font: inherit;
    font-size: var(--font-size-small);
}

.colour-swatch.selected {
    outline: 2px solid var(--color-accent);
    outline-offset: 1px;
}

/* The custom-colour option (2026-07-22): the native colour input styled as a
   seventh swatch — clicking it opens the browser's picker. */
input.colour-swatch.colour-custom {
    padding: 1px;
    background: var(--color-field);
}

/* The typed route to the same custom colour: a hex value, # optional. */
.colour-hex {
    width: 90px;
}

/* ------------------------------------------------------------
   The organisation's colour palette (2026-08-07): the Colours
   tab's rows and the context menu's colour list. A chip is the
   small square that carries the colour beside its NAME — the
   name is always there, so the colour is never the only cue.
   ------------------------------------------------------------ */
.colour-chip {
    display: inline-block;
    width: 12px;
    height: 12px;
    margin-right: 6px;
    border: 1px solid var(--color-border-strong);
    vertical-align: -1px;
    flex: none;
}

/* "None" has no colour to show, so it shows the absence of one: an
   empty chip with a diagonal line through it. */
.colour-chip-none {
    background:
        linear-gradient(to top right,
            transparent calc(50% - 1px), var(--color-border-strong) calc(50% - 1px),
            var(--color-border-strong) calc(50% + 1px), transparent calc(50% + 1px));
}

/* The Colours tab's live preview: the name as a planner will meet it on the
   grid, in the picked colour with the ink a block would actually use. */
.colour-preview {
    margin: 0 0 12px;
    padding: 6px 10px;
    border: 1px solid var(--color-border-strong);
    font-size: var(--font-size-base);
}

/* The font choice over a custom colour (revealed with one): two chips
   previewing black/white text on the picked colour itself. */
.colour-swatch.ink-swatch {
    font-weight: 600;
    font-size: 12px;
    line-height: 1;
}

.colour-swatch.ink-black { color: #1c232b; }
.colour-swatch.ink-white { color: #f2f3f5; }

.colour-swatch:disabled {
    cursor: default;
    opacity: 0.55;
}

.entity-subhead {
    font-size: var(--font-size-small);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
    margin: 16px 0 6px;
}

.entity-list {
    margin: 0 0 8px;
    padding-left: 18px;
}

/* Details tab break tiers: compact numeric wells inside the sentence. */
.break-tier .tier-input {
    width: 64px;
    box-sizing: border-box;
}

.break-tier .btn {
    width: auto;
    padding: 0 8px;
}

.group-header {
    grid-column: 1 / -1;
    height: 24px;
    display: flex;
    align-items: center;
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border-strong);
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.group-header-label {
    position: sticky;
    left: 0;
    padding: 0 6px;
    display: flex;
    align-items: center;
    gap: 4px;
}

.grid-employee {
    position: sticky;
    left: 0;
    /* Above shift blocks (z-index 1) AND worked-run brackets (z-index 2), so a
       run bracket/label overflowing left past its first shift slides behind the
       names instead of over them (rest-gap lines are z-index 1, already behind).
       Still below the corner (z-index 4); the plain day-header cells (z-index 3)
       are in the day columns and never overlap the name column horizontally. */
    z-index: 3;
    min-width: 160px;   /* the name column's floor (see .grid-corner) */
    height: var(--row-height);
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 0 6px;
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
    border-right: 1px solid var(--color-border-strong);
    white-space: nowrap;
    overflow: hidden;
}

.grid-employee[draggable="true"] {
    cursor: grab;
    user-select: none;
}

.grid-employee[draggable="true"]:active {
    cursor: grabbing;
}

.drag-handle {
    color: var(--color-text-muted);
}

.grid-employee .employee-name {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
}

.guest-marker {
    margin-left: 3px;
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
    font-style: italic;
}

/* Unassigned rows' name cells (2026-08-10, unassigned rows spec §5): muted
   italic over a light diagonal stripe — the stripe is the non-colour cue
   (never colour alone), a sibling of the availability stripes' vocabulary but
   deliberately lighter and wider-spaced (theirs run 7-9px, these 10-13px) so
   "this row is parked-work space" can never read as a verdict. background-image
   over an explicit background-color: the (0,1,0) .grid-employee shorthand
   above would otherwise leave the sticky cell surface-coloured under the
   stripes. The band is FIXED antiquewhite in both themes, so the ink and the
   stripe tint are fixed too (the info-strip owns-its-ink rule) — the dark
   theme's light-gray muted token would fail on it.
   (The per-row entry-count badge that sat here left the grid the same day —
   the counts' display home is the Details tab's modal.) */
.grid-employee.placeholder-name {
    font-style: italic;
    color: #6a6f77;
    background-color: var(--color-placeholder-bg);
    background-image: repeating-linear-gradient(45deg,
        transparent 0 10px,
        rgba(106, 111, 119, 0.18) 10px 13px);
}

/* The block-drag handle keeps the fixed ink too (its own rule reads the
   themed muted token, which fails on the fixed antiquewhite in dark mode). */
.grid-employee.placeholder-name .drag-handle {
    color: #6a6f77;
}

/* What the pointer carries while the unassigned block drags (2026-08-10): a
   plate reading "Unassigned (N)" — the whole section moves, never the one
   grabbed row. The native drag image is hidden (the copy ghost's
   transparent-pixel trick) and roster-entry-drag.js hand-positions this from
   the document-level dragover, removing it at dragend. Fixed inks on the
   fixed antiquewhite, like the name cells. */
.unassigned-drag-ghost {
    position: fixed;
    z-index: 108;   /* the drag-ghost layer — above panel chrome, below the drag tip */
    padding: 5px 10px;
    background: var(--color-placeholder-bg);
    border: 1px solid var(--color-border-strong);
    color: #6a6f77;
    font-style: italic;
    font-size: var(--font-size-base);
    pointer-events: none;
}

/* Remove-from-group: small ✕ that expands with its message on hover. */
.remove-btn {
    flex: none;
    display: flex;
    align-items: center;
    gap: 3px;
    height: 18px;
    padding: 0 3px;
    background: none;
    border: 1px solid transparent;
    color: var(--color-text-muted);
    font-family: var(--font-family);
    font-size: var(--font-size-small);
    cursor: pointer;
}

.remove-btn:hover {
    color: var(--color-breach);
    border-color: var(--color-breach);
    background: var(--color-bg);
}

.remove-btn .remove-text {
    display: none;
}

.remove-btn:hover .remove-text {
    display: inline;
}

/* ------------------------------------------------------------
   Modal (hand-rolled) — flat, token-driven.
   ------------------------------------------------------------ */
.modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: 100;
    background: rgba(0, 0, 0, 0.45);
    display: flex;
    align-items: center;
    justify-content: center;
}

/* The INTERRUPTION dialogs always paint over an ordinary modal (2026-08-16).
   ConfirmDialog and ConsentDialog are the two that are thrown in front of a
   gesture already in progress rather than opened as a destination — so they
   are routinely raised BY a modal (saving the roster's edit-entry dialog into
   a warning; Delete colour inside the colour editor; Void this rule inside the
   rule editor) and must sit on top of the thing that raised them.
   Every backdrop was z-index 100, so which one won came down to DOM order, and
   Home renders the edit-entry dialog after the consent dialog: the "At the
   limit — are you sure?" question opened invisibly BEHIND the dialog, and Save
   read as doing nothing at all. Both components carry this class, so the
   ordering is a rule rather than an accident of markup order — and a new
   modal-over-modal pair only needs it if it is one of these two, since nothing
   else in the app is raised from inside another modal. */
.modal-backdrop.modal-backdrop-top {
    z-index: 102;
}

.modal {
    position: relative;
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
    padding: 16px;
    min-width: 300px;
    max-width: 420px;
}

.modal p {
    margin: 0 24px 14px 0;
}

/* ConfirmDialog's block-content wrapper — the same clearance as .modal p, so
   inline-only confirms look identical and list-bearing ones stay valid HTML. */
.confirm-body {
    margin: 0 24px 14px 0;
}

.confirm-body p {
    margin: 10px 0 0;
}

/* The add-missing confirmation's rule list: one block per rule to be added. */
.apply-preview {
    margin: 10px 0 0;
    padding: 0;
    list-style: none;
}

.apply-preview li {
    margin: 0 0 8px;
}

.modal-close {
    position: absolute;
    top: 4px;
    right: 4px;
    height: 20px;
    width: 20px;
    background: none;
    border: 0;
    color: var(--color-text-muted);
    font-size: var(--font-size-base);
    cursor: pointer;
}

.modal-close:hover {
    color: var(--color-text);
}

.btn-danger {
    width: 100%;
    height: 28px;
    background: var(--color-breach);
    color: #ffffff;
    border: 1px solid rgba(0, 0, 0, 0.35);
    border-radius: 0;
    font-family: var(--font-family);
    font-size: var(--font-size-base);
    font-weight: 600;
    cursor: pointer;
}

.btn-danger:hover {
    filter: brightness(1.1);
}

.btn-primary {
    width: 100%;
    height: 28px;
    background: var(--color-accent);
    color: #ffffff;
    border: 1px solid rgba(0, 0, 0, 0.35);
    border-radius: 0;
    font-family: var(--font-family);
    font-size: var(--font-size-base);
    font-weight: 600;
    cursor: pointer;
}

.btn-primary:hover {
    filter: brightness(1.1);
}

.modal .btn-danger {
    margin-top: 8px;
}

.modal-form {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin: 0 0 14px;
}

.modal-form label {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 8px;
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
}

.modal-wide {
    min-width: 540px;
    max-width: 720px;
}

/* A setting the app will not let you change, and the line that says why (2026-09-11):
   the one-per-entry box on the Tag Groups tab, locked while the group's tags carry
   Fatigue Index answers, and the same tab's second gesture on the tag page, where the
   group picker's options lock instead. The label fades with its disabled control
   (.btn-primary's own pair) so the two read as one dead thing, and the note under it
   carries the meaning — which matters here beyond taste, since a disabled input is not
   focusable and a screen reader steps straight past it. The fade and the note are two
   different things and keep two class names (the house rule); both are named for the
   CONCEPT, not the screen, because the second use arrived the same day the first did.

   Note the page form's version is NOT faded: there the control stays live and only some
   of its options are closed, so fading the label would say the whole field was dead. */
.modal-form label.locked-setting {
    opacity: 0.55;
    cursor: not-allowed;
}

.modal-form .locked-setting-note,
.entity-form .locked-setting-note {
    /* Hugs the field above it rather than sitting a full gap below, so it reads as that
       row's own explanation and not as a new field. */
    margin: -4px 0 0;
    font-size: var(--font-size-small);
    line-height: 1.4;
}

/* ---- The roster's edit dialog (widened 2026-08-10 for the rich note editor,
   spec §6.3 — the rule-editor shell is the width precedent; relaid 2026-08-13,
   round-12 gallery: Robert took option C's job-colour band over option B's
   sectioned body). The modal loses its own padding so the band can run edge to
   edge; .roster-edit-body carries it back. Widened again 2026-09-11 (Robert,
   760px to 850px) so the Times row's "Number of breaks" label fits on one line
   beside the three fields left of it. ---- */
.modal.roster-edit-modal {
    width: min(850px, 94vw);
    max-width: min(850px, 94vw);
    padding: 0;
}

/* The band wears the block's colour by the block's own rule: the job-cN class
   supplies the palette background, an inline BlockColourStyle overrides both
   background and ink for a state colour or a custom job colour. The near-white
   here is .roster-entry's ink, restated because no block class is in play — an
   inline style beats it exactly as it beats the block's. position: relative so
   the ✕ inside positions against the band, not the modal. */
.roster-edit-band {
    position: relative;
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    padding: 9px 34px 9px 14px;
    color: #f2f3f5;
}

.roster-edit-band-job {
    font-size: 13px;
    font-weight: 700;
}

/* Right-anchored, and it wraps under the job name rather than squeezing it
   when a long job name and a long employee name meet on a narrow viewport. */
.roster-edit-band-who {
    margin-left: auto;
    opacity: 0.92;
}

/* Inherits the band's ink, so it stays legible over a light custom colour
   that brought dark ink with it. */
.roster-edit-band .modal-close {
    top: 7px;
    right: 7px;
    color: inherit;
    opacity: 0.85;
}

.roster-edit-band .modal-close:hover {
    color: inherit;
    opacity: 1;
}

.roster-edit-body {
    padding: 14px 16px 16px;
}

/* Start | End | Break on one row, so the whole when-question reads at once.
   Fixed three tracks, not auto-fit: an absence entry has no break field, and
   its two remaining columns must keep the width they have on a work entry. */
.roster-edit-times {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 10px;
    margin-top: 6px;
}

/* The break's two numbers share the third track (2026-09-10) rather than the grid
   growing a fourth. THAT IS WHY THE THREE TRACKS ABOVE ARE FIXED: an absence entry has
   no break field at all, and a fourth track would change the width of the Start and End
   fields it still has. The third track is already far wider than a number needs, so the
   pair fits inside it; the minutes get the larger share, being the number people type. */
.roster-edit-breaks {
    display: grid;
    grid-template-columns: 1.4fr 1fr;
    gap: 8px;
    min-width: 0;
}

/* The break sentence is a DERIVED footnote about the two numbers above it, so it
   wears this dialog's own footnote idiom: .roster-edit-audit's right-aligned italic
   line (Robert, 2026-09-11 — move it to the right and right-align it). Two of them
   now, reading the same way, rather than one caption per invention.
   It SPANS the whole grid row. Left to itself it is a direct child of the three-track
   grid, so it auto-placed into the first track of a second row — under Start, at the
   opposite end of the modal from the numbers it describes. Spanning is also what keeps
   it on one line: the third track alone is ~266px, which wraps this sentence into three
   ragged-left lines, and ragged-left is the one thing right-aligned text must not do.
   TWO classes, not one, and deliberately: `.modal p`'s own `margin: 0 24px 14px 0` is
   (0,1,1) and out-ranks a bare class here — which is why .break-meaning's 6px top margin
   has never applied inside this dialog, and why its 24px right margin is what held the
   sentence off the edge. .roster-edit-audit's comment records the same trap. Margin 0
   because the grid's own 10px gap already spaces it from the fields, and the NOTE head
   below brings its own 14px. */
.roster-edit-times .break-meaning {
    grid-column: 1 / -1;
    margin: 0;
    text-align: right;
    font-style: italic;
}

/* Label ABOVE the control here (the rule form's shape), and at base size —
   the house never-shrink rule; .modal-form's small muted labels are what this
   pass replaced. */
.roster-edit-field {
    display: flex;
    flex-direction: column;
    gap: 3px;
    color: var(--color-text);
    font-size: var(--font-size-base);
}

.roster-edit-field > span {
    font-weight: 600;
}

.roster-edit-field select {
    width: 100%;
}

.roster-edit-field input {
    width: 100%;
}

/* The parenthetical that used to sit in the note's label. Chrome type, so it
   reads as an aside to the head rather than part of the title. */
.roster-edit-head-hint {
    font-weight: 400;
    text-transform: none;
    letter-spacing: 0;
}

.roster-edit-body .note-editor,
.roster-edit-body .edit-tags {
    margin-top: 6px;
}

/* The section head is the label now, so the row is chips-and-picker alone and
   they may use the dialog's full width (the band's 300px squeeze is for the
   80px action panel, which this dialog is not). */
.roster-edit-body .edit-tags .tag-armed {
    max-width: none;
}

/* Destructive left, primary right (round 12): they were two equally heavy
   full-width bars stacked touching, one slip past Save apart. Both take
   button proportions here — the full-width .btn-* look is for confirm-shaped
   modals with one action, which this is not (the .rule-action precedent). */
.roster-edit-actions {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 18px;
}

.roster-edit-actions-spacer {
    flex: 1;
}

.roster-edit-save {
    width: auto;
    padding: 0 34px;
}

/* Red ghost, not a red slab: the app's own ghost-button idea in breach red, so
   Delete reads as available without competing with Save. The hover FILL is the
   confirmation cue — colour is never the only signal, the word says it too. */
.btn-danger.roster-edit-delete {
    width: auto;
    padding: 0 14px;
    margin-top: 0;
    background: none;
    color: var(--color-breach);
    border: 1px solid var(--color-breach);
}

.btn-danger.roster-edit-delete:hover {
    filter: none;
    background: var(--color-breach);
    color: #ffffff;
}

/* Provenance on its own row below the buttons, right-aligned (Robert,
   2026-08-13) — the rule modal's .rule-audit-line pattern, one italic line.
   Written with the body scope because it is a <p> inside .modal, whose own
   `margin: 0 24px 14px 0` would otherwise out-rank a bare class here. */
.roster-edit-body .roster-edit-audit {
    margin: 10px 0 0;
    text-align: right;
    font-style: italic;
    font-size: var(--font-size-small);
}

.modal h2 {
    font-size: 13px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin: 0 0 10px;
}

.modal .muted {
    color: var(--color-text-muted);
}

.changes-list {
    max-height: 55vh;
    overflow: auto;
    display: flex;
    flex-direction: column;
}

.change-row {
    display: flex;
    gap: 10px;
    padding: 3px 4px;
    border-bottom: 1px solid var(--color-border);
}

.change-row.undone {
    opacity: 0.55;
    text-decoration: line-through;
}

.change-when {
    flex: none;
    min-width: 88px;
}

.change-user {
    flex: none;
    min-width: 90px;
    font-weight: 600;
}

.name-input {
    width: 110px;
}

.input-hours {
    width: 64px;
}

/* ------------------------------------------------------------
   Rich notes (2026-08-10, docs/specs/rich-text-notes.md): the one
   editor (Notes band + edit dialog) and the one display (NoteDisplay).
   ------------------------------------------------------------ */
/* The editor wrapper wears the outlined-and-white field look; focus-within
   plays the .select focus rule (the ring sits wholly inside the box). */
.note-editor {
    display: flex;
    flex-direction: column;
    min-width: 0;
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
}

.note-editor:focus-within {
    outline: 2px solid var(--color-accent);
    outline-offset: -2px;
}

.note-editor-toolbar {
    flex: none;
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 1px 4px;
    border-bottom: 1px solid var(--color-border);
}

.note-editor-btn {
    height: 18px;
    min-width: 22px;
    padding: 0 4px;
    border: 0;
    background: none;
    color: var(--color-text);
    font-family: var(--font-family);
    font-size: var(--font-size-small);
    cursor: pointer;
}

.note-editor-btn:hover {
    background: var(--color-field);
}

/* Pressed state follows the caret (bold at the cursor etc.) — accent fill,
   the engaged-button idea at toolbar scale. */
.note-editor-btn.pressed {
    background: var(--color-accent);
    color: #ffffff;
}

.note-editor-counter {
    margin-left: auto;
    font-size: var(--font-size-small);
    color: var(--color-text-muted);
}

.note-editor-counter.over {
    color: var(--color-breach);
    font-weight: 600;
}

/* pre-wrap: typed newlines and runs of spaces display as typed — the module's
   serialiser maps them to <br>/&nbsp; on the way to the server. Explicit
   14px (Robert, 2026-08-10): the inherited 12px token read too small for a
   typing surface; the line height is stated because the dialog's three-row
   minimum below is computed from it. */
.note-editor-area {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 3px 6px;
    outline: none;   /* the wrapper's focus-within ring is the affordance */
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    cursor: text;
    color: var(--color-text);
    font-size: 14px;
    line-height: 1.4;
}

.note-display {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* R5 — compact rendering, everywhere notes render: zero vertical margins on
   lists, just enough left padding for two-digit markers. Notes are dense
   working text, not documents — Pro Tools density applies inside them too. */
.note-display ol, .note-display ul,
.note-editor-area ol, .note-editor-area ul {
    margin: 0;
    padding-left: 18px;
}

.note-display li,
.note-editor-area li {
    margin: 0;
}

/* The notes-dock variant fills the dock's remaining height — the point of the
   move (2026-08-10): the 80px band showed a toolbar and two lines, the dock
   shows as many as its half or full column holds, then scrolls internally. */
.note-editor-dock {
    flex: 1;
    min-height: 0;
    margin: 8px 10px 10px;
}

/* The dialog variant shows THREE rows even when empty (Robert, 2026-08-10 —
   was a fixed 160px box, mostly dead space on a short note), grows with the
   note, then scrolls at the old reading cap. 3 × 1.4em is three real lines at
   the editor's stated line height; +6px is the area's vertical padding. */
/* The system-settings variant SHARES this rule rather than the class name (the
   house rule is that two different things never share a class NAME — sharing a
   rule is fine): a banner message wants exactly the same "a few rows, then
   scroll" box, and calling a settings field .note-editor-dialog would be a lie
   the next reader has to unpick. */
.note-editor-dialog .note-editor-area,
.note-editor-settings .note-editor-area {
    min-height: calc(3 * 1.4em + 6px);
    max-height: 160px;
}



/* The ADD DOCK's variant (2026-08-12, Robert: "only take up a few rows … any
   further text that exceeds the limit can scroll within those rows. It doesn't
   get longer"). So unlike the dialog's, this box does NOT grow with the note:
   it is four rows whatever is in it, and the overflow the base
   .note-editor-area already carries does the rest. Fixed height stays the right
   call now that the form itself scrolls (2026-08-13): a note box that grew with
   its text would put the whole panel into scroll on the strength of one long
   note, and it is the last row, so there is nothing below it worth revealing.
   BOTH declarations are needed: `flex: none` undoes the base rule's `flex: 1`,
   which otherwise stretches or shrinks the area to whatever height the wrapper
   ends up with and makes the height below dead letter; and 4 × 1.4em is four
   real lines at the editor's stated line height, +6px its vertical padding. */
.note-editor-add .note-editor-area {
    flex: none;
    height: calc(4 * 1.4em + 6px);
}

/* ------------------------------------------------------------
   Status bar: one line at the very bottom; all mode hints.
   ------------------------------------------------------------ */
.status-bar {
    flex: none;
    height: 22px;
    display: flex;
    align-items: center;
    padding: 0 10px;
    background: var(--color-surface);
    border-top: 1px solid var(--color-border-strong);
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
    white-space: nowrap;
    overflow: hidden;
}

.status-bar.delete-mode {
    background: var(--color-breach);
    color: #ffffff;
}

/* (The green Assign/Notes bar lived here until 2026-08-12. Notes gave it up
   when its editor docked on 2026-08-10 — with the band unpainted, a green
   bottom strip alone reads as a bug rather than a signal — and Add gave it up
   the same way when its controls docked. Delete keeps its red above: it has no
   dock, so the chrome IS its signal.) */

.status-bar .dismissable {
    cursor: default;
    user-select: none;
}

.status-bar .status-notice {
    color: var(--color-breach);
    font-weight: 600;
}

.status-bar.delete-mode .status-notice {
    color: #ffffff;
}

/* ------------------------------------------------------------
   Context menu (right-click on a shift).
   ------------------------------------------------------------ */
.context-backdrop {
    position: fixed;
    inset: 0;
    z-index: 90;
}

.context-menu {
    position: fixed;
    z-index: 95;
    display: flex;
    flex-direction: column;
    min-width: 150px;
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
}

/* A nested menu (2026-08-07): hover the parent row, the flyout appears beside
   it. CSS-only — there is no state to keep, and a hover menu driven from .NET
   would round-trip on every pointer move across the list. */
.dropdown-submenu {
    position: relative;
    display: flex;
    flex-direction: column;
}

/* The parent row fills the menu's width and pushes its arrow to the right edge,
   which is the affordance saying "there is more this way". */
.dropdown-parent {
    width: 100%;
}

.dropdown-arrow {
    margin-left: auto;
    padding-left: 12px;
    color: var(--color-text-muted);
}

.dropdown-submenu:hover .dropdown-parent,
.dropdown-submenu:focus-within .dropdown-parent {
    background: var(--color-bg);
    color: var(--color-accent);
}

.dropdown-flyout {
    display: none;
    position: absolute;
    top: -1px;             /* aligns the flyout's first row with its parent row */
    left: 100%;
    z-index: 96;
    flex-direction: column;
    min-width: 170px;
    /* A long palette scrolls rather than running off the bottom of the screen. */
    max-height: 60vh;
    overflow-y: auto;
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
}

/* Keyboard reaches it too: focus-within keeps the flyout open while tabbing
   through its buttons. */
.dropdown-submenu:hover .dropdown-flyout,
.dropdown-submenu:focus-within .dropdown-flyout {
    display: flex;
}

/* Near the window's edges the flyout opens the other way — the classes are set
   by context-menu.js once it has measured both (2026-08-07). Only the sides
   move; everything else about the flyout is unchanged. */
.context-menu.flyout-left .dropdown-flyout {
    left: auto;
    right: 100%;
}

.context-menu.flyout-up .dropdown-flyout {
    top: auto;
    bottom: -1px;
}

/* ------------------------------------------------------------
   Drop markers: a thick accent insertion line showing exactly
   where the dragged row will land — above the hovered row,
   below a header (= top of that section), or on the end-strip
   (= bottom of that section).
   ------------------------------------------------------------ */
/* One shape: a 6px bar with inward-pointing arrowheads at both ends. */
.grid-employee.drop-above::before,
.grid-employee.drop-under::after,
.group-header.drop-above::before,
.group-header.drop-below::after,
.drop-end.drag-over::before {
    content: "";
    position: absolute;
    left: 0;
    width: 200px;
    height: 16px;
    background: var(--color-accent);
    clip-path: polygon(0 0, 10px 5px, calc(100% - 10px) 5px, 100% 0, 100% 100%, calc(100% - 10px) 11px, 10px 11px, 0 100%);
    z-index: 6;
    pointer-events: none;
}

.grid-employee.drop-above::before,
.group-header.drop-above::before {
    top: -8px;
}

.group-header {
    position: relative;
}

.group-header.drop-below::after {
    bottom: -8px;
}

/* The unassigned-block drag (2026-08-10): the same thick accent line, UNDER
   the hovered employee's row — the block lands exactly there. */
.grid-employee.drop-under::after {
    bottom: -8px;
}

.drop-end {
    grid-column: 1 / -1;
    position: relative;
    height: 10px;
    border-bottom: 1px solid var(--color-border);
}

.drop-end.drag-over::before {
    top: -8px;
}

.roster-cell {
    position: relative;
    height: var(--row-height);
    background: var(--color-bg);
    border-bottom: 1px solid var(--color-border);
    border-right: 1px solid var(--color-border);
}

.roster-cell.weekend {
    background: var(--color-surface-alt);
}

/* ------------------------------------------------------------
   Shift blocks: proportional sub-blocks of the day. A block is
   positioned/sized as a percentage of the 24h cell, so a
   midnight-spanning shift (width > 100%) renders continuously
   across the day boundary into the next cell.
   ------------------------------------------------------------ */
.roster-entry {
    position: absolute;
    top: var(--block-inset);
    bottom: var(--block-inset);
    z-index: 1;
    display: flex;
    align-items: center;
    padding: 0 3px;
    overflow: hidden;
    white-space: nowrap;
    font-size: var(--font-size-small);
    color: #f2f3f5;
    border: 1px solid rgba(0, 0, 0, 0.45);
    cursor: pointer;
    /* A block's text is never selectable (2026-10-07, from Robert's iPad test).
       The block drags through the browser's own drag and drop, and on an iPad
       the press-and-hold that starts that drag is also the gesture that selects
       text, so pressing a block to drag it sometimes selected its label instead.
       On a laptop a press-and-drag on a draggable block was already a drag and
       never a selection, so nothing changes there; the one visible difference is
       that double-clicking a block no longer highlights a word before the edit
       dialog opens. The reorder-mode drag handles and the search window's header
       carry the same rule. The callout line is iOS-only: it stops the long-press
       menu that would otherwise compete with the drag too. */
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}

/* Shared by shift blocks and the info-strip swatch. */
.job-c1 { background: var(--job-color-1); }
.job-c2 { background: var(--job-color-2); }
.job-c3 { background: var(--job-color-3); }
.job-c4 { background: var(--job-color-4); }
.job-c5 { background: var(--job-color-5); }
.job-c6 { background: var(--job-color-6); }

/* Click-to-highlight: every non-matching job dims. */
.roster-entry.dimmed {
    opacity: 0.22;
}

/* Potential work: dashed border + italic label (never colour alone). */
.roster-entry.potential {
    border: 1px dashed rgba(255, 255, 255, 0.85);
    font-style: italic;
}

/* Externally-managed work (API-fed, e.g. Annual Leave): diagonal ticking + a
   solid white edge — the potential treatment's sibling, distinguishable from
   its dashed border + italics, and never colour alone. These shifts can't be
   replaced by any gesture and only sys-admins delete them.

   The BOTTOM border stays the ordinary block's dark hairline (2026-08-13,
   Robert's bug report): in fill mode the cell's time ribbon lays a white mark
   along the blocks' bottom edge, and an entry covering the whole day makes
   that mark full width — with a white border under it the two merged into one
   fat white band and the block read as unfinished, while an ordinary all-day
   absence (Sick Leave) looked right. Three white sides plus the ticking carry
   the cue, so nothing is lost. Written as a longhand override AFTER the
   shorthand, which would otherwise reset it. */
.roster-entry.externally-managed {
    border: 1px solid rgba(255, 255, 255, 0.85);
    border-bottom-color: rgba(0, 0, 0, 0.45);
    background-image: repeating-linear-gradient(
        45deg,
        transparent,
        transparent 6px,
        rgba(255, 255, 255, 0.22) 6px,
        rgba(255, 255, 255, 0.22) 9px);
}

/* Noted entries: a folded corner, top right (Robert, 2026-09-06, choosing "a real
   fold" from a four-option sample page over the 6px flat white triangle it replaces,
   which read as a speck and vanished on pale blocks). Two tones make the fold:
   ::before is the shadow where the corner was (a dark 10px triangle filling the
   corner), ::after the flap folded back over it (a light 9px triangle pointing the
   other way), one pixel smaller so a hairline of shadow shows along the fold. The
   shadow is what gives it an edge on every job colour. Nothing else on a block uses
   ::before, so both pseudo-elements are free here; the coloured cell's fold below
   rides its own span for the reason its comment gives. */
.roster-entry.noted::before {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    border-top: 10px solid rgba(0, 0, 0, 0.35);
    border-left: 10px solid transparent;
}

.roster-entry.noted::after {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    width: 0;
    height: 0;
    border-bottom: 9px solid rgba(255, 255, 255, 0.95);
    border-right: 9px solid transparent;
}

/* The Fatigue Index figure on a block (Phase 2 slice 2, 2026-09-08): one decimal at
   the top right, in the block's OWN INK. Never a colour of its own and never a chip —
   the severity of a score belongs to the heat map and to the rule that refuses a
   placement, and a coloured figure here would ask a reader to judge a number by its
   tone on top of a job colour they did not choose. Inert (pointer-events: none), like
   the coloured cell's fold, so every block gesture still lands on the block; and no
   native tooltip, because blocks lost theirs on 2026-07-28 and the info strip is the
   one place a block's marks are explained.

   .has-fi on the block reserves the room, so an ordinary job name stops short of the
   figure — the block is a flex row with no ellipsis, so without the padding the name's
   letters would show between the digits. It does NOT bound a long one (overflow: hidden
   clips at the padding box, so the name runs into the reserved space), and since the plate
   went on 2026-09-10 NOTHING DOES: a name past the reserved width now shows through the
   digits at full strength. Read the background-color note below before treating that as a
   defect, and the one-line cure if it ever has to be closed. A NOTED block's fold owns the
   corner, so the figure steps 9px left of wherever `right` puts it.

   IT IS THE FIGURE AND NOTHING ELSE (Robert, 2026-09-11). A small dot rode after it,
   under .roster-entry-fi-own, saying the entry was scored on a profile of its own; he
   asked for it to go and the rule went with it. The fact is still SAID, in the one place
   it was ever explained in words — the info strip's third line reads "scored on its own
   profile" — so do not read its absence here as the information being lost. If a marker
   is ever wanted again it needs a name the reader is told, not a punctuation mark. */
.roster-entry .roster-entry-fi {
    position: absolute;
    /* HALF A PIXEL UP (Robert, 2026-09-10, from the block tuner). A negative `top` is
       unusual and deliberate: the box is bounded by the block's overflow, so the half
       pixel simply tucks the digits' line box against the border rather than showing
       above it, and it is what settles the figure optically against a 32px row. */
    top: -0.5px;
    /* IT HUGS THE FIGURE (Robert, 2026-09-10, revising the full-height plate he asked for
       earlier the same day): "I want it to only take up enough space to show the fatigue
       index score and have a small padding. This way the text of the job name can still be
       read to the end." So no `bottom`, and the padding below is what the box is made of.
       THE TWO VALUES ARE ROBERT'S OWN, typed by him after seeing a 2px-all-round first cut:
       none above, 1px below, so the box is 12px — the digits flush with the block's top
       edge and a single pixel of plate under them. Do not round them to something tidier.

       What the full-height version was for, and what is given up by hugging: on a TALL
       block the label is vertically centred well clear of this box, so the name runs past
       the figure and reads to its end, which is the case Robert was looking at. On a
       DEFAULT 30px row the label's line box (155 to 167 on a measured block) still crosses
       this one (151 to 163), so a long name shows its bottom four pixels under the plate
       as a sliced fragment rather than being cut dead — the trade the full-height version
       was making, now taken the other way. He has seen both; do not put `bottom: 0` back
       without asking. */
    padding-top: 0px;
    padding-bottom: 1px;
    right: 2px;
    /* THE PLATE IS GONE (Robert, 2026-09-10 evening, from the block tuner's own checkbox;
       it had been added that morning). It was `background-color: inherit`, the block's own
       colour, so a long job name disappeared behind the digits — and what makes losing it
       bearable is the rest of the same tuning run: the row is 2px taller and the name is
       nudged 2px down while the figures are showing, so the two lines sit further apart
       than they did when the plate was thought necessary.

       KNOWN AND ACCEPTED, so nobody re-reports it: they still cross, and with nothing
       opaque here a name past the reserved 30px now shows THROUGH the digits at full
       strength rather than behind them. Robert chose this having seen the tuner report the
       overlap in pixels. THE ONE-LINE CURE, if it ever has to be closed, is not to put the
       plate back but to bound the name: give .roster-entry-label `flex: 1 1 auto;
       min-width: 0; overflow: hidden; text-overflow: ellipsis` (round 13's option A), and
       the name can then never reach the figure at all. */
    background-color: transparent;
    padding-left: 5px;
    font-size: 9px;
    line-height: 11px;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
    /* NO `color` DECLARATION, deliberately: the figure inherits the block's own ink and so
       reads at the same strength as the label. It was color-mix at 88% until 2026-09-10,
       when Robert took it to 100% in the block tuner — and 100% of currentColor IS
       currentColor, so the honest way to write it is not to write it. The 88% existed to
       let the figure read a shade softer than the label; with the plate gone the figure has
       to hold its own against whatever passes under it, which is the opposite requirement.
       Never express this as an `opacity`: that would fade the whole box, which is the
       review finding of the same day. */
    white-space: nowrap;
    pointer-events: none;
    /* A block narrower than the figure must not clip its LEADING digits, which is what a
       right-anchored span inside an overflow:hidden block does — and a "45.3" clipped to
       "5.3" reads as a valid smaller score, where every other thing a block clips is a
       job name and obviously truncated. Bounded to the block instead, so the ellipsis
       says the number is cut. Narrow blocks are ordinary in the time-axis view. */
    max-width: calc(100% - 6px);
    overflow: hidden;
    text-overflow: ellipsis;
}

/* 2px + the fold's 10px, less the 1px the fold's hypotenuse gives back at the digits'
   own line: the figure steps 9px left of wherever `right` puts it, which is the
   relationship this rule has always carried (it read 12px while `right` was 3px). */
.roster-entry.noted .roster-entry-fi {
    right: 11px;
}

.roster-entry.has-fi {
    padding-right: 30px;
}

.roster-entry.has-fi.noted {
    padding-right: 39px;
}

/* ------------------------------------------------------------
   THE TWO VALUES THAT THE FATIGUE INDEX PAYS FOR (Robert, 2026-09-10, from the block
   tuner). Six values came out of that run; four of them sit on .roster-entry-fi, which
   only exists on a block that HAS a score, so they scope themselves. These two do not —
   a row height and a label nudge reach every block on the grid — so they are gated on
   the grid's own data-fatigue-index, which Home sets from whether the figures are
   showing. An organisation with the module off keeps the 30px row and the unnudged
   name, exactly as before.

   WHY IT IS THE FIGURES RATHER THAN THE SETTING: Robert asked for a view button that
   turns the figures off, which does not exist yet. When it lands it changes ONE
   expression, Home's ShowsFatigueIndex, and the grid goes back to its old proportions
   with it. Gating on the organisation's setting instead would have left the taller row
   behind after such a toggle.

   The 2px on the label is what lifts the job name clear of the figure's line, and the
   2px on the row is what gives it somewhere to go. Neither is worth having without the
   other. Note the label is a flex item under align-items: center, so a 2px margin moves
   the text down by ONE pixel; that is the tuned value, not a typo for 4.

   TAGS MODE IS DELIBERATELY NOT NUDGED, having been looked at: .block-tag-line stacks in
   a row whose height is exactly lines x --block-line + insets + 4px, so a 2px nudge would
   consume the whole 2px of slack and stand the last line on the block's bottom border.
   An untagged entry in that mode falls back to the job label and so does get the nudge,
   which is a 2px difference between two block shapes that already look nothing alike.
   ------------------------------------------------------------ */
.roster-grid[data-fatigue-index="true"] {
    --row-height: 32px;
}

/* The job label in its own span (2026-09-10). Wrapping it is the one structural change
   the nudge needs, and it lives in RosterCell's BlockContent so live blocks and ghosts
   can never read differently. */
.roster-grid[data-fatigue-index="true"] .roster-entry-label {
    margin-top: 2px;
}

/* A coloured cell's NOTE (2026-09-06): the same folded corner on the cell, its flap
   in the colour's own ink — --cell-ink rides CellColourStyle beside --cell-colour —
   because a white flap on a pale colour would vanish. A child span rather than the
   cell's pseudo-elements: the Pivot View's drag already owns a cell's ::before (the
   faint block) and ::after (the dim wash), so the fold's two triangles are the SPAN's
   own ::before and ::after. The shadow is a translucent black on a pale colour and
   a translucent white on a dark one (the ink says which), so it darkens or lightens
   whatever the cell wears. Inert, so every cell gesture still lands on the cell; at
   the blocks' z-index so the availability stripes (a background) never cover it. */
.roster-cell .cell-note-mark {
    position: absolute;
    top: 0;
    right: 0;
    width: 10px;
    height: 10px;
    pointer-events: none;
    z-index: 1;
}

.roster-cell .cell-note-mark::before {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    border-top: 10px solid rgba(0, 0, 0, 0.18);
    border-left: 10px solid transparent;
}

.roster-cell .cell-note-mark.ink-light::before {
    border-top-color: rgba(255, 255, 255, 0.28);
}

.roster-cell .cell-note-mark::after {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    width: 0;
    height: 0;
    border-bottom: 9px solid var(--cell-ink, rgba(0, 0, 0, 0.6));
    border-right: 9px solid transparent;
}

/* ------------------------------------------------------------
   Block display: tag names (?blocks=tags — spec docs/specs/
   tag-search-and-block-display.md §2). Blocks stack the entry's tag
   names, one nowrap-ellipsis line each; column width never grows,
   rows do. Blocks are absolutely positioned and cannot grow their
   row, so Home derives each row's line count and hands it down as
   --row-lines on every cell and the row's name cell — the one height
   rule below does the sizing, in tags mode only (.blocks-tags on the
   grid, set by Home). The line height derives from the block's own
   font token, so zoom (which overrides the tokens inline on
   .roster-grid) scales stacked lines the way it scales everything
   else; --block-line resolves against that inline value because it
   is declared on the same element.
   ------------------------------------------------------------ */
.roster-grid {
    --block-line: calc(var(--font-size-small) + 4px);
}

/* max(): a row needing no more than the normal height keeps it exactly (a
   zoomed-tall row absorbs its lines the same way). The +10px is the block's
   3px top/bottom insets and 1px borders, plus 2px breathing. */
.roster-grid.blocks-tags .roster-cell,
.roster-grid.blocks-tags .grid-employee {
    height: max(var(--row-height), calc(var(--row-lines, 1) * var(--block-line) + 2 * var(--block-inset) + 4px));
}

/* Stacked lines: left-aligned, vertically centred. Ghosts carry .roster-entry
   too, so they get the identical treatment. */
.roster-grid.blocks-tags .roster-entry {
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
}

.block-tag-line {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    line-height: var(--block-line);
}

/* ------------------------------------------------------------
   Fill mode's time ribbon (2026-08-11): filling gave up blocks
   sitting at their times, so a miniature of the day is OVERLAID on
   the blocks' bottom edge — one ribbon per cell in whole-day
   coordinates (never per block: that compressed each entry's day
   into its block's equal share, Robert's first correction), with a
   WHITE mark per entry at its true start and length (his second
   correction, restoring the first cut's look: the blocks' own
   coloured field stands for the day, white shows the shift within
   it). The band sits exactly where the in-block version drew it —
   the block's own cell inset plus its 1px border, written as
   calc(var(--block-inset) + 1px) since 2026-09-08 so it follows the
   blocks when the heat map widens that inset; z-index 1
   + later DOM order paints it over the blocks, which tile the cell
   edge to edge in fill mode so the band is always on colour. The
   whites are low-alpha colours, never an opacity on the wrapper (it
   would multiply into the marks); the hairline shadow keeps a mark
   legible over a light custom block colour, where white alone would
   wash out. overflow:hidden clips a midnight spanner's mark at the
   day's end; hit-testing stays the cell's (pointer-events none).

   The mark needs nothing below it — the block's own dark bottom
   border is what bounds it. That is why .roster-entry.externally-
   managed keeps its white edge on three sides only (see its rule):
   a white bottom border under a full-width white mark merged into
   one fat band, and the block read as unfinished. A bottom border on
   THIS wrapper was tried for one revision and reverted — it thickened
   the edge under every entry in the app to fix one block's.
   ------------------------------------------------------------ */
.cell-time-ribbon {
    position: absolute;
    left: 0;
    right: 0;
    bottom: calc(var(--block-inset) + 1px);
    height: 3px;
    background: rgba(255, 255, 255, 0.25);
    overflow: hidden;
    pointer-events: none;
    z-index: 1;
}

.cell-time-ribbon-seg {
    position: absolute;
    top: 0;
    bottom: 0;
    /* a zero-length entry still shows a tick */
    min-width: 2px;
    background: rgba(255, 255, 255, 0.92);
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
}

/* A segment's stand-in INSIDE a block for the length of a flight (the live demo's undo,
   roster-entry-drag.js's carryRibbonMark): the same white and the same hairline, placed by
   the script at the segment's own spot relative to the block, over the block's label. */
.roster-entry .flight-time-mark {
    position: absolute;
    min-width: 2px;
    background: rgba(255, 255, 255, 0.92);
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
    pointer-events: none;
    z-index: 3;
}

/* Mirrors .roster-entry.dimmed: a dimmed block's mark dims with it. */
.cell-time-ribbon-seg.dimmed {
    opacity: 0.22;
}

/* Another user's change arriving live: the new/moved block fades in, and a ghost
   of a removed/moved-away block fades out. Durations mirror Home's ChangeFadeMs;
   the ghost holds opacity 0 (forwards) until its cleanup render removes it, and is
   inert (no pointer events, no data-roster-entry) so nothing can interact with it.
   Deliberately NOT gated on prefers-reduced-motion (same call as the rejected-drop
   return animation): this is the only cue that a change came from another user,
   and an opacity crossfade contains no movement — it is the reduced-motion-safe
   substitute, not the thing that setting exists to remove. */
.roster-entry-appear {
    animation: roster-entry-fade-in 400ms ease-out;
}

.roster-entry-ghost {
    pointer-events: none;
    cursor: default;
    animation: roster-entry-fade-out 400ms ease-in forwards;
}

@keyframes roster-entry-fade-in {
    from { opacity: 0; }
}

@keyframes roster-entry-fade-out {
    to { opacity: 0; }
}

/* Assign mode: cells advertise that clicking creates. Keyed off the grid's
   data-mode (set by Home) rather than a per-cell class, so mode toggles never
   re-render cells. */
.roster-grid[data-mode="assign"] .roster-cell {
    cursor: pointer;
}

/* Unassigned rows' day cells (2026-08-10, unassigned rows spec §5; the colour
   is Robert's antiquewhite, picked the same day): the band that marks a whole
   parked-work row. background-color ONLY, and placed ABOVE the .cell-coloured
   / .avail-* / .batch-fail rules at the same (0,2,0) specificity so ALL of
   them win purely by coming later in the file (the cell-colour block's
   ordering law, one row up the stack: a mark, a verdict stripe or a failure
   paint on a placeholder cell must show). The hover cross-highlight does NOT
   repaint these cells (Robert, 2026-08-10, reversing the first cut): the
   .hl-* rules below carry :not(.placeholder-row) exactly as they carry
   :not(.cell-coloured), so a placeholder cell keeps its colour under the
   cursor and the cross shows a hole there. Beats the weekend shading (same
   specificity, earlier in the file) — a uniform row reads better than
   alternating shades. */
.roster-cell.placeholder-row {
    background-color: var(--color-placeholder-bg);
}

/* ------------------------------------------------------------
   The HEAT MAP's bands (the Fatigue Index, Phase 2 slice 2,
   2026-09-08): a cell holding scored work takes the colour of
   its highest Fatigue Index, on HeatMapView's fixed scale — the
   band edges are the rail-typical triggers (amber from 35, red
   from 45) so the same colour means the same thing on every
   roster and in every week.

   FIXED in both themes, like the unassigned rows' antiquewhite
   and the compliance pin's yellow: a scale a reader learns must
   not move under them when they switch theme.

   THE VIEW IS GATED HERE, on the grid's data-heat, and the cell wears its band class
   whether or not the view is on. That is not tidiness: the availability stripes, the
   search's orange row and the compliance pin are classes JS puts on a cell, and they
   survive a re-render only because Blazor rewrites the class attribute exactly when the
   string it renders changes. A band class that came and went with the view would drop
   all three off every cell holding work, on the toggle, with nothing to repaint them.
   The ancestor is wrapped in :where() so it adds NO specificity, which is what keeps the
   rule at (0,2,0) and leaves the ordering below true.

   ORDER AND SPECIFICITY, both deliberate:
     - it sits ABOVE the .avail-* rules at the same (0,2,0), so a
       VERDICT STRIPE paints over the heat exactly as it paints
       over a cell colour. A stripe is what the roster will not
       allow; the heat is a display.
     - it sits BELOW the hover cross, the search orange and the
       compliance pin, which are all (0,5,0)+ and win wherever
       they sit. That is the WEEKEND tint's treatment rather than
       the cell colour's, and it is a choice: those three mark
       ONE thing under the pointer or one thing you followed a
       link to, where the heat is the whole view. Giving the heat
       the cell colour's privilege would mean repeating a :not()
       chain through fourteen highlight rules, which is precisely
       the fragility the 2026-08-18 stripe rules warn about.
   background-color only, never the shorthand (the 2026-08-18
   rule), so a stripe's background IMAGE always survives it.

   The blocks are inset while the view is on (see .roster-grid
   [data-heat] below), because in the default fill view a single
   block covers its whole cell and the colour would otherwise show
   as a 3px halo. The figure stays on the block in the block's own
   ink, so the colour is never the only cue.
   ------------------------------------------------------------ */
/* GREEN TO DEEP RED (Robert, 2026-09-10), replacing a ramp that began in the grid's own
   near-white blue-greys. His reason is the one that matters: "a roster entry that has a
   score below 15 will still show a different colour but it will be green. It will always
   be obvious that the heat map is engaged." The cool end was so close to the ordinary cell
   background that a whole week of quiet duties looked like the view had not switched on,
   which is the worst thing a heat map can do — a reader cannot tell "nothing is warm" from
   "nothing is painted".

   The band edges are unchanged, so the two that carry meaning still land where they did:
   amber from 35, where a day duty trips the rail-typical trigger, and the turn to red at
   45, where a night one does. What changed is the four tones below 35 and the depth of the
   two above 50.

   The lightness does NOT descend monotonically, and that is the palette rather than a
   slip: green to red passes through yellow, which is intrinsically the lightest hue, so
   any honest green-yellow-red ramp peaks in the middle (it is the shape of every RdYlGn
   scale). Adjacent bands are told apart by hue and saturation instead, and the FIGURE on
   the block is the primary cue in every band — the colour is never alone, which is also
   what keeps the scale usable for a reader who cannot separate green from red. */
.heat-fi-1 { --heat-fi-band: #bfe3b2; }   /* under 15 */
.heat-fi-2 { --heat-fi-band: #9ed48c; }   /* 15 */
.heat-fi-3 { --heat-fi-band: #cfe394; }   /* 25 */
.heat-fi-4 { --heat-fi-band: #f4e3a1; }   /* 35 — a day duty's rail-typical trigger */
.heat-fi-5 { --heat-fi-band: #f2c975; }   /* 40 */
.heat-fi-6 { --heat-fi-band: #e89a72; }   /* 45 — a night duty's */
.heat-fi-7 { --heat-fi-band: #d4604a; }   /* 50 */
.heat-fi-8 { --heat-fi-band: #a52a1e; }   /* 60 and over */

/* THE RISK INDEX's ramp (2026-09-11). Robert's instruction was "the colours should be
   similar, not purple. Just make them subtly different but still make them turn red", and
   that is a deliberate rejection of a ramp in another hue family: the two maps tell the
   same story about the same duties, so a reader should not have to learn a second
   vocabulary to read the second one.

   WHICH MAP YOU ARE IN IS SAID THREE OTHER WAYS, which is what lets the colours be
   siblings rather than opposites: the figure on every block changes precision (one
   decimal to two), the info strip's legend NAMES the index in words, and the button's
   glyph changes. The colour is the one cue that is not asked to carry it.

   THE SHIFT IS PINKER AND DEEPER, applied consistently down the ramp so it reads as a
   family difference rather than four unrelated tweaks: the golds go peach where the
   Fatigue Index's go amber, the warm band goes coral where its sibling goes orange, and
   the top goes CRIMSON where the Fatigue Index's goes brick. The top band is the clearest
   single tell of the two side by side.

   BAND 1 IS BYTE-IDENTICAL to the Fatigue Index's own green, and that is Robert's own
   carve-out, made twice: quiet should look the same in both maps, so "nothing here is
   warm" is one thing a reader learns once. Only four bands move.

   Five bands, not eight, and the reasoning for the edges is on HeatMapView.RiskIndexBands
   where a test can reach it. */
.heat-ri-1 { --heat-ri-band: #bfe3b2; }   /* under 1.00 — the Fatigue Index map's own green */
.heat-ri-2 { --heat-ri-band: #dde196; }   /* 1.00 — the model's own yardstick */
.heat-ri-3 { --heat-ri-band: #f3c584; }   /* 1.30 */
.heat-ri-4 { --heat-ri-band: #e07f63; }   /* 1.50 — a step under the rail-typical 1.60 trigger */
.heat-ri-5 { --heat-ri-band: #ab2438; }   /* 2.00 and over — twice the reference risk */

/* WHICH RAMP THIS MAP READS. A cell wears BOTH metrics' band classes at all times (see
   RosterCell, where the reason is written out: a class that changed with the view or the
   metric would rewrite the cell's class attribute and drop the availability stripes, the
   search orange and the compliance pin off every cell holding work), so both variables are
   set on it and this pair picks one off the grid's own data-heat.

   It assigns a VARIABLE rather than a background, so it joins none of the specificity
   battles below it — which is what lets every rule under here go on saying
   var(--heat-band) exactly as it did when there was one map, instead of doubling a block
   of selectors the 2026-08-18 stripe rules warn is the fragile part of this file. */
.roster-grid[data-heat="fi"] .roster-cell { --heat-band: var(--heat-fi-band); }
.roster-grid[data-heat="ri"] .roster-cell { --heat-band: var(--heat-ri-band); }

/* ONE selector, on the shared `heat` class the cell wears beside its band classes, so
   HeatMapView's band lists stay the only place a band is enumerated: a ninth band added
   there paints without a CSS edit, where hand-listed selectors would have left it
   colourless with nothing failing. Gated on the PRESENCE of data-heat, which the grid
   renders only while a map is on, so it serves both metrics. */
:where(.roster-grid[data-heat]) .roster-cell.heat {
    background-color: var(--heat-band);
}

/* THE HEAT BEATS THE HOVER CROSS (Robert, 2026-09-10: "the hover indicators on the rows
   and columns overrule the css showing the heat map. The heat map should take priority
   and not change"). It is the CELL COLOUR's privilege, granted here for the same reason:
   the cross is a transient pointer aid, where a band is the whole point of the view a
   planner deliberately switched on — and a cross that repaints a third of the visible
   bands makes the scale unreadable exactly while somebody is reading it. The cross still
   shows: it paints every UNSCORED cell of the row and column, and the name cell and date
   header still go bold, so the pointer is never lost — the marked cells are simply holes
   in it, as they already are over a coloured cell and over an unassigned row.

   HOW IT WINS, and the ladder is deliberate on both sides. It repeats the hover rules'
   own :not() chain (the 2026-08-18 rule: repeating the exclusions is the right way to win
   rather than a bigger hammer), which takes it to (0,6,0) against their (0,5,0) — and the
   ancestor sits in :where() so the [data-heat] gate adds nothing, or it would reach (0,7,0)
   and start beating things it must not. At (0,6,0) it TIES with the stripe re-asserts, the
   search-highlight rules and the batch-fail rule below, all of which come LATER in the
   file and therefore still win: a verdict, a followed link and a failed batch each mark
   one thing, where the heat is a display. So a striped cell under the cursor still shows
   its stripe, which is the rule that must never bend.

   THE COMPLIANCE PIN IS EXCLUDED BY NAME, and that is not tidiness (the review of
   2026-09-10 caught it). The pin's own rules are (0,5,0) and do not name a hover class, so
   these would have beaten them: a cell you followed a compliance link to would have lost its
   yellow the moment you moved the pointer onto it, which is when you are looking at it. An
   exclusion is the right tool rather than a bigger number — on a pinned cell neither rule
   matches, and the pin's own rule paints, which is exactly the arrangement it already has
   with the ordinary hover cross. The SEARCH's orange needs no exclusion: its hovered rules
   are (0,6,0) and sit later in the file, so they already win.

   A future exclusion added to the hover rules must be added here too, exactly as it must
   be added to the stripe re-asserts. */
.roster-grid:where([data-heat]) .roster-cell.heat.hl-row:not(.cell-coloured):not(.placeholder-row):not(.pin-hl-row):not(.pin-hl-col),
.roster-grid:where([data-heat]) .roster-cell.heat.hl-col:not(.cell-coloured):not(.placeholder-row):not(.pin-hl-row):not(.pin-hl-col),
.roster-grid:where([data-heat]) .roster-cell.heat.hl-cell:not(.cell-coloured):not(.placeholder-row):not(.pin-hl-row):not(.pin-hl-col) {
    background-color: var(--heat-band);
}

/* Room for the colour to show. In the fill view a lone block tiles its whole cell, so
   without this the heat would be a 3px halo and no map at all; 6px top and bottom of a
   30px row leaves the band a fifth of the row on each side and the block still 18px,
   which holds its label and its figure (checked in a browser). The mock-up's own
   "heat behind the blocks" frame insets by the same proportion.

   Written as the TOKEN, so the time ribbon's offset, the tags-mode row height and the
   Pivot View's faint block move with the blocks rather than detaching from them — which
   is exactly what a literal 6px here did on the first cut (the review of 2026-09-08:
   two of the ribbon's three pixels fell below every block, onto the heat). */
.roster-grid[data-heat] {
    --block-inset: 6px;
}

/* A colour on an EMPTY cell (2026-08-07). Robert's ruling, in two halves:
     - the STRIPE WARNINGS beat the mark. Whether someone can legally work a day
       is more urgent than a note about it, and the stripes only show mid-drag.
     - the HOVER CROSS-HIGHLIGHT does NOT (corrected 2026-08-07, same day, from a
       screenshot: a marked cell went blue under the cursor and the mark simply
       vanished). The cross is a transient pointer aid; the mark is a statement.
   ORDER IS LOAD-BEARING and the two halves are enforced differently. This rule
   sits ABOVE the .avail-* / .batch-fail rules at the same (0,2,0) specificity, so
   those win purely by coming later in the file — move this block below them and
   it reverses. The hover half is enforced the other way, by the .hl-* rules
   carrying :not(.cell-coloured), because they are (0,3,0) and would otherwise
   win wherever they sat.
   The colour arrives as an inline --cell-colour custom property, never an inline
   background — an inline background would outrank every one of those rules. */
.roster-cell.cell-coloured {
    background-color: var(--cell-colour);
}

/* Grid cross-highlight, always on (classes applied client-side by
   hover-highlight.js). The tint follows the active mode — accent blue at rest,
   the engaged green in Assign/Notes, the breach red in Delete — keyed off the
   grid's data-mode (its ONE mode channel, set by Home) so the cursor cross
   always matches the panels. */
.roster-grid {
    --hl-colour: var(--color-accent);
}

.roster-grid[data-mode="assign"],
.roster-grid[data-mode="notes"] {
    --hl-colour: var(--color-assign-bg);
}

.roster-grid[data-mode="delete"] {
    --hl-colour: var(--color-breach);
}

/* :not(.cell-coloured) — a MARKED cell keeps its colour under the cursor
   (Robert, 2026-08-07, correcting the first cut): the cross-highlight is a
   transient pointer aid and the mark is a planner's statement about that day, so
   the row and column tints simply skip it, leaving a hole in the cross. The
   stripe warnings are the other half of that ruling and still paint over a mark —
   see the .avail-* rules below, which stay unqualified.
   :not(.placeholder-row) — the unassigned rows' antiquewhite keeps the same
   privilege (Robert, 2026-08-10: the cursor must not change those cells'
   colour), by the same mechanism: these rules are (0,3,0)+ and would win
   wherever they sat, so the exclusion must live here, not in rule order. */
.roster-grid .roster-cell.hl-row:not(.cell-coloured):not(.placeholder-row),
.roster-grid .roster-cell.hl-col:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--hl-colour) 10%, var(--color-bg));
}

.roster-grid .roster-cell.hl-cell:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--hl-colour) 28%, var(--color-bg));
}

.roster-grid .grid-employee.hl-row {
    font-weight: 700;
}

.roster-grid .grid-date-header.hl-col {
    font-weight: 700;
    color: var(--color-text);
}

/* ------------------------------------------------------------
   The SEARCH's employee highlight (Robert, 2026-09-04): an
   employee picked in the search window marks their row on the
   ORDINARY grid — the name cell orange, the row light orange,
   and the cell under the cursor a darker orange at the hover
   cross's own 28%, with the name bold and italic — for as long
   as the search is armed (closing the window clears the search,
   and the paint with it). Classes painted client-side by
   cell-highlight.js after every rebuild, the pin's contract.
   Fixed oranges in BOTH themes and NOT following the mode the
   hover blue follows (the pinned yellow's precedent: a marker,
   not a theme surface). Same exclusions as the hover cross —
   a marked cell and the unassigned rows keep their own colours.
   Sits AFTER the hover rules so, at equal specificity, the orange
   row wins over the blue row tint while the pointer passes
   through, and BEFORE the pin block so a compliance pin still
   wins where the two cross (its own button removes it).
   background-color only, never the shorthand: the verdict
   stripes are a background IMAGE and must show through (the
   2026-08-18 rule), and the re-asserts below give the stripe's
   own colour back at one specificity higher, as the pin's do.
   ------------------------------------------------------------ */
.roster-grid {
    --search-hl-colour: #f08c1e;
}

.roster-grid .roster-cell.search-hl-row:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--search-hl-colour) 14%, var(--color-bg));
}

/* The hover column crossing the orange row: a step darker, so the cross
   still reads as it passes through. */
.roster-grid .roster-cell.search-hl-row.hl-col:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--search-hl-colour) 20%, var(--color-bg));
}

/* The cell under the cursor on that row: the hover cross's own 28%, in orange. */
.roster-grid .roster-cell.search-hl-row.hl-cell:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--search-hl-colour) 28%, var(--color-bg));
}

/* Stripes over the orange row keep their verdict colour — (0,6,0) and (0,7,0)
   against the (0,5,0) and (0,6,0) rules above. */
.roster-grid .roster-cell.search-hl-row.avail-amber:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--color-warning) 14%,
        color-mix(in srgb, var(--search-hl-colour) 14%, var(--color-bg)));
}

.roster-grid .roster-cell.search-hl-row.avail-red:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--color-breach) 16%,
        color-mix(in srgb, var(--search-hl-colour) 14%, var(--color-bg)));
}

.roster-grid .roster-cell.search-hl-row.hl-cell.avail-amber:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--color-warning) 14%,
        color-mix(in srgb, var(--search-hl-colour) 28%, var(--color-bg)));
}

.roster-grid .roster-cell.search-hl-row.hl-cell.avail-red:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--color-breach) 16%,
        color-mix(in srgb, var(--search-hl-colour) 28%, var(--color-bg)));
}

/* The name cell: solid orange with a fixed dark ink (both themes), bold AND
   italic — the italic is what tells "searched" from the hover cross's plain
   bold and the pin's. */
.roster-grid .grid-employee.search-hl-name {
    background: var(--search-hl-colour);
    color: #3a2104;
    font-weight: 700;
    font-style: italic;
}

/* ------------------------------------------------------------
   The PINNED highlight (compliance link, 2026-08-15): the yellow
   the report's row click lands with — one employee's row, one
   date's column, and the employee's name cell — cleared by the
   panel's "Remove highlight" button. Classes painted client-side
   by cell-highlight.js after every rebuild (the rule-test-probe
   contract). Fixed yellows in BOTH themes (the antiquewhite
   precedent: a marker, not a theme surface), and the same
   exclusions as the hover cross above — a marked cell and the
   unassigned rows keep their own colours, the same ruling by the
   same mechanism. Sits AFTER the hover rules so, at equal
   specificity, the pin wins while the pointer passes through.
   ------------------------------------------------------------ */
.roster-grid .roster-cell.pin-hl-row:not(.cell-coloured):not(.placeholder-row),
.roster-grid .roster-cell.pin-hl-col:not(.cell-coloured):not(.placeholder-row) {
    background-color: #fdf0b3;
}

/* The crossing point — the clicked entry's own cell — reads strongest. */
.roster-grid .roster-cell.pin-hl-row.pin-hl-col:not(.cell-coloured):not(.placeholder-row) {
    background-color: #fbdf74;
}

/* The pinned yellow must not hide a verdict either, and the workflow is
   exactly the one that produces the clash: the compliance report links you to a
   rule-break, and the next thing you do is arm Add mode to fix it — which paints
   the stripes straight onto the pinned row and column. background-color above
   can no longer wipe the stripe IMAGE; these rules give back the stripe's own
   base COLOUR, blended into the pin's yellow so the cell still reads amber or
   red rather than reading as plain yellow with faint texture on it.
   Specificity: the pin rules are (0,5,0) row/column and (0,6,0) at the crossing,
   so a re-assert needs (0,6,0) and (0,7,0) — which is what the repeated :not()
   chain buys, exactly as it does for the hover cross above. */
.roster-grid .roster-cell.pin-hl-row.avail-amber:not(.cell-coloured):not(.placeholder-row),
.roster-grid .roster-cell.pin-hl-col.avail-amber:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--color-warning) 14%, #fdf0b3);
}

.roster-grid .roster-cell.pin-hl-row.avail-red:not(.cell-coloured):not(.placeholder-row),
.roster-grid .roster-cell.pin-hl-col.avail-red:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--color-breach) 16%, #fdf0b3);
}

/* The crossing point keeps reading strongest, over the deeper yellow. */
.roster-grid .roster-cell.pin-hl-row.pin-hl-col.avail-amber:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--color-warning) 14%, #fbdf74);
}

.roster-grid .roster-cell.pin-hl-row.pin-hl-col.avail-red:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--color-breach) 16%, #fbdf74);
}

/* The name cell: the highlight's own yellow with a fixed dark ink (both
   themes), bold like the hover cross's name treatment. */
.roster-grid .grid-employee.pin-hl-name {
    background: #fbdf74;
    color: #3f3410;
    font-weight: 700;
}

/* "Remove highlight" (the eraser, view-group right edge): deliberately the
   loudest control on the bar — it wears the highlight's own yellow so button
   and marker read as one thing, solid where every neighbour is a ghost. Its
   exception is named IN the .btn:hover chain above (the (0,9,0) rule). */
.btn.btn-remove-highlight {
    background: #fbdf74;
    color: #3f3410;
    font-weight: 700;
    border: 1px solid #c9a83c;
}

.btn.btn-remove-highlight:hover {
    background: #f7d24d;
    color: #3f3410;
}

/* ------------------------------------------------------------
   Assignment drag-and-drop (classes applied client-side by
   roster-entry-drag.js — availability painted once at drag start,
   hover feedback is pure CSS from then on). State = colour AND
   pattern: amber gets sparse stripes, red dense ones, so the
   two never rely on hue alone.
   ------------------------------------------------------------ */
/* Blocks advertise dragging only where drags are allowed: every mode but
   Reorder, where rows drag and the JS gate cancels an entry's dragstart
   (None-only until 2026-09-06). draggable itself is unconditional markup. The
   block's own rule outranks the Add-mode cell pointer below it in the tree,
   which is right: in a write mode a block both takes the click AND drags. */
.roster-grid:not([data-mode="reorder"]) .roster-entry {
    cursor: grab;
}

/* No drag affordance on externally-managed blocks the user can't edit (the JS
   gate refuses the dragstart); pointer keeps the click/double-click gestures
   (highlight, note via the edit dialog) honest. */
.roster-grid:not([data-mode="reorder"])[data-em-editable="false"] .roster-entry.externally-managed {
    cursor: pointer;
}

.roster-entry.drag-source {
    opacity: 0.35;
}

/* Grabbing on the press. It covers only the press, and deliberately so: once a
   native HTML5 drag starts the browser draws its own cursor and ignores CSS
   entirely, so the pointer reverts to an arrow for the rest of the gesture (tried
   2026-08-03, confirmed in Chrome — a body-level forced grabbing cursor set on
   dragstart changed nothing). The drag API offers no grabbing cursor either;
   its only choices are copy/move/link/no-drop. Owning the cursor for the whole
   drag means abandoning native DnD for hand-rolled pointer events — a rewrite of
   roster-entry-drag.js, the drag ghost, drag-scroll.js and the reorder-row drags,
   judged not worth it for a cursor (Robert, 2026-08-03). */
.roster-grid:not([data-mode="reorder"]) .roster-entry:active {
    cursor: grabbing;
}

.roster-cell.avail-amber {
    background-color: color-mix(in srgb, var(--color-warning) 14%, var(--color-bg));
    background-image: repeating-linear-gradient(45deg,
        transparent 0 7px,
        color-mix(in srgb, var(--color-warning) 38%, transparent) 7px 9px);
}

.roster-cell.avail-red {
    background-color: color-mix(in srgb, var(--color-breach) 16%, var(--color-bg));
    background-image: repeating-linear-gradient(45deg,
        transparent 0 4px,
        color-mix(in srgb, var(--color-breach) 45%, transparent) 4px 7px);
}

/* The hover cross-highlight must never hide a verdict, and that is held by TWO
   things (both 2026-08-18, from Robert's report that Add mode's green wiped the
   stripes). One: those rules set background-COLOR, never the `background`
   shorthand, which resets background-image to none and was erasing the stripes
   outright — so no highlight can wipe a stripe IMAGE any more, this one or a
   future one. Two: these rules give the stripe back its own base COLOUR with the
   highlight tint blended in, so the cursor stays visible without the verdict
   losing its colour.
   THE :not() CHAIN IS LOAD-BEARING, and leaving it off is how this broke
   (2026-08-18, Robert: the stripes vanished under the Add-mode green). These
   selectors were (0,4,0) against a (0,3,0) hover rule when they were written;
   the hover rules then grew :not(.cell-coloured) (2026-08-07) and
   :not(.placeholder-row) (2026-08-10) for reasons that had nothing to do with
   stripes, which took them to (0,5,0) and silently overtook these. Repeating
   the SAME exclusions here restores the margin — and it is the right way to
   win rather than a bigger hammer, because on an excluded cell neither rule
   then matches and the unqualified .avail-* rules below-in-source paint the
   stripes straight over the mark or the antiquewhite, which is exactly what
   the 2026-08-07 ruling asks for. A future exclusion added to the hover rules
   must be added here too, or the stripes go again. */
.roster-grid .roster-cell.hl-row.avail-amber:not(.cell-coloured):not(.placeholder-row),
.roster-grid .roster-cell.hl-col.avail-amber:not(.cell-coloured):not(.placeholder-row),
.roster-grid .roster-cell.hl-cell.avail-amber:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--color-warning) 14%,
        color-mix(in srgb, var(--hl-colour) 14%, var(--color-bg)));
    background-image: repeating-linear-gradient(45deg,
        transparent 0 7px,
        color-mix(in srgb, var(--color-warning) 38%, transparent) 7px 9px);
}

.roster-grid .roster-cell.hl-row.avail-red:not(.cell-coloured):not(.placeholder-row),
.roster-grid .roster-cell.hl-col.avail-red:not(.cell-coloured):not(.placeholder-row),
.roster-grid .roster-cell.hl-cell.avail-red:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--color-breach) 16%,
        color-mix(in srgb, var(--hl-colour) 14%, var(--color-bg)));
    background-image: repeating-linear-gradient(45deg,
        transparent 0 4px,
        color-mix(in srgb, var(--color-breach) 45%, transparent) 4px 7px);
}

/* ------------------------------------------------------------
   Rest-gap measure lines (?gaps=true): ◄— 14.5hrs —► between an
   employee's consecutive shifts. Drawn in the cell where the
   (visible part of the) gap starts and overflowing across cell
   borders exactly like shift blocks do. Purely informational:
   never intercepts clicks or drops, sits under the blocks.
   ------------------------------------------------------------ */
.rest-gap {
    position: absolute;
    top: 0;
    bottom: 0;
    /* Like .roster-entry: z-index 1 lifts the overflow above later sibling cells'
       opaque backgrounds, or the line is sliced off at the first cell border.
       Gaps render before shifts in the cell, so blocks still paint on top. */
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    background: linear-gradient(var(--color-text-muted), var(--color-text-muted)) center / calc(100% - 8px) 1px no-repeat;
}

.rest-gap::before,
.rest-gap::after {
    content: "";
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    border: 4px solid transparent;
}

.rest-gap::before {
    left: 0;
    border-left-width: 0;
    border-right-color: var(--color-text-muted);
}

.rest-gap::after {
    right: 0;
    border-right-width: 0;
    border-left-color: var(--color-text-muted);
}

.rest-gap-label {
    position: relative;
    padding: 0 3px;
    background: var(--color-bg);
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
    white-space: nowrap;
}

/* ------------------------------------------------------------
   Worked-hours run brackets (?worked=true): ⌐— 16hrs —¬ spanning
   each run of shifts not separated by a 24h break. Same measure-
   line family as .rest-gap, but hugging the top of the row with
   downward end ticks, because the span it measures is the shifts
   themselves — it must read as "over" the blocks, not between
   them. Rendered after the blocks (z-index 2) so the line stays
   visible crossing them; pointer-events none keeps every block
   gesture working underneath.
   ------------------------------------------------------------ */
.worked-run {
    position: absolute;
    top: 0;
    height: 12px;
    z-index: 2;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    pointer-events: none;
    background: linear-gradient(var(--color-text-muted), var(--color-text-muted)) center 1px / 100% 1px no-repeat;
}

.worked-run::before,
.worked-run::after {
    content: "";
    position: absolute;
    top: 1px;
    width: 1px;
    height: 6px;
    background: var(--color-text-muted);
}

.worked-run::before {
    left: 0;
}

.worked-run::after {
    right: 0;
}

.worked-run-label {
    position: relative;
    padding: 0 3px;
    background: var(--color-bg);
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
    line-height: 1.1;
    white-space: nowrap;
}

/* A run of one shift shows only the hours note — no line, no end ticks. */
.worked-run-single {
    background: none;
}

.worked-run-single::before,
.worked-run-single::after {
    display: none;
}

/* Clicking a multi-day run's label focuses that run (thicker line, everything
   else dimmed). A None-mode gesture like the job highlight: the label opts back
   into pointer events only while the grid is in None mode, so write-mode cell
   clicks under it keep working. */
.roster-grid[data-mode="none"] .worked-run-clickable {
    pointer-events: auto;
    cursor: pointer;
}

/* Any multi-shift run's label opts back into pointer events (all modes) so it can
   surface the run's breakdown in the info strip on hover — a read-only gesture, so
   it needs no None-mode gate like click-to-focus above. */
.worked-run-hoverable {
    pointer-events: auto;
}

.worked-run-active {
    background-size: 100% 2px;
}

.worked-run-active::before,
.worked-run-active::after {
    width: 2px;
}

/* ------------------------------------------------------------
   Alt/Option-drag copy ghost (2026-08-05). A copy draws its own
   drag image — a clone of the block with a COPYING label above
   it — because the browser's native one is a fixed snapshot we
   can neither label nor animate away when Alt is released.
   Above the panel (z-index 95) so it reads as floating over the
   whole app; below the toast host (110) and the drag tip (120),
   both of which carry words the planner needs to read.
   ------------------------------------------------------------ */
.copy-ghost {
    position: fixed;
    z-index: 108;
    pointer-events: none;
    opacity: 0.85;
    transition: transform 220ms ease-out, opacity 220ms ease-out;
}

/* The multi-drag's constellation ghost (2026-08-06): clones of EVERY selected
   block at their true offsets from the anchor, following the cursor as one —
   the native drag image can only show the grabbed block. Same layer and
   translucency as the copy ghost; under the drag tip (120) so reasons stay
   readable over it. */
.multi-ghost {
    position: fixed;
    z-index: 108;
    pointer-events: none;
    opacity: 0.85;
}

/* Deliberately the worked-hours run label's chrome (see .worked-run-label):
   small muted type on a background-coloured plate. Sits above the block, out
   of the way of the job text inside it. */
.copy-ghost-label {
    position: absolute;
    bottom: 100%;
    left: 0;
    padding: 0 3px;
    background: var(--color-bg);
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
    line-height: 1.1;
    white-space: nowrap;
}

/* The copy did not happen — Alt released mid-drag, or the write refused.
   Drifts up as it fades, so the block reads as evaporating rather than
   being put down anywhere. */
.copy-ghost-leaving {
    transform: translateY(-10px);
    opacity: 0;
}

/* The landing cell: an inset ring, recoloured by the cell's verdict. */
.roster-cell.drag-over {
    outline: 2px solid var(--color-accent);
    outline-offset: -2px;
}

.roster-cell.drag-over.avail-amber {
    outline-color: var(--color-warning);
}

.roster-cell.drag-over.avail-red {
    outline-color: var(--color-breach);
}

/* The probe entries the steps drag: a gentle accent pulse says "start here".
   Decorative attention-draw only (the pink/orange colour and "Drag Me" name
   carry the meaning), so reduced motion switches it off. */
.roster-entry.rule-test-probe {
    animation: rule-test-probe-pulse 1.4s ease-in-out infinite;
}

@keyframes rule-test-probe-pulse {
    0%, 100% { outline: 2px solid transparent; outline-offset: 1px; }
    50% { outline: 2px solid var(--color-accent); outline-offset: 1px; }
}

@media (prefers-reduced-motion: reduce) {
    .roster-entry.rule-test-probe {
        animation: none;
    }
}

/* (The search navigate's one-shot .found-pulse outline lived here until
   2026-08-10. Robert removed it: the OPACITY is the cue — the selected entry
   holds full strength while everything else dims, a state that stays put
   instead of a flash you can miss. scrollToEntry now only scrolls.) */

/* ------------------------------------------------------------
   Marquee multi-select (2026-08-05, docs/specs/marquee-multi-select.md):
   drag a rectangle in empty grid space to select the entries it touches.
   The rectangle and count badge are fixed-position and JS-owned
   (marquee-select.js). The dim rides the grid: marquee-live while the band
   is dragging (JS-owned class), marquee-active while a committed selection
   exists (rendered by Home). Selected blocks keep full opacity AND carry an
   accent outline — never colour (or dimness) alone. */
.marquee-rect {
    position: fixed;
    z-index: 110;
    border: 1px solid var(--color-accent);
    background: color-mix(in srgb, var(--color-accent) 12%, transparent);
    pointer-events: none;
}

/* Count badge by the cursor — the drag-tip's chrome. */
.marquee-count {
    position: fixed;
    z-index: 120;
    padding: 2px 8px;
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
    color: var(--color-text);
    font-size: var(--font-size-small);
    pointer-events: none;
}

.roster-grid.marquee-live {
    user-select: none;
}

.roster-grid.marquee-live .roster-cell {
    cursor: crosshair;
}

/* Same dim as the job highlight's .dimmed — one visual language for
   "not what you're focused on". */
.roster-grid.marquee-live .roster-entry:not(.marquee-selected),
.roster-grid.marquee-active .roster-entry:not(.marquee-selected) {
    opacity: 0.22;
}

.roster-entry.marquee-selected {
    outline: 2px solid var(--color-accent);
    outline-offset: 1px;
    z-index: 2;   /* the outline must paint over neighbouring cells' backgrounds */
}

/* EMPTY cells select too (2026-08-07), so a block of days with no work on it can
   be marked with a colour in one gesture. Same accent outline as a selected
   block, pulled inside the cell (outline-offset would overlap the neighbours) —
   and z-index for the same reason blocks need it. Cells carry no dim: an empty
   cell has nothing in it to fade, and dimming the grid's own background would
   read as a rendering fault. */
.roster-cell.marquee-cell-selected {
    outline: 2px solid var(--color-accent);
    outline-offset: -2px;
    z-index: 2;
}

/* ------------------------------------------------------------
   Batch failure paint (marquee spec slice 5, ruling 7): a rejected
   multi-drop paints its failing LANDING cells with the avail-red stripe
   vocabulary (pattern + colour, never colour alone; the reason rides the
   cell title) behind a flash — two quick flashes then steady full opacity,
   a light turning on, not a gentle pulse. The flash count is the
   --batch-fail-flashes token below (set it to 1 for a single flash); the
   steady state is the element's natural style, so the animation simply
   runs its iterations and stops. Lifetime is the rejection toast's,
   exactly — Home clears the paint when the toast goes — or any new drag
   or marquee start, whichever comes first. Conflicting existing entries
   flash a breach outline on the same lifecycle.
   ------------------------------------------------------------ */
:root {
    --batch-fail-flashes: 2;
}

.roster-cell.batch-fail {
    background-color: color-mix(in srgb, var(--color-breach) 16%, var(--color-bg));
    background-image: repeating-linear-gradient(45deg,
        transparent 0 4px,
        color-mix(in srgb, var(--color-breach) 45%, transparent) 4px 7px);
    animation: batch-fail-flick 180ms linear var(--batch-fail-flashes);
}

.roster-entry.batch-fail-conflict {
    outline: 2px solid var(--color-breach);
    outline-offset: 1px;
    z-index: 2;   /* like .marquee-selected: the outline must clear neighbouring cells */
    animation: batch-fail-flick 180ms linear var(--batch-fail-flashes);
}

/* The hover cross-highlight outranks .batch-fail on the cell left under the
   cursor after the drop, so the same two-part treatment applies as to the
   avail-* rules above: the highlight can no longer wipe the stripe image (it
   sets background-color, not the shorthand), and this rule gives the stripe its
   own base colour back with the tint blended in. The :not() chain is what puts
   it above the hover rule — without it these sit at (0,4,0) under a (0,5,0). */
.roster-grid .roster-cell.hl-row.batch-fail:not(.cell-coloured):not(.placeholder-row),
.roster-grid .roster-cell.hl-col.batch-fail:not(.cell-coloured):not(.placeholder-row),
.roster-grid .roster-cell.hl-cell.batch-fail:not(.cell-coloured):not(.placeholder-row) {
    background-color: color-mix(in srgb, var(--color-breach) 16%,
        color-mix(in srgb, var(--hl-colour) 14%, var(--color-bg)));
    background-image: repeating-linear-gradient(45deg,
        transparent 0 4px,
        color-mix(in srgb, var(--color-breach) 45%, transparent) 4px 7px);
}

/* One flash = visible, then dark; N iterations then the natural (steady,
   fully visible) style. The duplicated stops make each toggle a hard edge —
   the light-switch look — rather than a crossfade. */
@keyframes batch-fail-flick {
    0%, 49% { opacity: 1; }
    50%, 100% { opacity: 0; }
}

/* Reduced motion: steady red immediately. The red is the information, the
   flash is only the attention cue — and rapid flashing is the one animation
   category with photosensitivity stakes. */
@media (prefers-reduced-motion: reduce) {
    .roster-cell.batch-fail,
    .roster-entry.batch-fail-conflict {
        animation: none;
    }
}

/* Availability reason, following the cursor over amber/red cells (JS-positioned;
   native tooltips don't show during a drag). */
.drag-tip {
    position: fixed;
    z-index: 120;
    display: none;
    max-width: 280px;
    padding: 4px 8px;
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
    color: var(--color-text);
    font-size: var(--font-size-small);
    line-height: 1.4;
    white-space: pre-line;
    pointer-events: none;
}

/* ------------------------------------------------------------
   Toast (hand-rolled): ONE at a time, centred just under the top
   of the screen, so it overlays panel chrome and NEVER the grid
   — the most important rule here (a single toast fits inside the
   ~78px panel band; a downward stack is disallowed in the
   service). Deliberately big: not to be missed. Tone = colour
   AND glyph/wording, never colour alone. The full-width host
   must not eat clicks around the toast, hence the pointer pair.
   ------------------------------------------------------------ */
.toast-host {
    position: fixed;
    top: 8px;
    left: 0;
    right: 0;
    z-index: 110;
    display: flex;
    flex-direction: column;
    align-items: center;
    pointer-events: none;
}

.toast {
    pointer-events: auto;
    display: flex;
    align-items: center;
    gap: 12px;
    min-width: 380px;
    max-width: 680px;
    padding: 12px 20px;
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
    font-size: 15px;
    cursor: pointer;
    animation: toast-in 120ms ease-out;
}

/* Children are pointer-transparent so hover/leave fire once on the toast itself,
   not on every icon/text-span boundary crossing (Blazor has no mouseenter/leave). */
.toast > * {
    pointer-events: none;
}

.toast .toast-icon {
    font-size: 17px;
    font-weight: 700;
}

/* Opts back into pointer events (siblings are transparent): its own hover/title
   work, and its click stops propagation so it can't dismiss the toast. */
.toast .toast-copy {
    pointer-events: auto;
    margin-left: auto;
    background: transparent;
    border: none;
    color: var(--color-text-muted);
    font: inherit;
    font-size: 12px;
    white-space: nowrap;
    padding: 4px 8px;
    cursor: pointer;
}

.toast .toast-copy:hover {
    background: var(--color-field);
    color: var(--color-text);
}

.toast .toast-copy:focus-visible {
    outline: 1px solid var(--color-accent);
    outline-offset: -1px;
}

/* Rejection toasts list the broken rules as a two-column table (rule name |
   what it objects to) under the headline — one row per rule, so multiple broken
   rules are visibly separate rules. The taller toast top-aligns its icon and
   copy button against the headline line, runs wider so details don't wrap, and
   the rows sit a step below the 15px headline. That sizing keeps a short
   rule list inside the panel band (the never-over-the-grid rule); a longer
   list, each wider-reading note adding a line, is held there by the
   max-height on the rule toast below. */
.toast-message .toast-rules {
    display: grid;
    grid-template-columns: fit-content(240px) 1fr;
    gap: 2px 12px;
    margin-top: 6px;
    font-size: 13px;
    text-align: left;
}

.toast-rule-name {
    font-weight: 700;
}

/* The same note under a rejection toast's rule row (a Hard refusal opens no dialog, so
   the toast is where that user reads the exact number): a line of its own under the
   detail, in the muted voice. */
.toast-rule-note {
    display: block;
    margin-top: 2px;
    font-size: 12px;
    color: var(--color-text-muted);
}

.toast:has(.toast-rules) {
    align-items: flex-start;
    max-width: 900px;
    padding: 10px 16px;
    /* Never over the grid however many rules and notes it lists (the slice-4
       review): the host sits 8px down and the grid starts about 117px down
       (the 36px panel row and the 80px info strip), so the toast stops at
       108px, border-box, and scrolls inside itself. Its children are
       pointer-transparent, so the wheel reaches the toast. */
    max-height: 108px;
    overflow-y: auto;
    overflow-x: hidden;
}

.toast:has(.toast-rules) .toast-icon,
.toast:has(.toast-rules) .toast-copy {
    margin-top: 1px;
}

.toast-error {
    border-left: 4px solid var(--color-breach);
}

.toast-error .toast-icon {
    color: var(--color-breach);
}

.toast-undo {
    border-left: 4px solid var(--color-accent);
}

.toast-undo .toast-icon {
    color: var(--color-accent);
}

/* Success (e.g. "Roster emailed to …") — its own class even though it shares
   undo's accent colouring today (two different things never share a class
   name); the ✓ glyph is what tells the two apart. */
.toast-success {
    border-left: 4px solid var(--color-accent);
}

.toast-success .toast-icon {
    color: var(--color-accent);
}

@keyframes toast-in {
    from { opacity: 0; transform: translateY(-6px); }
    to { opacity: 1; transform: none; }
}

/* ---- Consent dialog (phase 6: acknowledge ambers / override exceedable breaches).
   One machinery, two tones — the accent bar and glyphs carry the severity
   (colour + glyph, never colour alone). */

.consent-modal {
    max-width: 480px;
    /* A long batch's grouped reasons scroll inside the dialog rather than
       growing it past the viewport; single-gesture dialogs never reach this. */
    max-height: 82vh;
    overflow-y: auto;
}

/* The missing-tag heading names the tag group a planner has to go and find, so
   that name escapes the heading's chrome casing and shows exactly as the
   organisation authored it (Robert, 2026-08-09 — the point is seeing what is
   missing at a glance). Bold against the heading's 600 carries it the rest of
   the way. */
.consent-missing-name {
    text-transform: none;
    letter-spacing: 0;
    font-weight: 700;
}

.consent-reasons {
    list-style: none;
    margin: 0 0 12px;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

/* Batch gestures group the reasons under the entry they belong to (one combined
   dialog, never a chain) — a small bold heading per entry. */
.consent-group-title {
    margin: 10px 0 4px;
    font-weight: 700;
}

.consent-reasons li {
    display: flex;
    gap: 8px;
    align-items: baseline;
    padding: 4px 6px;
    background: var(--color-field);
}

.consent-reasons li.consent-breach {
    border-left: 3px solid var(--color-breach);
}

.consent-reasons li.consent-warning {
    border-left: 3px solid var(--color-warning);
}

.consent-glyph {
    flex: none;
    font-weight: 700;
}

/* A reason's sentence and, under it, the one-sentence note shown when a second, wider
   reading of the history changed its figure (2026-09-15, slice 4): the note takes the
   modal's muted voice through .muted, one step smaller, and is words alone. */
.consent-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.consent-note {
    font-size: 12px;
}

li.consent-breach .consent-glyph {
    color: var(--color-breach);
}

li.consent-warning .consent-glyph {
    color: var(--color-warning);
}

.consent-reason-input {
    width: 100%;
    margin-bottom: 4px;
}

/* The amber counterpart of btn-danger: confirming a tight-but-legal placement. */
.btn-acknowledge {
    width: 100%;
    height: 28px;
    background: var(--color-warning);
    color: #ffffff;
    border: 1px solid rgba(0, 0, 0, 0.35);
    border-radius: 0;
    font-family: var(--font-family);
    font-size: var(--font-size-base);
    font-weight: 600;
    cursor: pointer;
    margin-top: 8px;
}

.btn-acknowledge:hover {
    filter: brightness(1.1);
}

/* Change-list consent markers — the audit trail's visible face. */
.change-consent {
    margin-left: 8px;
    font-weight: 600;
    white-space: nowrap;
}

.change-consent.consent-breach {
    color: var(--color-breach);
}

.change-consent.consent-warning {
    color: var(--color-warning);
}

/* ---- Rule builder (phase 6) ---- */

.rules-page {
    max-width: 1200px;
}

.rules-table td {
    vertical-align: top;
}

.rule-source {
    display: block;
    font-size: 11px;
}

.rule-params {
    max-width: 260px;
}

.rule-inactive {
    opacity: 0.55;
}

/* Dimmed at rest, full strength under the cursor. The hover restore is what
   makes tooltips on expired/voided rows work at all: opacity < 1 turns the
   row into a stacking context, trapping the data-tip ::after dimmed and
   BEHIND the following rows — at opacity 1 the context disappears and the
   tip paints over the table. Doubles as the row-click affordance now rows
   open the modal. */
.rule-inactive:hover {
    opacity: 1;
}

.tier-badge {
    display: inline-block;
    padding: 1px 6px;
    font-size: var(--font-size-small);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #ffffff;
}

.tier-badge.tier-hard {
    background: var(--color-breach);
}

.tier-badge.tier-exceedable {
    background: var(--color-warning);
}

.tier-badge.tier-advisory {
    background: var(--color-accent);
}

.rule-editor {
    margin: 14px 0;
    padding: 12px;
    border: 1px solid var(--color-border-strong);
    background: var(--color-surface);
}

/* btn-primary is width:100% for modal footers; the standalone New rule button
   sizes to its label. Spacing comes from its .table-toolbar row. */
.new-rule-btn {
    width: auto;
    padding: 0 18px;
    margin: 0;
}

/* On /rules the editor presents as a modal (2026-07-19): wider than the stock
   .modal. The modal itself must stay overflow-visible — a scrolling modal
   clips its own help pops and data-tips at its edges (no z-index escapes an
   overflow clip). So the BACKDROP scrolls instead: pops overhang the modal
   onto the backdrop, and an editor taller than the viewport scrolls as a
   whole. margin:auto (not the backdrop's centring) keeps the modal centred
   when it fits and its top reachable when it doesn't. */
.modal-backdrop-scroll {
    overflow-y: auto;
    overflow-x: hidden;
    padding: 24px 12px;
}

.rule-editor-modal {
    margin: auto;
    width: min(1150px, 94vw);
}

.rule-editor h2,
.rules-section {
    font-size: 13px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin: 18px 0 8px;
}

.rule-editor h2 {
    margin-top: 0;
}

.rule-editor-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 8px 14px;
    margin-bottom: 10px;
}

.rule-editor-grid label {
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-size: var(--font-size-small);
    color: var(--color-text-muted);
}

.rule-param-input {
    width: 120px;
}

.rule-explainer {
    max-width: 1100px;
    margin: 6px 0 10px;
}

/* TEMPORARY SIGNAGE (phase 4, 2026-07-30) — delete this whole block together
   with Rules/BehaviourChangeNotices.cs and its two call sites. The amber is a
   reinforcement only: the uppercase lead and the glyph carry the meaning, so
   the notice still reads as a notice in monochrome or to a colour-blind user. */
/* The summary column is capped at 260px, which would fold a two-sentence
   notice into a very tall thin ribbon. Only the cells that carry one widen. */
.rule-params:has(.rule-behaviour-change) {
    max-width: 560px;
}

.rule-behaviour-change {
    max-width: 1100px;
    margin: 6px 0 0;
    padding: 5px 8px;
    border-left: 3px solid var(--color-warning);
    background: var(--color-field);
    color: var(--color-text);
    /* Base size, deliberately: this is text a planner must read, and
       --font-size-small is chrome (feedback 2026-07-22). */
    line-height: 1.45;
}

.rule-behaviour-change .bc-glyph {
    color: var(--color-warning);
    font-weight: 700;
}

/* A rule whose scoring module this organisation does not run (2026-09-11, Robert's
   ruling). Its own class and its own glyph class, NOT the .rule-behaviour-change block
   above: that one is temporary signage marked for deletion with its own C# file, and two
   different things never share a class name. Same look, deliberately, because it is the
   same kind of statement — a note about this rule, under its summary. The amber is
   reinforcement only; the glyph and the sentence carry the meaning. */
.rule-params:has(.rule-cannot-apply) {
    /* The summary column is capped at 260px, which would fold this into a tall thin
       ribbon. Only the cells carrying one widen. */
    max-width: 560px;
}

.rule-cannot-apply {
    max-width: 1100px;
    margin: 6px 0 0;
    padding: 5px 8px;
    border-left: 3px solid var(--color-warning);
    background: var(--color-field);
    color: var(--color-text);
    /* Base size, deliberately: text a user must read, never --font-size-small,
       which is chrome (feedback 2026-07-22). */
    line-height: 1.45;
}

.rule-cannot-apply .ca-glyph {
    color: var(--color-warning);
    font-weight: 700;
}

/* The compliance report's version of the same statement, deliberately the same look: one
   sentence shared by two surfaces should not read as two different kinds of warning. It
   does NOT take .notice, which is the breach red on this page — a rule that cannot apply
   is a caveat about what the report covers, not a rule-break. */
.compliance-cannot-apply {
    max-width: 1100px;
    margin: 8px 0;
    padding: 5px 8px;
    border-left: 3px solid var(--color-warning);
    background: var(--color-field);
    color: var(--color-text);
    line-height: 1.45;
}

.compliance-cannot-apply .ca-glyph {
    color: var(--color-warning);
    font-weight: 700;
}

.rule-disapply {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 8px 0;
}

.rule-editor-actions {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 10px;
    flex-wrap: wrap;
}

.rule-editor-actions .rule-action {
    width: auto;
    padding: 0 14px;
}

/* Under the modal title: the unsaved-new-version note (clone mode). */
.rule-created-line {
    margin: -4px 0 10px;
}

/* Bottom-right of the rule modal (2026-07-28): the rule's provenance —
   created / last modified / voided, who and when, comma-separated — in one
   italic row below the actions, so nobody digs the change history for it. */
.rule-audit-line {
    text-align: right;
    margin: 10px 0 0;
    font-style: italic;
}

/* The rule modal's form (2026-07-28): Bootstrap-grid columns in three scan
   lines — what the rule is, what it's called, when it applies — with the
   validity row's right half reserved for the handover gestures (Clone /
   the closing-off hint). Labels read at BASE size (never shrink text users
   read — feedback 2026-07-22); controls fill their columns so the tracks
   stay aligned row to row. */
/* Scoped to the FIELD rows: the whole modal body lives inside .rule-form now
   (2026-07-28 — every block is a Bootstrap row), and the disapply checkbox
   label must keep its own inline layout. */
.rule-form .rule-form-row label {
    display: flex;
    flex-direction: column;
    gap: 3px;
    color: var(--color-text-muted);
}

/* Titled hairline section heads — "Limits" over the parameter grid, "In force"
   over the dates (2026-07-28, softening the hr-boxed validity section): the
   dates still read as their own concern without full-width rules boxing them
   in. Heading type is the table-header chrome treatment. The negative bottom
   margin nets against the next row's 12px column gutter, so a head sits 6px
   from its own section and 18px from the section above. */
.rule-form .rule-section-head {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 18px 0 -6px;
    font-size: var(--font-size-small);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
}

.rule-form .rule-section-head::after {
    content: "";
    flex: 1;
    border-top: 1px solid var(--color-border);
}

/* Bold labels (feedback 2026-07-28) — the label/value pair reads apart at a
   glance, disabled fields included. */
.rule-form .label-line {
    font-weight: 600;
}

.rule-form .select {
    width: 100%;
    box-sizing: border-box;
    /* Bottom-align controls across a row: a label that wraps to two lines
       (some parameter labels do) must not leave its input floating at a
       different height from its neighbours'. */
    margin-top: auto;
}

/* One vertical rhythm for the whole form: rows never carry Bootstrap's
   negative first-line compensation (the earlier 10px row-to-row margin sat
   ON TOP of the g-3 columns' 16px gutter and doubled the spacing between
   every pair of rows) — the columns' 12px top gutter is the one source of
   vertical space. */
.rule-form .row {
    --bs-gutter-y: 12px;
    margin-top: 0;
}

/* Inside the rowed form that rhythm owns vertical space — these elements'
   standalone-page margins (and p's browser default on .notice) would stack
   on top of it. */
.rule-form .rule-explainer,
.rule-form .rule-timeline,
.rule-form .rule-locked-note,
.rule-form .rule-disapply,
.rule-form .notice,
.rule-form .rule-audit-line {
    margin: 0;
}

/* With the divider above the buttons gone, the extra top margin is what
   separates the actions footer from the dates. */
.rule-form .rule-editor-actions {
    margin-top: 20px;
}

/* The slot beside Until: bottom-aligned so the Clone button (or the hint)
   sits level with the date input it belongs to. */
.rule-until-side {
    display: flex;
    align-items: flex-end;
    gap: 8px;
}

.rule-until-side .btn {
    flex-shrink: 0;
}

.rule-until-hint {
    margin: 0 0 3px;
}

/* The edit-mode immutability explainer above the (disabled) value fields. */
.rule-locked-note {
    max-width: 900px;
    margin: 8px 0;
}

.rules-audit {
    max-height: 30vh;
}

/* MBSearchableTable (Components/Shared) — the house searchable/paged table:
   filter row under the headers (one search per column, run against the store —
   a DB query in the SQL era), Previous/Next paging, optional export button.
   The mb-* classes are the component's own; column widths stay page-owned
   (the history-* vocabulary below, passed in via TableClass/CssClass). */

.mb-table .mb-filter-row td {
    padding: 3px 6px;
    background: var(--color-surface);
}

.mb-table .mb-filter-row .select {
    width: 100%;
    box-sizing: border-box;
}

/* THE ONE EXEMPTION from the outlined-and-white house field look (Robert,
   2026-08-04): a filter row is a band of fields sitting directly under the
   header row, and outlining every one of them draws a second grid of boxes over
   the table's own. Quiet wells here instead — the row reads as one strip, and
   the table keeps the ruled lines. The hover border is the affordance a resting
   outline would otherwise give; focus keeps the app-wide accent ring.
   background-COLOR, never the shorthand: this selector is (0,4,0) and outranks
   the enabled-select chevron rule (.app-shell select.select:not(:disabled), at
   (0,3,1)), so a shorthand here silently strips the chevron off every dropdown
   filter — which it did, until a drive caught it. */
.mb-table .mb-filter-row .select:not(:disabled) {
    background-color: var(--color-field);
    border-color: transparent;
}

.mb-table .mb-filter-row .select:not(:disabled):hover {
    border-color: var(--color-border-strong);
}

.mb-table td {
    vertical-align: top;
}

/* Drag-to-reorder rows (2026-08-07, MBSearchableTable's opt-in IsRowDraggable —
   the colour palette alone so far). The insertion line is an INSET shadow on the
   row's cells rather than the grid's absolutely-positioned bar: a <tr> is a poor
   containing block, and an inset shadow costs no layout, so the row under the
   cursor can't nudge everything below it while you aim. Above or below depending
   on which way the drag is going, so the last position is reachable. */
.mb-table tr.mb-row-draggable {
    cursor: grab;
}

.mb-table tr.mb-row-draggable:active {
    cursor: grabbing;
}

.mb-table tr.mb-drop-above > td {
    box-shadow: inset 0 3px 0 var(--color-accent);
}

.mb-table tr.mb-drop-below > td {
    box-shadow: inset 0 -3px 0 var(--color-accent);
}

/* The handle column: as narrow as the glyph, and the glyph is the whole cue that
   the row moves (the row itself is what drags, like the roster's employee rows). */
.mb-table td.mb-drag-cell {
    width: 1%;
    padding-right: 4px;
    white-space: nowrap;
}

/* The rules table's scope footnote — sits under the table, so it needs its own
   clearance rather than the paragraph rhythm the intro rows use. */
.rules-scope-note {
    margin-top: 12px;
}

/* The tier key (Components/Shared/TierKeyModal) runs wider than the 420px
   confirm default: each row is a fixed-width badge plus a sentence, and at
   420px every one of the three wrapped to three lines. */
.modal-tier-key {
    max-width: 620px;
}

/* "Why doesn't one setting decide what counts as a shift?" — several
   paragraphs of prose, which read badly in the 420px default (2026-08-01). */
.modal-why-grouping {
    max-width: 620px;
}

/* The /error-logs detail modal (2026-08-15): stack traces are wide, and the
   pre blocks scroll rather than stretch the dialog off screen. */
.error-detail-modal {
    max-width: 900px;
}

.error-stack {
    white-space: pre-wrap;
    word-break: break-word;
    font-family: Consolas, Menlo, monospace;
    font-size: 11px;
    line-height: 1.4;
    background: var(--color-field);
    border: 1px solid var(--color-border);
    padding: 8px 10px;
    margin: 4px 0 12px 0;
    max-height: 300px;
    overflow-y: auto;
}

/* MBTableColumn.HeaderContent — chrome sitting beside a header's text (the
   rules table's tier "Key" button). Accent, and it must not inherit the
   header's uppercase/letter-spacing, or "Key" reads as another column name. */
.tier-key-btn {
    margin-left: 6px;
    color: var(--color-accent);
    text-transform: none;
    letter-spacing: normal;
    text-decoration: underline;
}

/* MBTableColumn.IsSortable (2026-08-27): the whole header is the sort
   gesture. The button inherits the th's own chrome so a sortable header reads
   exactly like its neighbours; the arrow glyph carries the state (↕ sortable,
   ▲/▼ sorted — a glyph, never colour alone). The focus ring is mandatory per
   the house borderless-controls rule. */
.mb-sort-btn {
    background: transparent;
    border: none;
    padding: 0;
    font: inherit;
    color: inherit;
    text-transform: inherit;
    letter-spacing: inherit;
    cursor: pointer;
}

.mb-sort-btn:hover {
    color: var(--color-text);
}

.mb-sort-btn:focus-visible {
    outline: 1px solid var(--color-accent);
    outline-offset: 1px;
}

/* Faint until the column is actually sorted — the ↕ is an affordance, the
   ▲/▼ is state and shows at full strength. */
.mb-sort-arrow {
    opacity: 0.55;
}

.mb-sort-arrow.mb-sorted {
    opacity: 1;
}

.mb-pager {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 8px 0;
}

/* Right edge of the pager row = right edge of the full-width table above it. */
.mb-pager .mb-export {
    margin-left: auto;
}

.mb-empty {
    padding: 14px 8px;
}

/* History pages (rule changes and roster changes): their column widths and
   row states on top of the MBSearchableTable chrome. */

/* Wider than the 980px rules-page cap (2026-07-21): with the Organisation
   column showing (sys-admins, multi-org viewers) the free-text Details column
   was left cramped. Fixed-width columns above absorb nothing, so the extra
   width all goes to Details/Organisation. */
.history-page {
    max-width: 1400px;
}

.history-table .history-when {
    width: 130px;
    white-space: nowrap;
}

.history-table .history-who {
    width: 150px;
}

/* The error log's occurrence count (2026-08-17). Narrow and right-aligned, because
   it is a number read against the numbers above and below it — and it carries no
   filter, so the width is the header word plus a little. */
.history-table .error-times {
    width: 58px;
    text-align: right;
    white-space: nowrap;
}

.history-table .history-action {
    width: 150px;
}

.history-table .history-rule {
    width: 240px;
}

.history-table .history-emp,
.history-table .history-job {
    width: 190px;
}

/* Undone changes stay on record, struck through (the change list's .undone). */
.history-table tr.history-undone td {
    opacity: 0.55;
    text-decoration: line-through;
}

.history-change-line + .history-change-line {
    margin-top: 2px;
}

/* The single searchable rules table (2026-07-28, replacing the per-group
   tables): the originating group reads from the leading Source column, and
   the page-owned "Rule state" dropdown sits above the table. The rules tab is
   the one .table-toolbar with a left-hand control, so the filter pushes itself
   left and leaves the button where it sits on every other tab. */
.rules-state-filter {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    margin-right: auto;
}

/* The Employees tab's limit field (2026-09-06): "Employees allowed", its number,
   Apply and the used count sit left in the toolbar, the rules-state-filter
   shape, so Import and + New employee stay where they sit on every other tab.
   Nothing in the row wraps (Robert, same day: the first cut let the label and
   the used count fold onto two and three lines beside a full-width Apply, and
   read as cramped): every text is nowrap, the gaps are wider than the
   toolbar's own, and Apply sizes to its word like the create buttons. */
.employee-limit {
    display: inline-flex;
    align-items: center;
    gap: 12px;
    margin-right: auto;
    white-space: nowrap;
}

.employee-limit-label {
    font-weight: 600;
}

.employee-limit-input {
    width: 70px;   /* the unassigned-row box's width, shared as a RULE and not a class name */
}

.btn-primary.employee-limit-apply {
    width: auto;
    padding: 0 14px;
    margin: 0;
}

/* A primary button that is DISABLED (the Employees tab's Import and + New
   employee at the employee limit, 2026-09-06) has to look it: the accent fill
   fades and the cursor says no, so the greying Robert asked for is visible
   rather than a bare attribute. The tooltip carries the reason. */
.btn-primary:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

.employee-limit-used,
.employee-limit-saved {
    margin-left: 6px;
}

/* The compliance tab's report controls (road-to-live-data item 7): date range
   and the Show dropdown sit left in the toolbar, the rules-state-filter shape
   — there is no create button on a report, so nothing sits right. */
.compliance-filters {
    display: inline-flex;
    align-items: center;
    gap: 14px;
    margin-right: auto;
}

.compliance-filters .panel-label {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

/* The checkbox reads as one control with its words: the box first, no field
   chrome (it is not a text field, so the outlined-and-white rule doesn't
   apply), and the whole label clickable. */
.compliance-filters .compliance-check {
    cursor: pointer;
    gap: 6px;
}

.compliance-filters .compliance-check input {
    cursor: pointer;
    margin: 0;
}

/* The report pages' toolbar controls (2026-08-27, first used by Hours by
   tag): the compliance-filters shape under its own name — the two surfaces
   look the same on purpose, and the never-share-a-class-name rule keeps a
   future compliance tweak from silently restyling every report. */
.report-filters {
    display: inline-flex;
    align-items: center;
    gap: 14px;
    margin-right: auto;
}

.report-filters .panel-label {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

.report-filters .report-check {
    cursor: pointer;
    gap: 6px;
}

.report-filters .report-check input {
    cursor: pointer;
    margin: 0;
}

/* Hours are numbers: right-aligned so magnitudes line up down the column.
   The header cell shares the class, so its sort button sits over its numbers. */
.report-table .report-hours {
    text-align: right;
    width: 130px;
}

/* The Hours by job report's identifier column: narrow, never wrapping, so the
   name column takes the slack (the Jobs tab's own shape). */
.report-table .report-job-number {
    width: 110px;
    white-space: nowrap;
}

/* The severity cell stacks its marker lines; the chips carry their own colour
   (glyph + words, never colour alone). */
.compliance-table .compliance-severity .change-consent {
    margin-left: 0;
}

/* The roster link's own column (2026-08-16), last in the table: this report's
   rows are deliberately NOT clickable — unlike every other table here — so the
   way through to the roster is a visible link instead. It never wraps, and it
   reads like the app's other links (accent, underline on hover). */
.compliance-table .compliance-roster {
    width: 1%;
    white-space: nowrap;
}

.compliance-roster-link {
    color: var(--color-accent);
    text-decoration: none;
}

.compliance-roster-link:hover {
    text-decoration: underline;
}

/* A button that reads exactly like the sibling sections' in-paragraph links
   (catalog, history) — for affordances that open something in-page rather
   than navigate. */
.link-button {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    cursor: pointer;
}

.rules-table .rule-source-name {
    width: 170px;
}

/* Clickable listing rows (rule-group list, a group's rules). */
.row-link {
    cursor: pointer;
}

.row-link:hover td {
    background: var(--color-field);
}

/* The catalog page (/rule-groups). */
.rule-group-title {
    display: flex;
    align-items: baseline;
    gap: 10px;
}

.rule-group-title-action {
    font-size: var(--font-size-small);
    text-transform: none;
    letter-spacing: normal;
}

.rule-group-stamp {
    margin: 0 0 6px;
    font-size: var(--font-size-small);
}

/* The centred card the chrome-less pages sit on: the sign-in and password
   pages, and /Error. All of them render static SSR under EmptyLayout, so there
   is no action panel and no theme class above them — which is the point for the
   error page, whose whole job is to work when the rest of the app cannot.
   Two class names rather than one shared name, per the house rule that two
   different things never share a CSS class name; they share the RULE instead. */
.login-shell,
.error-shell {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--color-bg);
}

.login-card,
.error-card {
    width: 340px;
    padding: 24px;
    border: 1px solid var(--color-border-strong);
    background: var(--color-surface);
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* The logo heads the card (2026-10-06) at the design's larger size, the one its
   canvas shows on the website hero: a 32px symbol, a 12px gap and the name at
   18px, in the logo's teal. These cards never wear the dark theme. */
.login-card h1,
.error-card h1 {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 0 0 6px;
    color: var(--logo-ink);
}

.login-card h1 .logo-symbol,
.error-card h1 .logo-symbol {
    width: 32px;
    height: 32px;
}

.login-card h1 .logo-name,
.error-card h1 .logo-name {
    font-size: 18px;
}

/* Wider than the sign-in card because it holds sentences rather than fields,
   and capped against the viewport so the words still fit on a phone. */
.error-card {
    width: 420px;
    max-width: calc(100vw - 32px);
}

.error-card p {
    margin: 0;
}

/* `.muted` and `.notice` are otherwise only defined under .panel-info / .admin /
   .modal / .rule-form, so on these chrome-less cards BOTH classes matched
   nothing at all: the sign-in subtitle rendered at full strength, and the
   sign-in refusal, the no-accounts-yet prompt and the reset-confirm error
   rendered as plain body text with no error colour. Fixed together, because
   fixing one and documenting the other is how a known defect becomes a
   permanent one. The markup on every one of those pages has always said what
   it meant.

   TWO THINGS THIS DOES NOT DO, both checked in a browser rather than assumed.
   A `.muted` paragraph whose whole content is a LINK looks unchanged — the
   anchor carries its own colour and the app defines no base link colour, so
   "Forgot your password?", "← Back to sign in" and /Error's "Back to the app"
   stay browser-default blue. And waking a dead class up changes EVERY use of
   it: two lines using `.muted` turned out to be the primary message on their
   page ("an email is on its way", "Your password has been created") and went
   grey, so the class came off those two. Read every use before reviving a
   class. */
.login-card .muted,
.error-card .muted {
    color: var(--color-text-muted);
}

.login-card .notice,
.error-card .notice {
    color: var(--color-breach);
}

/* Weight only: the colour comes from `.notice` beside it, so the error red is
   defined once for these cards rather than twice. (`.admin .notice` is the
   precedent for that red outside the action panel. No colour vocabulary to
   collide with here — the roster's red-means-breach reading needs a roster, and
   this page never has one — and the words carry it regardless.) */
.error-lead {
    font-weight: 600;
}

.login-label {
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-size: var(--font-size-small);
    color: var(--color-text-muted);
}

.login-submit {
    height: 30px;
}

/* The user menu: the signed-in name is the dropdown button, with a caret big
   enough to read as "this opens" (the old small ▾ was easy to miss). */
.user-menu-btn {
    font-weight: 600;
}

.dropdown-caret {
    display: inline-block;
    margin-left: 6px;
    font-size: 14px;
    line-height: 1;
    color: var(--color-accent);
}

.btn.active .dropdown-caret,
.btn.delete-active .dropdown-caret {
    color: inherit;
}

/* Small input+button rows (group create/rename). */
.sysadmin-row {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 8px 0;
}

/* The sync section's effect-from picker. */
.sync-from {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 6px;
}

/* "Add rule group rules" (2026-08-16) — the sys-admin modal off the rules
   table's toolbar. Wider than a plain modal because it previews every rule it
   would copy in, and its backdrop scrolls: a group of six rules plus overlap
   warnings is easily taller than the viewport. */
.modal-add-group {
    margin: auto;
    width: min(560px, 94vw);
    max-width: none;
}

.modal-add-group .typeahead-input {
    width: 260px;
}

/* The picked group's effect-from sentence sits directly under its date field. */
.add-group-dates-note {
    margin-top: -8px;
}

/* The live sample timeline (SVG on the tokens). */
.rule-timeline {
    width: 100%;
    max-width: 660px;
    height: 88px;
    display: block;
    background: var(--color-field);
    border: 1px solid var(--color-border);
    margin: 4px 0 10px;
}

.rule-timeline .tl-axis {
    fill: var(--color-border-strong);
}

.rule-timeline .tl-tick {
    fill: var(--color-text-muted);
    font-size: 9px;
}

.rule-timeline .tl-label {
    fill: var(--color-text);
    font-size: 10px;
    font-weight: 600;
}

.rule-timeline .tl-label-red {
    fill: var(--color-breach);
    font-size: 10px;
    font-weight: 600;
}

.rule-timeline .tl-shift {
    fill: var(--color-accent);
    opacity: 0.75;
}

.rule-timeline .tl-ok {
    fill: var(--color-accent);
    opacity: 0.35;
}

.rule-timeline .tl-amber {
    fill: var(--color-warning);
}

.rule-timeline .tl-red {
    fill: var(--color-breach);
}

.rule-timeline .tl-empty {
    fill: none;
    stroke: var(--color-border);
}

.rule-timeline .tl-rest {
    fill: var(--color-warning);
    opacity: 0.35;
}

.rule-timeline .tl-night {
    fill: var(--color-accent);
    opacity: 0.2;
}

.rule-timeline .tl-red-band {
    fill: var(--color-breach);
    opacity: 0.3;
}

.rule-timeline .tl-cap {
    fill: var(--color-breach);
}

/* ---- Rules page: plain-language affordances ---- */

/* Abbreviations always explain themselves. */
.rules-page abbr {
    text-decoration: underline dotted;
    text-underline-offset: 2px;
    cursor: help;
}

.tier-legend {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin: 10px 0 14px;
    padding: 8px 10px;
    background: var(--color-field);
    border-left: 3px solid var(--color-border-strong);
}

.tier-legend .tier-item {
    display: flex;
    align-items: baseline;
    gap: 8px;
}

.tier-legend .tier-badge {
    flex: none;
    min-width: 86px;
    text-align: center;
}

/* Clickable help dots + their popovers. */
.label-line {
    display: flex;
    align-items: center;
    gap: 5px;
}

.help-anchor {
    position: relative;
    display: inline-flex;
}

.help-dot {
    width: 14px;
    height: 14px;
    padding: 0;
    border: 1px solid var(--color-border-strong);
    border-radius: 6px;   /* the one deliberate circle — a round ? reads as "help" */
    background: var(--color-field);
    color: var(--color-text-muted);
    font-size: 10px;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
}

.help-dot:hover {
    color: var(--color-text);
    border-color: var(--color-accent);
}

/* While a help pop is open, an invisible full-screen layer sits under it
   (z 55 < the pop's 60): clicking anywhere outside the pop dismisses it. */
.help-dismiss {
    position: fixed;
    inset: 0;
    z-index: 55;
}

.help-pop {
    position: absolute;
    top: 18px;
    left: -6px;
    z-index: 60;
    width: 300px;
    padding: 8px 10px;
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
    border-left: 3px solid var(--color-accent);
    color: var(--color-text);
    font-size: var(--font-size-base);
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    white-space: normal;
    cursor: default;
}

/* The editor panel hosts its own ✕ (same affordance as the modals). */
.rule-editor {
    position: relative;
}

/* Instant tooltips on the rules page: elements carrying data-tip show it on
   hover with no browser delay (native title waits ~1s). */
.rules-page [data-tip] {
    position: relative;
}

.rules-page [data-tip]:hover::after {
    content: attr(data-tip);
    position: absolute;
    left: 0;
    top: calc(100% + 4px);
    z-index: 70;
    width: max-content;
    max-width: 340px;
    padding: 6px 9px;
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
    border-left: 3px solid var(--color-accent);
    color: var(--color-text);
    font-size: var(--font-size-base);
    font-weight: 400;
    line-height: 1.4;
    text-transform: none;
    letter-spacing: normal;
    white-space: normal;
    pointer-events: none;
}

/* ------------------------------------------------------------
   "Test this rule…" (2026-07-28): the practice-roster modal.
   Sits ABOVE the rule editor's backdrop (z-index 100) and below
   the toasts (110). Rendered as a sibling of the editor overlay
   — a child would be trapped in its stacking context, where no
   z-index could lift it in front. The body is the real roster
   page in an iframe (its own document, so the grid's JS modules
   and panel behave exactly as on the roster). Near-fullscreen:
   the roster needs the width.
   ------------------------------------------------------------ */
.rule-test-overlay {
    position: fixed;
    inset: 0;
    z-index: 105;
    background: rgba(0, 0, 0, 0.45);
    display: flex;
}

.rule-test-modal {
    position: relative;
    margin: auto;
    width: min(1500px, 96vw);
    height: min(940px, 94vh);
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
    padding: 14px 16px 16px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.rule-test-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 16px;
    /* Clear the ✕ in the top-right corner. */
    padding-right: 28px;
}

.rule-test-modal h2 {
    font-size: 13px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin: 0 0 6px;
}

.rule-test-what {
    margin: 0 0 4px;
    display: flex;
    align-items: center;
    gap: 8px;
}

/* The rule's plain-English sentence is what the whole modal is about — it
   reads big and bold on its own line so the eye lands on it first. Centred
   briefly on 2026-07-31 and put back to the left the same day (Robert):
   left-aligned is where the eye starts, and it sits over the steps below it. */
.rule-test-summary {
    margin: 0;
    color: var(--color-text);
    font-size: 18px;
    font-weight: 600;
}

.rule-test-controls {
    display: flex;
    align-items: flex-end;
    gap: 12px;
    flex-shrink: 0;
}

/* Shown only while "Testing with" is on all the organisation's rules: the
   numbered steps are pinned against the isolated set, so they can stop being
   true. Sits between the summary and the steps it qualifies. */
.rule-test-scope-notice {
    margin: 0;
    font-size: var(--font-size-base);
}

.rule-test-instructions {
    overflow-y: auto;
    max-height: 30vh;
    flex-shrink: 0;
    border-top: 1px solid var(--color-border);
    padding-top: 8px;
    /* Comfortably above the app's base size (feedback 2026-07-31): the steps
       are the whole task now the long preamble lives in the About pop-up. */
    font-size: 14px;
}

.rule-test-steps {
    margin: 0;
    padding-left: 22px;
}

.rule-test-steps li {
    margin-bottom: 7px;
    line-height: 1.5;
}

/* "About the practice roster": the preamble and the permission-dropdown notes
   moved into a pop-up (feedback 2026-07-31) so the steps read unburied. */
.rule-test-about-link {
    margin-top: 6px;
    color: var(--color-accent);
    font-size: var(--font-size-base);
}

.rule-test-about-link:hover {
    text-decoration: underline;
}

/* Fixed, not absolute (2026-08-01): as a child of the modal it only dimmed the
   modal's own area, so a click in the darker region OUTSIDE the modal — which
   reads as "outside the box" just as much — dismissed nothing. Covering the
   viewport makes every click off the panel close it. It still sits inside the
   rule-test overlay's stacking context, so z-index 10 keeps it on top there. */
.rule-test-about-overlay {
    position: fixed;
    inset: 0;
    z-index: 10;
    background: rgba(0, 0, 0, 0.35);
    display: flex;
}

.rule-test-about {
    position: relative;
    margin: auto;
    width: min(560px, 90%);
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
    padding: 14px 18px 16px;
    font-size: var(--font-size-base);
}

.rule-test-about h3 {
    margin: 0 0 10px;
    font-size: 13px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.rule-test-about p {
    margin: 0 0 10px;
    line-height: 1.5;
}

/* The three tiers, explained once here instead of costing a step in every
   walkthrough (2026-08-01). */
.rule-test-about-list {
    margin: 0 0 10px;
    padding-left: 20px;
    line-height: 1.5;
}

.rule-test-about-list li {
    margin-bottom: 5px;
}

.rule-test-about-actions {
    display: flex;
    justify-content: flex-end;
}

.rule-test-frame {
    flex: 1;
    width: 100%;
    min-height: 0;
    border: 1px solid var(--color-border-strong);
    background: var(--color-bg);
}

/* The launch button sits at the opposite end of the actions row from Save;
   the span (not the disabled button, which swallows hover) carries the tip. */
.rule-test-anchor {
    margin-left: auto;
}

.buffer{
    margin-top: 20px;
}

/* Grid benchmark (2026-08-03): a sys-admin diagnostic page. The inputs are a
   short row of numbers rather than the stacked entity-form column, and the
   report is preformatted text sized to be read and pasted. Wide content scrolls
   inside its own box so the page body never scrolls sideways (house rule). */
.bench-form {
    flex-direction: row;
    flex-wrap: wrap;
    gap: 14px;
    max-width: none;
}

.bench-form label {
    justify-content: flex-start;
    gap: 6px;
}

.bench-form input {
    width: 76px;
}

.bench-report {
    margin-top: 14px;
    padding: 12px 14px;
    background: var(--color-field);
    border: 1px solid var(--color-border);
    color: var(--color-text);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px;
    line-height: 1.45;
    overflow-x: auto;
    white-space: pre;
}

/* The pivot group page's lines editor (the tag pivot, 2026-09-02): a narrow number
   field for the quantity, and the add-a-tag picker under the lines table. */
.pivot-qty { width: 5.5em; }
.pivot-add-line { margin: 8px 0 12px; max-width: 360px; }
.entity-form textarea.pivot-description { min-height: 4.5em; resize: vertical; }

/* ------------------------------------------------------------
   The Pivot View (slice 2, 2026-09-02): the roster with a job's
   pivot-group tags as rows. The same grid, the same cells; what
   differs is the left column and the section headers. The section
   header reuses .group-header's look (uppercase, muted) with the ✕
   the Colours tab's rows carry; a row label cell is a .grid-employee
   so every module that keys on that class (viewport, hover) still
   works, with its own class for the label's look.
   ------------------------------------------------------------ */
.pivot-section-head {
    justify-content: space-between;
    /* 12px, not the .group-header 10px (Robert, 2026-09-05): this band carries the
       job name and the pivot group applied to it, which is the whole subject of the
       section below it — not a quiet department caption. Set on the head, so the
       job label and the group both take it; the 24px band has room for it. */
    font-size: var(--font-size-base);
}

.pivot-section-head .group-header-label {
    text-transform: none;
    letter-spacing: normal;
    font-weight: 600;
}

.pivot-section-head .pivot-section-group {
    color: var(--color-text);
    font-weight: 400;
}

.pivot-section-head .pivot-section-remove {
    position: sticky;
    right: 6px;
    margin-left: auto;
    background: none;
    border: 0;
    padding: 0 6px;
    color: var(--color-text-muted);
    cursor: pointer;
    font-size: var(--font-size-base);
}

.pivot-section-head .pivot-section-remove:hover {
    color: var(--color-breach);
}

.grid-employee.pivot-row-label {
    cursor: default;
}

.grid-employee.pivot-row-other {
    font-style: italic;
    color: var(--color-text-muted);
}

.pivot-section-not-configured .group-header-label {
    font-style: italic;
    color: var(--color-text-muted);
}

/* The absence signifier: a grey, inert block naming the absence that emptied the
   cell. Page-only; never colour alone, because it carries the name. */
.pivot-absence-signifier {
    position: absolute;
    inset: var(--block-inset) 2px;
    z-index: 1;
    display: flex;
    align-items: center;
    padding: 0 3px;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-size: var(--font-size-small);
    font-style: italic;
    color: var(--color-text-muted);
    background: var(--color-field);
    border: 1px dashed var(--color-border-strong);
    pointer-events: none;
}

/* ------------------------------------------------------------
   The jobs a Pivot View drag cannot reach (2026-09-03, Robert).
   A section belongs to a job, so a drag can only land on rows of
   its OWN job. Those other rows were painted avail-red on
   every cell until this date, which both looked ugly across half a
   screen and said the wrong thing: red is a fatigue verdict, and no
   verdict was ever computed for a cell the gesture cannot reach.
   They recede instead — head, labels and cell contents — so the one
   job that IS reachable is the only thing left at full strength.
   roster-entry-drag.js adds the class from the grid's data-job
   attributes at drag start and drops it with the rest of the paint.
   ------------------------------------------------------------ */
.pivot-section-head.drag-other-job,
.grid-employee.drag-other-job {
    opacity: 0.3;
}

/* THE CELL ITSELF MUST NOT TAKE THE OPACITY, and this is the load-bearing half.
   Opacity makes a stacking context, which would trap a block's z-index: 1 inside
   its own cell — so a midnight spanner's overflow would be painted over by the
   next cell's background instead of running across the border, which is the one
   thing this grid's block positioning exists to do. The cell's background is
   washed by an overlay sitting UNDER the blocks (z-index 0 against their 1) and
   the blocks dim themselves, which reaches the same picture with no stacking
   context on the cell. It is inert (pointer-events: none), so the cell is still
   the drop target it was and .NET still refuses the drop with its own sentence. */
.roster-cell.drag-other-job::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    background: var(--color-bg);
    opacity: 0.7;
    pointer-events: none;
}

.roster-cell.drag-other-job > .roster-entry,
.roster-cell.drag-other-job > .pivot-absence-signifier {
    opacity: 0.3;
}

/* ------------------------------------------------------------
   The person's work elsewhere (2026-09-03, Robert's idea). While
   a Pivot View drag is live, or Add has a person picked, the
   cells a drop or click could land on carry data-elsewhere: what
   that person already holds on that day, as a FAINT BLOCK, so the
   user can see where a stripe comes from and where the person is
   already working on a screen whose rows are tags rather than
   people. roster-entry-drag.js writes the label as the attribute
   and the block's colour, ink and ribbon as custom properties on
   the cell's own style, and CSS renders them here as the two
   pseudo-elements: attribute-level paint survives a Blazor
   re-render exactly as the stripe classes do, where a node
   inserted by JS would not.

   It is the whole block a real entry would draw (Robert's second
   ruling of the day, replacing a bare watermark): the job's own
   background and ink, the block's hairline, the fill view's white
   time-ribbon mark at the entry's true place in the day, and the
   label in italics, all at a fraction of the strength. Never
   colour alone (it carries the words), inert, and UNDER the real
   blocks (z-index 0 against their 1), decided from a screenshot:
   drawn over a cell that holds another person's work, its text
   landed on the block's name and neither could be read. So on an
   occupied cell the block wins and the faint one is hidden (fill
   is the default view); the same day's other rows still carry it,
   and in Add mode the cell's own tooltip does too. The ribbon is
   ::after, which the dim wash also owns: the two can never meet,
   because a wash sits only on jobs a drag cannot reach and a faint
   block only where it can, and the :not() says so. No opacity on
   the cell itself, for the stacking-context reason the wash
   records.
   ------------------------------------------------------------ */
.pivot-grid .roster-cell[data-elsewhere]::before {
    content: attr(data-elsewhere);
    position: absolute;
    /* The faint block sits where a real block sits, so it reads --block-inset
       (2026-09-08) rather than repeating its 3px: the heat map widens that inset, and a
       row of faint blocks that failed to line up with the real ones beside them would be
       the very thing this element exists to link. */
    inset: var(--block-inset) 2px;
    z-index: 0;
    padding: 0 3px;
    line-height: calc(var(--row-height) - 2 * var(--block-inset) - 2px);
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-size: var(--font-size-small);
    font-style: italic;
    color: var(--ew-ink, #f2f3f5);
    background: var(--ew-bg, var(--color-field));
    border: 1px solid rgba(0, 0, 0, 0.45);
    opacity: 0.45;
    pointer-events: none;
}

.pivot-grid .roster-cell[data-elsewhere]:not(.drag-other-job)::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: calc(var(--block-inset) + 1px);   /* follows the block, as the real ribbon does */
    height: 3px;
    z-index: 0;
    background: var(--ew-ribbon);
    opacity: 0.45;
    pointer-events: none;
}

/* The absence signifier is a child element at the blocks' own z-index, so the
   block rule above would not cover it and the two texts would meet: it wins
   the cell exactly as a block does. */
.pivot-grid .roster-cell:has(> .pivot-absence-signifier)[data-elsewhere]::before {
    display: none;
}

/* The companion: the same person's REAL blocks, wherever they sit on the screen,
   wear a dashed accent outline for the length of the gesture and keep their
   strength inside a dimmed job, so the eye links a faint line to its source. */
.roster-entry.pivot-same-person {
    outline: 2px dashed var(--color-accent);
    outline-offset: -2px;
}

.roster-cell.drag-other-job > .roster-entry.pivot-same-person {
    opacity: 1;
}

/* The corner's two-step picker: the job first, then the group for it. */
.grid-corner .pivot-corner {
    display: flex;
    flex: 1;
    min-width: 0;
    gap: 4px;
    align-items: center;
}

.grid-corner .pivot-corner .typeahead {
    flex: 1;
    min-width: 0;
}

/* Holding a job turns the corner into a two-line panel: the job on a chip, the
   group field under it (Robert's mock-up, 2026-09-03 — it replaced a bare "#214"
   beside the field, which could not show the name at all).

   ABSOLUTE, AND IT OVERFLOWS DOWNWARD AND RIGHT ON PURPOSE. .roster-area scrolls,
   so a second line drawn ABOVE the sticky corner would be clipped away; and making
   the corner taller instead would shove the whole grid down a line at the moment
   the user is about to type in a field that had just moved (the name-column width
   jump, in a second place). So nothing moves — a panel appears over the strip of
   header and first section head beside it, for the seconds the two-step gesture
   takes. .grid-corner is position: sticky, so it is the containing block; its
   contain: inline-size keeps the track from growing and does not clip. */
.grid-corner .pivot-corner-picked {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 1;
    flex-direction: column;
    align-items: stretch;
    min-width: 100%;
    width: max-content;
    max-width: 420px;
    padding: 3px 4px 4px;
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
}

/* In the column the field sizes to its own height rather than stretching. */
.grid-corner .pivot-corner-picked .typeahead {
    flex: none;
}

.grid-corner .pivot-corner-chip {
    display: flex;
    align-items: center;
    gap: 4px;
    min-width: 0;
    padding: 2px 2px 2px 5px;
    background: var(--color-field);
    border: 1px solid var(--color-border);
}

.grid-corner .pivot-corner-chip-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-size: var(--font-size-small);
    font-weight: 600;
    color: var(--color-text);
}

/* The tag chips' own remove button, one control over: a quiet ✕ that grows on
   hover. It runs the very gesture Escape runs, which is what Robert asked for. */
.grid-corner .pivot-corner-chip-remove {
    flex: none;
    padding: 0 3px;
    background: none;
    border: 0;
    color: var(--color-text-muted);
    font-size: var(--font-size-small);
    line-height: 1;
    cursor: pointer;
}

.grid-corner .pivot-corner-chip-remove:hover {
    color: var(--color-text);
}

/* The Fatigue Index (2026-09-08): the Details tab's section, the Tag Groups tab's
   per-position table, the report page's columns and the four licence notices. Minute
   boxes are compact like the break tiers'; scores are numbers, so right-aligned. */
.job-form .fatigue-minutes {
    width: 90px;
}

.fatigue-profiles {
    max-width: 780px;
    margin: 6px 0 10px;
}

.fatigue-profiles .select {
    width: 100%;
    max-width: 320px;
}

.report-table .fatigue-score {
    text-align: right;
    width: 110px;
}

.report-table .fatigue-time,
.report-table .fatigue-day {
    white-space: nowrap;
}

.fatigue-notices {
    margin: 18px 0 6px;
    max-width: 780px;
}

.fatigue-notices p {
    margin: 0 0 6px;
}

/* ------------------------------------------------------------
   Blazor's "An unhandled error has occurred" bar, hidden until the framework shows it
   (it sets display itself). HERE, NOT IN A LAYOUT'S SCOPED STYLESHEET, since 2026-09-23:
   the rule lived in MainLayout.razor.css, and a scoped rule only matches elements that
   layout renders, so the demo's own layout (DemoLayout) rendered the same bar with no
   rule at all. It sat in the page's flow just under the fold, made the page 40-odd
   pixels taller than the window, and showed the words to any visitor whose page scrolled
   (seen at 1,024 by 768 at the demo's end). One global rule serves every layout.
   ------------------------------------------------------------ */
#blazor-error-ui {
    color-scheme: light only;
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    box-sizing: border-box;
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;
}

/* ------------------------------------------------------------
   The live demo at /try (docs/specs/live-demo.md, 2026-09-22;
   reshaped 2026-09-23 on the research of the day before and
   Robert's picks). The real grid under ONE fixed strip, and the
   teaching drawn OVER the page by demo.js: a caption and an arrow
   beside the step's target, a dashed marker on the target cell in
   step 1. Light theme only (the shell carries no theme class). The
   strip and, under it since 2026-09-23, the real action panel are
   the two things above the grid; neither changes height, and the
   toast sits over them and never reaches the grid (the house toast
   rule).
   ------------------------------------------------------------ */
.demo-strip {
    flex: none;
    height: 56px;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 0 20px;
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border-strong);
    white-space: nowrap;
}

/* The logo, at the website header's size (2026-10-06): the strip is the same
   white bar a visitor arrives from when they press Try it out, so the logo keeps
   its size and colour across the jump. It is a link back to the website's home
   page (2026-10-06), so it drops the link underline. */
.demo-wordmark {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    color: var(--logo-ink);
    text-decoration: none;
}

.demo-wordmark .logo-symbol {
    width: 24px;
    height: 24px;
}

.demo-wordmark .logo-name {
    font-size: 15px;
}

.demo-live {
    color: var(--color-text-muted);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.demo-count {
    padding-left: 14px;
    border-left: 1px solid var(--color-border);
    color: var(--color-accent);
    font-size: 13px;
    font-weight: 700;
}

.demo-strip-right {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 8px;
}

.demo-link {
    color: var(--color-accent);
    font-size: var(--font-size-base);
    text-decoration: underline;
}

/* The one quiet line saying the demo is guided (the free-play line at the end), under
   the grid in the empty half of the screen, where it collides with nothing at any width. */
.demo-guided {
    margin: 10px 20px;
    color: var(--color-text-muted);
    font-size: 12px;
}

/* The busy, too-narrow and expired notices, and the loading line: a plain block
   under the strip, where the grid would be. */
.demo-notice {
    padding: 24px 20px;
    max-width: 640px;
    font-size: 15px;
    line-height: 1.5;
}

.demo-notice p {
    margin: 0 0 12px;
}

/* With the marquee off, a press-and-drag on empty cells would select the grid's
   text; the demo has nothing to select. */
.demo-roster .roster-grid {
    user-select: none;
}

.demo-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
}

/* The strip's own buttons: the house ghost button and the solid primary, at the
   strip's size. Their own classes, because .btn-primary is the modal's full-width
   button and the two must not share a name (the never-share-a-class rule). */
.demo-btn {
    display: inline-block;
    height: 30px;
    padding: 0 14px;
    background: transparent;
    color: var(--color-text);
    border: 1px solid var(--color-border-strong);
    border-radius: 0;
    font-family: var(--font-family);
    font-size: var(--font-size-base);
    font-weight: 600;
    line-height: 28px;
    text-decoration: none;
    cursor: pointer;
}

.demo-btn:hover {
    background: var(--color-field);
}

.demo-btn:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

.demo-btn-primary {
    background: var(--color-accent);
    color: #ffffff;
    border-color: rgba(0, 0, 0, 0.35);
}

.demo-btn-primary:hover {
    background: var(--color-accent);
    filter: brightness(1.1);
}

/* THE TEACHING LAYER. Fixed over the whole page, takes no layout space, passes the
   pointer through: a drop on a cell under the caption lands on the cell, and nothing
   here can move the grid when a drag starts (the layout-shift-cancels-the-drag rule,
   measured 2026-09-22). Below the modal backdrop (100) and the toasts (110), so the
   confirmation dialog and a refusal paint over it, and above the grid's own layers. */
.demo-layer {
    position: fixed;
    inset: 0;
    z-index: 60;
    pointer-events: none;
}

.demo-arrows {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    overflow: visible;
}

.demo-arrows path {
    fill: none;
    stroke: var(--color-text);
    stroke-width: 3;
    stroke-linecap: round;
}

.demo-arrow-head {
    fill: var(--color-text);
    stroke: none;
}

/* The caption (Robert's pick, 2026-09-23, over the video's comic style and a plain
   line): an outlined box with a small hard shadow, ordinary bold type. White and 16px
   until later that day, when the screenshots of the build showed it small against the
   grid's colour blocks and step 4 had already needed yellow to be seen: EVERY note is
   the same yellow now (the pinned highlight's #fbdf74, the app's one "look here"
   colour) and a size up, so a visitor who finds the first note learns the shape. The
   width is demo.js's CAPTION_WIDTH; the two must agree. */
.demo-cap {
    position: absolute;
    max-width: 340px;
    padding: 9px 13px;
    background: #fbdf74;
    border: 2px solid var(--color-text);
    box-shadow: 3px 3px 0 rgba(36, 38, 42, 0.9);
    color: var(--color-text);
    font-size: 18px;
    font-weight: 600;
    line-height: 1.3;
    white-space: normal;
}

/* The step's number on the note, a badge matching the strip's "2 of 6" (2026-09-23): it
   says "this box is the instruction, and there are six" without a word. Bigger on step
   3's loud note. A circle, like the final card's ticks. */
/* Step 6's second note (2026-09-28, evening), under the grid: a wider box than the others,
   so its two sentences are three lines rather than five. */
.demo-cap-roles {
    max-width: 460px;
}

.demo-cap-num {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 22px;   /* a single digit is a 22px circle; step 7's "7a" to "7c" (2026-09-28) widen it into a pill */
    height: 22px;
    padding: 0 6px;
    box-sizing: border-box;
    margin-right: 8px;
    border-radius: 11px;
    background: var(--color-text);
    color: #ffffff;
    font-size: 13px;
    font-weight: 800;
    vertical-align: 2px;
}

/* The begin note (Robert, 2026-09-23; the note itself the button since later that day): a
   real button wearing step 1's note, placed by demo.js in step 1's place, above the veil
   and the layer. Parked off-screen until the script places it, so it never shows for a
   frame in the corner. A slightly deeper yellow under the pointer says it is clickable.
   A CALL TO ACTION since later on 2026-09-28 (Robert's pick, option C of that evening's
   design round, after the one-line "Click here to begin…" read as bland): a headline over
   a smaller meta line at the left, and a Start pill in the ink colour at the right, all
   inside the one button, so the whole box still begins and the pill only says so. Each
   line stays on one line (nowrap), so the box is as wide as its words; demo.js keeps its
   bottom edge where step 1's note will sit. */
.demo-begin {
    position: fixed;
    left: -9999px;
    top: 0;
    z-index: 61;
    max-width: none;
    margin: 0;
    padding: 12px 14px 12px 16px;
    display: flex;
    align-items: center;
    gap: 22px;
    font-family: var(--font-family);
    text-align: left;
    white-space: nowrap;
    cursor: pointer;
}

.demo-begin-text {
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.demo-begin-title {
    font-size: 20px;
    font-weight: 800;
    line-height: 1.15;
}

.demo-begin-meta {
    font-size: 13px;
    font-weight: 600;
    line-height: 1.3;
    color: #4a3d0c;   /* a darker olive rather than the ink, so the line reads as secondary on the yellow (8:1) */
}

.demo-begin-start {
    padding: 11px 18px;
    background: var(--color-text);
    border: 2px solid var(--color-text);
    color: #ffffff;
    font-size: 15px;
    font-weight: 700;
    line-height: 1;
}

.demo-begin:hover {
    background: #f7d34f;
}

.demo-begin:disabled {
    cursor: default;
}

/* THE VEIL (Robert, 2026-09-23): the whole page dimmed but the begin note, until the
   visitor clicks the note. It only CATCHES the pointer (since later that day, when the note
   itself became the button): a click on it does nothing, and nothing under it can be
   dragged or clicked. Below the layer (60) and the begin note (61), above everything of
   the grid's own; below the modal backdrop and the toasts, which cannot be up before the
   demo begins anyway. The text colour at 45%: a grey veil at 50% muddied the colours,
   and a dark one is the look a first-run overlay has elsewhere, so it reads as
   deliberate. No animation when it goes, per the no-animation ruling: it is simply not
   rendered once step 1 has begun. */
.demo-veil {
    position: fixed;
    inset: 0;
    z-index: 59;
    background: rgba(36, 38, 42, 0.45);
}

/* The strip sits ABOVE the veil before step 1 (2026-09-23, from both findability reports),
   so a visitor who wants out before beginning has Exit demo rather than only the browser's
   Back. The page shows no Start again there, since nothing has begun. Positioned only so
   the z-index applies: nothing about its layout changes, and it applies before step 1
   only, never at a drag start. */
.demo-strip.demo-strip-over-veil {
    position: relative;
    z-index: 60;
}

/* The action panel's switched-off controls on the demo (2026-09-23, from the
   screenshots): a ghost button only lightens when disabled, so the greyed panel still
   read as a menu to try. Faded hard here, labels and date boxes with them, so the one
   control that IS live (Undo in step 3, Search in step 6, the # cycle throughout) is the
   one at full strength. */
.demo-shell .action-panel .btn:disabled,
.demo-shell .action-panel .panel-label:has(> .select:disabled) {
    opacity: 0.4;
}

/* Step 4's Okay, inside the drawn caption: the one thing in the teaching layer that takes
   the pointer, on its own line at the caption's right. */
.demo-cap-okay {
    display: block;
    margin: 10px 0 0 auto;
    pointer-events: auto;
}

/* The docked search window on the demo (step 5 and the end). The ordinary shell's grid
   (four rows: panel, two dock halves, status bar) has no row for the demo's strip, so the
   demo gets its own: strip, panel, then the roster and the dock side by side. */
.demo-shell:has(> .search-window.docked) {
    grid-template-rows: auto auto minmax(0, 1fr) minmax(0, 1fr);
}

.demo-shell:has(> .search-window.docked) > .demo-strip {
    grid-area: 1 / 1 / 2 / 3;
}

.demo-shell:has(> .search-window.docked) > .action-panel {
    grid-area: 2 / 1 / 3 / 3;
}

.demo-shell:has(> .search-window.docked) > .roster-area {
    grid-area: 3 / 1 / 5 / 2;
}

.demo-shell:has(> .search-window.docked) > .search-window.docked {
    grid-area: 3 / 2 / 5 / 3;
}

/* The final card (Robert, 2026-09-23: "use some nice design to set it out"; back to this
   shape on the evening of 2026-09-28, his ask: the simple list, under the employee rows).
   It sits in the roster area's own flow under the grid, in the captions' family (the dark
   2px outline, the hard shadow, the accent bar across its top). For most of 2026-09-28 it
   was a modal over the grid with a picture of the role view and the visitor's own audit
   rows; that opened the instant the search armed and read as the search's result. */
.demo-final {
    /* Wide enough that each ticked line fits on one line in its column (the longest,
       "Live updates for all logged in users", needs about 330px); a narrower window
       shrinks the card and that line wraps. */
    max-width: 760px;
    margin: 0 auto;
    padding: 24px 28px 22px;
    background: var(--color-surface);
    border: 2px solid var(--color-text);
    border-top: 8px solid var(--color-accent);
    box-shadow: 6px 6px 0 rgba(36, 38, 42, 0.9);
    color: var(--color-text);
}

.demo-final-title {
    margin: 0 0 16px;
    font-size: 24px;
    font-weight: 800;
    line-height: 1.2;
}

.demo-final-list {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px 24px;
    margin: 0 0 18px;
    padding: 0;
    list-style: none;
    font-size: 16px;
    font-weight: 600;
}

.demo-final-list li {
    display: flex;
    align-items: center;
    gap: 10px;
}

.demo-final-tick {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: var(--color-accent);
    color: #ffffff;
    font-size: 14px;
    font-weight: 800;
}

.demo-final-closing {
    margin: 0 0 18px;
    padding-top: 16px;
    border-top: 1px solid var(--color-border);
    color: var(--color-text-muted);
    font-size: 18px;
    line-height: 1.45;
}

/* Two buttons since later on 2026-09-23: "Play with the demo", which puts the card away,
   and Get in touch, the solid one (Robert: Get in touch on the card, the invitation to play
   kept as clear). */
.demo-final-actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
}

/* THE END CARDS' LAYER (Robert, 2026-10-01, later): fixed over the page, centred, its top
   set by demo.js (placeEndCards) to where the begin note's top was, so the questions card
   sits where "Try the roster for yourself" sat. Above the roster area and the panel it
   covers, below the modals (100) and the toasts (110), and beside the teaching layer (60),
   which draws nothing at the end. It takes the pointer: its buttons are real. */
.demo-end-cards {
    position: fixed;
    left: 0;
    right: 0;
    top: 110px;   /* overwritten by demo.js; the measured value at 1,440 wide as the fallback */
    z-index: 61;
    padding: 0 16px;
    pointer-events: none;
}

.demo-end-cards > * {
    pointer-events: auto;
}

/* THE QUESTIONS CARD (Robert's decisions of 2026-10-01): under the free play line and
   above the final card, the final card's width so the two align, and otherwise as small
   as reasonably possible: compact type, the captions' family's dark outline and hard
   shadow and nothing more. The ✕ at the top right and "No thanks" beside Send are what
   make it obviously dismissable. Not a modal: free play is live beneath it. */
.demo-questions {
    position: relative;
    max-width: 760px;
    margin: 0 auto 20px;
    padding: 12px 40px 12px 16px;
    background: var(--color-surface);
    border: 2px solid var(--color-text);
    box-shadow: 4px 4px 0 rgba(36, 38, 42, 0.9);
    color: var(--color-text);
    font-size: 13px;
}

.demo-questions-close {
    position: absolute;
    top: 6px;
    right: 8px;
    width: 26px;
    height: 26px;
    padding: 0;
    background: transparent;
    border: 1px solid transparent;
    color: var(--color-text-muted);
    font-size: 15px;
    line-height: 24px;
    cursor: pointer;
}

.demo-questions-close:hover {
    color: var(--color-text);
    border-color: var(--color-border-strong);
}

.demo-questions-title {
    margin: 0 0 8px;
    font-size: 15px;
    font-weight: 800;
    line-height: 1.2;
}

.demo-q {
    margin: 0 0 8px;
}

.demo-q-label {
    display: block;
    margin: 0 0 4px;
    font-weight: 600;
}

.demo-q-options {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

/* An option is a radio drawn as a small outlined button: one click picks it, the pick
   fills with the accent, and a click on another option in the row moves the fill. */
.demo-q-option {
    height: 26px;
    padding: 0 10px;
    background: var(--color-surface);
    color: var(--color-text);
    border: 1px solid var(--color-border-strong);
    border-radius: 0;
    font-family: var(--font-family);
    font-size: 13px;
    line-height: 24px;
    cursor: pointer;
}

.demo-q-option:hover {
    background: var(--color-field);
}

.demo-q-option.demo-q-selected,
.demo-q-option.demo-q-selected:hover {
    background: var(--color-accent);
    color: #ffffff;
    border-color: var(--color-accent);
}

.demo-q-email-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
}

.demo-q-email-row .demo-q-label {
    margin: 0;
}

.demo-q-email {
    width: 280px;
    height: 26px;
    font-size: 13px;
}

.demo-questions-problem {
    margin: 0 0 8px;
    color: var(--color-breach);
    font-weight: 600;
}

.demo-questions-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}

.demo-questions-actions .demo-btn {
    height: 26px;
    line-height: 24px;
    font-size: 13px;
}

/* After Send: one line and its ✕, until closed. */
.demo-questions-done {
    padding-top: 8px;
    padding-bottom: 8px;
}

.demo-questions-thanks {
    margin: 0;
    font-size: 14px;
    font-weight: 600;
    line-height: 26px;
}

/* Step 5's copy of Grace's Friday block (Robert, 2026-09-23): the grid's own element,
   cloned by demo.js, taken out of the cell's absolute placement (its inline `left` is the
   fill view's slot, hence the !important) and shown half as big again, with a small arrow
   pointing at its figure. */
.demo-sample {
    display: flex;
    align-items: flex-start;
    margin-top: 12px;
}

.demo-sample .roster-entry.demo-sample-block {
    position: relative;
    left: 0 !important;
    top: 0;
    bottom: auto;
    height: 26px;
    flex: none;
    zoom: 1.5;
    cursor: default;
}

.demo-sample-arrow {
    flex: none;
    width: 48px;
    height: 34px;
    margin: 2px 0 0 -4px;
    overflow: visible;
}

.demo-sample-arrow path {
    fill: none;
    stroke: var(--color-text);
    stroke-width: 2.5;
    stroke-linecap: round;
}

.demo-sample-arrow .demo-arrow-head {
    fill: var(--color-text);
    stroke: none;
}

/* Step 3's caption, where the visitor clicked Confirm, with a long arrow up to the
   action panel's Undo button (Robert, 2026-09-23: "it should be in your face"): the same outlined box,
   bigger and heavier, and the Ctrl+Z note under it in smaller, quieter type. */
.demo-cap-loud {
    max-width: 420px;
    padding: 14px 20px;
    border-width: 3px;
    box-shadow: 5px 5px 0 rgba(36, 38, 42, 0.9);
    font-size: 26px;
    font-weight: 700;
}

.demo-cap-loud .demo-cap-num {
    width: 30px;
    height: 30px;
    margin-right: 10px;
    font-size: 16px;
    vertical-align: 3px;
}

.demo-cap-note {
    display: block;
    margin-top: 4px;
    color: var(--color-text-muted);
    font-size: 14px;
    font-weight: 600;
}

.demo-arrows path.demo-arrow-loud {
    stroke-width: 4.5;
}

/* Step 2's second arrow, from the shift to the cell it goes to (Robert, 2026-09-23):
   dotted, so it reads as the path of the move rather than a second pointer. */
.demo-arrows path.demo-arrow-dotted {
    stroke-width: 2.5;
    stroke-dasharray: 2 6;
}

/* While the visitor is dragging, the caption steps back: the grid is what they are
   looking at. An opacity change moves nothing, which is the whole point. */
.demo-shell:has(> .roster-area[data-demo-dragging]) .demo-cap {
    opacity: 0.55;
}

/* Step 6's first note and its arrow fade in when the step arrives (Robert, 2026-09-28, later
   still), two seconds after the search armed, rather than snapping on. demo.js puts the class
   on both and sets the duration inline from the layer's data-fade-ms (DemoSteps.RolesNoteFadeMs,
   the one number), and a negative delay when a re-draw resumes a fade already under way; the
   keyframes carry no number. Opacity only, so nothing moves, and it runs whatever the
   reduced-motion setting says: an opacity fade is not motion (the remote-change fade's own
   reasoning, and the undo's flight ignores the setting by Robert's ruling). */
.demo-layer .demo-fade-in {
    animation-name: demo-note-fade-in;
    animation-timing-function: ease-out;
    animation-fill-mode: both;
}

@keyframes demo-note-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

.demo-marker {
    position: absolute;
    box-sizing: border-box;
    border: 2px dashed var(--color-text);
}

/* Step 2's target is an AMBER cell, and during the drag its stripes are the lesson, so the
   marker and its tag get out of the way for exactly that long (2026-09-23: step 2 went
   without a marker earlier that day because the dashes and the tag hid the stripes, and the
   findability research then showed both arrowheads landing in the neighbouring cells with
   nothing marking the one they meant). Opacity, so nothing moves when the drag starts.
   Step 1's target is a plain cell, so its marker stays up throughout. */
.demo-shell:has(> .roster-area[data-demo-dragging]) .demo-layer[data-step="2"] :is(.demo-marker, .demo-tag) {
    opacity: 0;
}

.demo-tag {
    position: absolute;
    padding: 1px 6px;
    background: var(--color-surface);
    border: 2px solid var(--color-text);
    color: var(--color-text);
    font-size: 11px;
    font-weight: 700;
}

/* On a tablet held upright (2026-10-07: the demo starts from 820 pixels since then,
   DemoSteps.MinimumWidthPx), the 100px day column made a week 860 pixels with the
   name column, so Sunday ran off the edge. 94px fits the whole week from 820:
   160 + 7 x 94 = 818. demo.js reads this to count the days, so the two agree. The
   docked search window narrows too, from 420 to 320 pixels, or step 5 would leave
   the grid about two days wide. */
@media (max-width: 1023px) {
    .demo-roster {
        --day-width: 94px;
    }

    .demo-shell {
        --search-dock-width: 320px;
    }
}

/* A WIDE WINDOW SHOWS THE DEMO'S GRID A SIZE UP (2026-09-23; both findability reports
   recommended about 1.2 to 1.3 times on wide screens). At 1,920 pixels the whole demo was a
   thin strip across the top of a blank page, and the shifts, which are the things a visitor
   drags, were the smallest targets on it. Two steps, and nothing below 1,400 pixels, so the
   narrow end of the range is exactly as it was. The column width is on the page's own
   <main> because demo.js reads it there before the grid exists, to work out how many days
   fit (DemoScenarios.DaysToFill); the row and type tokens are on the grid itself, so a
   dialog rendered inside the roster area keeps its own size. The Fatigue Index row keeps
   its 2px (the .roster-grid[data-fatigue-index] rule's own tuning). The name column is not
   scaled: its 160px floor still holds every name in the cast at the larger type. */
@media (min-width: 1400px) {
    .demo-roster {
        --day-width: 120px;
    }

    .demo-roster .roster-grid {
        --row-height: 36px;
        --font-size-base: 14px;
        --font-size-small: 12px;
    }

    .demo-roster .roster-grid[data-fatigue-index="true"] {
        --row-height: 38px;
    }
}

@media (min-width: 1800px) {
    .demo-roster {
        --day-width: 130px;
    }

    .demo-roster .roster-grid {
        --row-height: 39px;
        --font-size-base: 15px;
        --font-size-small: 13px;
    }

    .demo-roster .roster-grid[data-fatigue-index="true"] {
        --row-height: 41px;
    }
}

/* The live demo's VIDEO MODE (DemoVideo, 2026-09-29; laptop only): the marketing video's
   drive films the demo with its teaching hidden. The page renders a hidden
   .demo-video-mode span as a direct child of the shell, and everything here keys on it.
   - The drawn teaching goes; its layer stays in the DOM because the drive reads each
     step's targets off the layer's attributes.
   - The page LOOKS like the real app's. The panel's buttons lose the demo's fading, and
     every disabled field on the page (the panel's dates, the grid corner's department box)
     loses the locked grey, because in the real app those are live. They all keep their demo
     gates (the drive only drags and undoes), so they are still disabled underneath.
     background-COLOR, never the shorthand, so a disabled select keeps its chevron image.
   - The narrow-window rule below does not apply: it exists for the teaching's room, and
     the video's 4:5 page is narrower than it. */
.demo-layer.demo-layer-silent {
    display: none;
}

.demo-shell:has(> .demo-video-mode) .action-panel .btn:disabled,
.demo-shell:has(> .demo-video-mode) .action-panel .panel-label:has(> .select:disabled) {
    opacity: 1;
}

.demo-shell:has(> .demo-video-mode) .action-panel .btn:disabled {
    color: var(--color-text);
    cursor: default;
}

.demo-shell:has(> .demo-video-mode) .select:disabled {
    background-color: var(--color-surface);
    border-color: var(--color-border-strong);
    cursor: default;
}

/* Bigger screens only (Robert's decision 8): below the width the page starts no
   session for, and for a window shrunk after it started, the message replaces the
   grid. The number is a stated assumption. */
.demo-narrow {
    display: none;
    padding: 24px 20px;
    max-width: 640px;
    font-size: 15px;
    line-height: 1.5;
}

@media (max-width: 819px) {
    .demo-shell:not(:has(> .demo-video-mode)) > .demo-narrow {
        display: block;
    }

    .demo-shell:not(:has(> .demo-video-mode)) > :is(.roster-area, .action-panel, .demo-notice, .demo-layer, .demo-veil, .demo-begin),
    .demo-shell:not(:has(> .demo-video-mode)) > .demo-strip .demo-count {
        display: none;
    }
}

/* The Live demo visits page (2026-10-22, /live-demo-visits): the totals on one line, the
   stage table and the excluded addresses kept narrow so the numbers sit beside their
   words, and the add-an-address row. Its own class names throughout, so no other admin
   page's tables or forms move when these do. */
.live-demo-totals {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 22px;
    margin: 10px 0 4px;
}

.live-demo-stages,
.live-demo-excluded {
    width: auto;
    min-width: 520px;
}

.live-demo-stages .live-demo-number,
.live-demo-excluded .live-demo-number {
    text-align: right;
    white-space: nowrap;
}

.live-demo-heading {
    font-size: 13px;
    font-weight: 600;
    margin: 22px 0 6px;
}

.live-demo-excluded-actions {
    white-space: nowrap;
}

.live-demo-excluded-actions .btn + .btn {
    margin-left: 6px;
}

.live-demo-add-address {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    margin: 10px 0 4px;
}

.live-demo-add-address .panel-label {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

.live-demo-address-input {
    width: 260px;
}

.live-demo-attribution {
    margin-top: 22px;
}
