/* ---- Palette ----
   A command console, not a spreadsheet: cool gunmetal surfaces with a signal-amber accent for
   anything that is an action, and status colours that do real work — amber for at risk, green for
   clear, red for a violation, blue for a decision somebody made.

   Every one of these is measured, not chosen by eye. Each text colour clears WCAG AA (4.5:1) on
   the surface it sits on *and* on its own badge tint, in both modes; the lowest ratio anywhere in
   the set is 4.53. Amber never carries white text — the primary button is dark ink on amber in
   both modes, which measures 8.7:1 and is also what a hazard control actually looks like.

   Status colours in light mode sit closer together than a chart palette would tolerate (red and
   amber measure ΔE 10.6 against a floor of 15). That is acceptable *here* and nowhere else: every
   status in this app is a labelled badge — "VS DAILY", "Non-compliant", "Exempted" — so the word
   carries the meaning and the colour only reinforces it. Nothing in this app states a status by
   hue alone, and nothing new should. */
:root {
    --bg: #f7f9fb;
    --surface: #ffffff;
    /* A panel that sits above the page: cards, tiles, the nav. Distinct from --surface so a card
       on a card still reads as two things. */
    --raised: #ffffff;
    --border: #d3dce7;
    --text: #0f1722;
    --muted: #5a6b80;

    /* Interactive text — links, active nav. Dark enough to read on white. */
    --accent: #8a5500;
    /* The same signal at full strength, for fills. Never carries white text; see --accent-ink. */
    --accent-fill: #f0a500;
    --accent-ink: #12161c;

    --pos: #0b6b33;
    --neg: #b42318;
    --info: #1d4ed8;

    --badge-bg: #eef2f7;
    --badge-text: #3c4a5c;

    --shadow: 0 1px 2px rgb(15 23 34 / 0.06), 0 4px 12px rgb(15 23 34 / 0.05);
    --shadow-sm: 0 1px 2px rgb(15 23 34 / 0.07);

    /* So the controls the browser draws for itself — checkboxes, dropdown lists, scrollbars —
       follow the theme too. Without it a dark page still gets a white checkbox, which is exactly
       what the "Show departed" toggle was. */
    color-scheme: light;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #0b0f14;
        --surface: #121821;
        --raised: #18202b;
        --border: #2a3949;
        --text: #e3e9f0;
        --muted: #8fa0b5;

        /* Amber reads directly on a dark surface at 10.9:1, so the link colour and the fill are
           the same hue here — no need for the darkened variant light mode requires. */
        --accent: #f5b942;
        --accent-fill: #f5b942;
        --accent-ink: #12161c;

        --pos: #3ddc97;
        --neg: #ff6b6b;
        --info: #5ab8f7;

        --badge-bg: #1e2733;
        --badge-text: #a9bacd;

        --shadow: 0 1px 2px rgb(0 0 0 / 0.5), 0 6px 20px rgb(0 0 0 / 0.35);
        --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.45);

        color-scheme: dark;
    }
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
    -webkit-font-smoothing: antialiased;
}

/* A cold light from the top of the screen. Two very wide, very faint washes — the accent at 5%
   and the informational blue at 4% — so the page has a direction and a temperature instead of
   being one flat fill. Fixed, so it does not slide up as a long report scrolls. */
body::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background:
        radial-gradient(70rem 30rem at 15% -10%, color-mix(in srgb, var(--accent-fill) 5%, transparent), transparent 70%),
        radial-gradient(60rem 28rem at 90% -5%, color-mix(in srgb, var(--info) 4%, transparent), transparent 70%);
}

main {
    max-width: 1100px;
    margin: 0 auto;
    padding: 2rem 1.25rem 4rem;
}

/* A link inside a sentence — "set them on the admin page". Excludes the nav's own chrome, which
   carries .link/.brand and is styled as navigation rather than as prose. Left underlined: in a
   paragraph, colour alone is not enough to mark a link. */
main a:not(.link):not(.brand) {
    color: var(--accent);
    text-decoration-color: color-mix(in srgb, var(--accent) 45%, transparent);
    text-underline-offset: 0.15em;
}

main a:not(.link):not(.brand):hover {
    text-decoration-color: currentColor;
}

.page-head {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    align-items: flex-end;
    justify-content: space-between;
    margin-bottom: 2rem;
}

.page-head-actions {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

/* The amber rule is the one piece of furniture every page shares. It costs no markup — a
   pseudo-element on the heading — and it is what makes a page read as part of a console rather
   than as a document. */
h1 {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    margin: 0;
    font-size: 1.6rem;
    font-weight: 650;
    letter-spacing: -0.01em;
}

h1::before {
    content: "";
    flex: none;
    width: 4px;
    align-self: stretch;
    min-height: 1.4em;
    border-radius: 2px;
    background: var(--accent-fill);
}

h2 {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 2.5rem 0 0.25rem;
    font-size: 0.95rem;
    font-weight: 650;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
}

/* A page's standfirst. Prose, because seventeen of the nineteen of these are a sentence.
   It used to be display:flex for the sake of the other two, which quietly wrecked any sentence
   with an inline element in it: under flex the surrounding text becomes one anonymous item and
   the <a> becomes another, so the at-risk page's "set them on the admin page" was laid out as a
   narrow column *beside* its own sentence rather than inside it — two words stacked over each
   other with the full stop orphaned after them. Worst on a phone, wrong at every width. */
.sub {
    margin: 0.35rem 0 0;
    color: var(--muted);
    font-size: 0.9rem;
}

/* The two it was really written for: a row of separate facts — a tag beside a date range, an id
   beside a first-seen date — rather than a sentence. Wraps, so a long pair drops to a second line
   instead of holding the header open. */
.sub.facts {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem;
}

.tag {
    padding: 0.1rem 0.45rem;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--surface);
    font-weight: 600;
    color: var(--text);
}

.count {
    padding: 0.05rem 0.4rem;
    border-radius: 999px;
    background: var(--surface);
    border: 1px solid var(--border);
    color: var(--muted);
    font-size: 0.8rem;
    font-weight: 500;
}

.note {
    max-width: 62ch;
    margin: 0 0 0.9rem;
    font-size: 0.85rem;
}

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

.windows {
    display: flex;
    gap: 0.25rem;
}

.win {
    padding: 0.35rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--muted);
    font: inherit;
    font-size: 0.85rem;
    cursor: pointer;
}

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

.win.active {
    background: var(--accent-fill);
    border-color: var(--accent-fill);
    color: var(--accent-ink);
    font-weight: 650;
}

/* Wide tables scroll in their own box; the page itself must never scroll sideways. */
section {
    overflow-x: auto;
}

table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.9rem;
}

th, td {
    padding: 0.55rem 0.7rem;
    text-align: left;
    border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
    white-space: nowrap;
}

/* Figures line up down a column. Deliberately not applied to standalone numbers — a tile's value
   is set at display size, where tabular figures look loose. */
td {
    font-variant-numeric: tabular-nums;
}

th {
    color: var(--muted);
    font-weight: 600;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    border-bottom-color: var(--border);
}

tbody tr:hover {
    background: color-mix(in srgb, var(--accent-fill) 6%, transparent);
}

.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.name {
    font-weight: 500;
}

/* A name cell stacks: the name, then whatever qualifies it. Beside the name these pushed the cell
   wide, and the player column is pinned and tight on a phone — so the qualifier was what forced the
   horizontal scroll the pinning exists to avoid. */
td.name .name-main {
    display: block;
}

.badge.below {
    display: inline-block;
    margin-left: 0;
    margin-top: 0.15rem;
}

td.name .account-handle {
    display: block;
    margin-top: 0.1rem;
    font-size: 0.75rem;
    font-weight: 400;
}

/* What a rule measures, under its name. Deliberately not `.fine`, whose top margin is sized for
   standalone paragraphs and pushes the line off its row inside a cell. */
td.name .rule-note {
    display: block;
    margin-top: 0.15rem;
    font-size: 0.75rem;
    font-weight: 400;
    white-space: normal;
    max-width: 22rem;
}

/* "How this is calculated". Sits between the heading and the report, closed, so it is findable at
   the moment someone doubts a verdict without taking space from the report the rest of the time. */
.help {
    margin: 0 0 1.25rem;
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 0.6rem 0.85rem;
    background: var(--raised);
    box-shadow: var(--shadow-sm);
}

.help > summary {
    cursor: pointer;
    font-size: 0.85rem;
    color: var(--muted);
}

.help[open] > summary {
    margin-bottom: 0.6rem;
}

.help-table {
    font-size: 0.8rem;
}

/* The situation column carries the longer text; letting it wrap keeps the table inside a phone
   rather than pushing the verdict off the edge. */
.help-table td:first-child {
    white-space: normal;
    max-width: 18rem;
}

/* A broken rule, in the at-risk list. A member can break all three at once, so these wrap onto
   their own lines on a narrow screen rather than forcing the row sideways. */
.badge.miss {
    display: inline-block;
    margin: 0.1rem 0.25rem 0.1rem 0;
    white-space: nowrap;
}

.excused {
    margin-top: 2rem;
}

/* Threshold entry. Sized to the numbers it holds — six figures of VS points is the widest
   realistic value — so the row stays narrow enough to read on a phone. */
.threshold-input {
    width: 6.5rem;
    padding: 0.35rem 0.5rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--text);
    font-size: 0.9rem;
    text-align: right;
}

/* Day/month selects plus a lone year number box, in place of the browser's native date input —
   see DateOnlyPicker for why. Each field is sized to its own content rather than a shared width. */
.date-picker {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}

.date-picker select,
.date-picker-year {
    padding: 0.35rem 0.4rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--text);
    font-size: 0.9rem;
}

.date-picker-year {
    width: 4.5rem;
}

.date-picker-clear {
    font-size: 0.8rem;
}

/* The effective-date hint and any scheduled change sit under what they qualify rather than
   beside it, so a narrow column does not push the save button off the row. */
[data-testid="threshold-from-note"],
[data-testid="threshold-upcoming"],
[data-testid="threshold-since"] > span {
    display: block;
}

[data-testid="threshold-upcoming"] {
    color: var(--text);
}

/* Colour is a hint, never the message: every figure carries an explicit +/- sign, so the
   gain/loss distinction survives a colour-blind reader or a black-and-white print. */
.pos { color: var(--pos); }
.neg { color: var(--neg); }
.zero { color: var(--muted); }

/* ---- Scrollable tables ----
   Any wide data table lives in one of these boxes so it scrolls inside itself and the *page* never
   scrolls sideways — the difference between "the header stays put" and "the whole app slides away"
   on a phone. The header row stays put while scrolling down, and the identity column (marked .pin)
   stays put while scrolling right, so you can always tell which row and which column you are reading.

   Sticky cells need border-collapse: separate (with collapse some browsers drop their borders) and an
   opaque background, or the scrolled content shows through them. */
.table-scroll {
    overflow: auto;
    max-height: 75vh;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--raised);
    box-shadow: var(--shadow);
}

.table-scroll table {
    border-collapse: separate;
    border-spacing: 0;
}

.table-scroll thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: color-mix(in srgb, var(--muted) 8%, var(--raised));
}

.table-scroll th.pin,
.table-scroll td.pin {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--raised);
    border-right: 1px solid var(--border);
}

/* The pinned header cell is the corner — above both the sticky row and the sticky column. */
.table-scroll thead th.pin {
    z-index: 3;
    background: color-mix(in srgb, var(--muted) 8%, var(--raised));
}

/* Keep the pinned cell's background in step with the row hover, or it reads as a separate strip. */
.table-scroll tbody tr:hover td.pin {
    background: color-mix(in srgb, var(--accent-fill) 6%, var(--raised));
}

/* A value cell holding more than one figure stacks them rather than running them together on one
   line — a long value is what pushes the compliance badge in the next column off a phone screen. */
.stat-line {
    display: block;
    white-space: nowrap;
}

.badge {
    margin-left: 0.35rem;
    padding: 0.05rem 0.35rem;
    border-radius: 4px;
    background: var(--badge-bg);
    color: var(--badge-text);
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.blazor-error-boundary {
    padding: 1rem;
    background: #b32121;
    color: #fff;
}

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

/* ---- Shell & navigation ---- */

.shell {
    min-height: 100vh;
}

.nav {
    display: flex;
    align-items: center;
    /* Wrap on a narrow screen so the account block (name + Sign out) drops to its own line and stays
       visible, instead of overflowing off the right edge where Sign out could not be found. */
    flex-wrap: wrap;
    gap: 0.6rem 1.25rem;
    padding: 0.7rem 1.25rem;
    border-bottom: 1px solid var(--border);
    background: var(--surface);
    box-shadow: var(--shadow-sm);
}

/* Set like a callsign rather than a wordmark, with the accent square that the page headings
   repeat as their rule. */
.brand {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    font-weight: 700;
    font-size: 0.95rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--text);
    text-decoration: none;
}

.brand::before {
    content: "";
    width: 0.6rem;
    height: 0.6rem;
    border-radius: 2px;
    background: var(--accent-fill);
}

.nav .links {
    display: flex;
    gap: 0.25rem;
    flex: 1;
}

.link {
    padding: 0.35rem 0.7rem;
    border-radius: 6px;
    color: var(--muted);
    text-decoration: none;
    font-size: 0.9rem;
}

.link:hover {
    background: color-mix(in srgb, var(--accent-fill) 12%, transparent);
    color: var(--text);
}

/* NavLink adds this when the route matches. Amber, because where you are is the one piece of
   state the nav carries — and it is the same amber the page's own heading rule uses, so the two
   read as a pair. */
.link.active {
    background: color-mix(in srgb, var(--accent-fill) 16%, transparent);
    box-shadow: inset 0 -2px 0 var(--accent-fill);
    color: var(--text);
    font-weight: 600;
}

.account {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    font-size: 0.85rem;
}

.who {
    color: var(--muted);
    /* A long name (a Discord handle especially) must not crowd out Sign out — cap it and ellipsize. */
    max-width: 11rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Sign out reads as an action, not a stray underlined word that is easy to miss on a phone. */
.signout-btn {
    padding: 0.3rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--text);
    font: inherit;
    font-size: 0.85rem;
    cursor: pointer;
    white-space: nowrap;
}

.signout-btn:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.link-button {
    padding: 0;
    border: 0;
    background: none;
    color: var(--accent);
    font: inherit;
    font-size: 0.85rem;
    cursor: pointer;
    text-decoration: underline;
}

/* ---- Auth pages ---- */

.bare {
    display: grid;
    place-items: center;
    min-height: 100vh;
    padding: 1.25rem;
}

.auth-card {
    width: 100%;
    max-width: 22rem;
    padding: 2rem;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--raised);
    box-shadow: var(--shadow);
}

.auth-card h1 {
    margin-bottom: 0.25rem;
}

.auth-card label {
    display: block;
    margin: 1rem 0 0;
    font-size: 0.85rem;
    color: var(--muted);
}

.auth-card input {
    width: 100%;
    margin-top: 0.3rem;
    padding: 0.5rem 0.6rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--text);
    font: inherit;
    box-sizing: border-box;
}

.primary {
    margin-top: 1.25rem;
    padding: 0.55rem 0.9rem;
    width: 100%;
    border: 0;
    border-radius: 6px;
    background: var(--accent-fill);
    color: var(--accent-ink);
    font: inherit;
    font-weight: 650;
    cursor: pointer;
    box-shadow: var(--shadow-sm);
}

.primary:hover:not(:disabled) {
    background: color-mix(in srgb, var(--accent-fill) 85%, var(--text));
}

.primary:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

/* Everything that is an action but not *the* action: Grant exemption, Mentor, the week arrows,
   Record violation. Every one of these was an unstyled browser button until now — the single
   biggest reason the app read as a wireframe. Outlined rather than filled, so a row of them does
   not compete with the one amber button that matters on the page. */
.secondary {
    padding: 0.35rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--raised);
    color: var(--text);
    font: inherit;
    font-size: 0.85rem;
    font-weight: 500;
    cursor: pointer;
    box-shadow: var(--shadow-sm);
}

.secondary:hover:not(:disabled) {
    border-color: var(--accent-fill);
    background: color-mix(in srgb, var(--accent-fill) 10%, var(--raised));
}

.secondary:disabled {
    opacity: 0.4;
    cursor: not-allowed;
    box-shadow: none;
}

/* A visible focus ring on everything interactive, in the accent — keyboard users get the same
   "where am I" the nav's active pill gives everyone else. */
:is(button, a, select, input, textarea, summary):focus-visible {
    outline: 2px solid var(--accent-fill);
    outline-offset: 2px;
}

.primary.small {
    width: auto;
    margin: 0;
    padding: 0.3rem 0.6rem;
    font-size: 0.8rem;
}

.auth-error {
    padding: 0.5rem 0.7rem;
    border-radius: 6px;
    background: var(--neg);
    color: #fff;
    font-size: 0.85rem;
}

.divider {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin: 1.25rem 0;
    color: var(--muted);
    font-size: 0.8rem;
}

.divider::before,
.divider::after {
    content: "";
    flex: 1;
    border-top: 1px solid var(--border);
}

.discord {
    display: block;
    padding: 0.55rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--text);
    text-align: center;
    text-decoration: none;
    font-weight: 500;
}

.discord:hover {
    border-color: var(--accent);
}

.fine {
    margin-top: 0.75rem;
    font-size: 0.75rem;
}

/* ---- Roster, player, admin ---- */

.search {
    padding: 0.4rem 0.6rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--text);
    font: inherit;
    font-size: 0.85rem;
    min-width: 14rem;
}

tr.clickable {
    cursor: pointer;
}

.badge.ok {
    background: color-mix(in srgb, var(--pos) 15%, transparent);
    color: var(--pos);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--pos) 30%, transparent);
}

.badge.gone {
    background: color-mix(in srgb, var(--muted) 16%, transparent);
    color: var(--muted);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--muted) 25%, transparent);
}

/* A rule somebody missed, which is not the same claim as "not applicable" — and .gone was
   carrying both. The at-risk report marks a miss with .miss as well, so the two can finally look
   different: a miss is amber, the colour this app uses for "your attention is wanted here", while
   a bare .gone stays grey for the N/A it also means. */
.badge.gone.miss {
    background: color-mix(in srgb, var(--accent-fill) 15%, transparent);
    color: var(--accent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-fill) 35%, transparent);
}

.badge.alt {
    background: color-mix(in srgb, var(--info) 15%, transparent);
    color: var(--info);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--info) 30%, transparent);
}

/* Genuinely red, unlike .badge.gone (muted) which softens a missed contribution rule elsewhere on
   purpose. The player summary's compliance column is a check the reader is meant to act on, so it
   says so plainly rather than gently. */
.badge.bad {
    background: color-mix(in srgb, var(--neg) 15%, transparent);
    color: var(--neg);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--neg) 32%, transparent);
}

/* A badge doubling as a button (the summary table's compliance cells) needs the browser's default
   button chrome stripped — .badge already supplies its own background, padding and border. */
button.badge {
    border: none;
    cursor: pointer;
    font-family: inherit;
}

.names {
    margin: 0;
    padding: 0;
    list-style: none;
}

.names li {
    display: flex;
    gap: 0.6rem;
    align-items: baseline;
    padding: 0.35rem 0;
    border-bottom: 1px solid var(--border);
}

/* Something the reader should know before acting — an unset target, a rejected exemption. Carries
   the accent on its leading edge so it reads as a note rather than another panel. */
.notice {
    padding: 0.5rem 0.7rem;
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent-fill);
    border-radius: 6px;
    background: var(--surface);
    font-size: 0.85rem;
}

select {
    padding: 0.3rem 0.4rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--text);
    font: inherit;
    font-size: 0.85rem;
}

code {
    font-size: 0.8rem;
    color: var(--muted);
}

/* ---- Error bar ----
   Hidden until Blazor's JS reveals it. Rewriting app.css dropped the template's display:none and
   the banner sat on every page announcing an error that had not happened — spotted only because a
   Playwright failure dumped the page tree. */
#blazor-error-ui {
    display: none;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1000;
    padding: 0.7rem 1.25rem;
    background: var(--neg);
    color: #fff;
    font-size: 0.85rem;
    box-shadow: 0 -2px 12px rgb(0 0 0 / 0.25);
}

#blazor-error-ui .reload {
    color: #fff;
    text-decoration: underline;
}

#blazor-error-ui .dismiss {
    float: right;
    cursor: pointer;
}

.refresh {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.35rem;
}

/* The throttle warning. Capped in width because it sits in a right-aligned column that would
   otherwise stretch the header, and left-aligned inside that cap because a wrapped sentence is
   harder to read ragged-left. */
.refresh-confirm {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.5rem;
    max-width: 26rem;
    text-align: left;
}

.refresh-confirm-actions {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

/* Header controls: windows/search on the left of a toggle. */
.head-controls {
    display: flex;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
}

.toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.85rem;
    color: var(--muted);
    cursor: pointer;
    white-space: nowrap;
}

.toggle input {
    cursor: pointer;
}

/* Departed rows are shown muted — present for context, but not the focus. */
tr.departed td {
    opacity: 0.6;
}

/* VS entry */
.field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    font-size: 0.85rem;
    color: var(--muted);
}

.field input,
.field select,
.field textarea {
    padding: 0.4rem 0.6rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--text);
    font: inherit;
}

.field textarea {
    resize: vertical;
}

.upload-label {
    display: block;
    margin-bottom: 0.35rem;
    font-size: 0.85rem;
    color: var(--muted);
}

.grid-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
}

.week-picker {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-bottom: 1.25rem;
    flex-wrap: wrap;
}

/* Day entry opens as a focused overlay over the (dimmed) week grid. */
.overlay-backdrop {
    position: fixed;
    inset: 0;
    z-index: 200;
    background: rgba(0, 0, 0, 0.55);
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: 2.5rem 1rem;
    overflow-y: auto;
}

.overlay-panel {
    width: min(760px, 100%);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
    padding: 1.5rem 1.75rem;
}

.week-label {
    font-weight: 600;
}

/* The player page carries one of these per kind of contribution, so they need to read as separate
   blocks rather than one long run of rows. */
.contrib {
    margin-top: 1.5rem;
}

.contrib h3 {
    margin-bottom: 0.35rem;
}

.contrib tfoot td {
    border-top: 2px solid var(--border);
    font-weight: 600;
}

/* A kind entered once a week has a figure rather than a table. Give it the weight the total row
   in the daily tables carries, so the two read as the same kind of statement. */
.weekly-figure .num {
    font-size: 1.35rem;
    font-weight: 600;
    margin-right: 0.5rem;
}

.week-stat {
    margin: -0.5rem 0 0.75rem;
    font-size: 0.85rem;
}

/* The VS week grid (inside a .table-scroll box) pins two columns — rank and player — so a player's
   identity stays visible while the six days scroll. Keep them tight: every extra pixel of padding or
   width here is a pixel of scores pushed off the screen, which is what made the scrolling painful. */
table.grid.week th.col-rank,
table.grid.week td.col-rank,
table.grid.week th.col-player,
table.grid.week td.col-player {
    position: sticky;
    z-index: 1;
    background: var(--bg);
    padding-left: 0.5rem;
    padding-right: 0.5rem;
}

table.grid.week th.col-rank,
table.grid.week td.col-rank {
    left: 0;
    box-sizing: border-box;
    width: 2.25rem;
    min-width: 2.25rem;
    text-align: right;
}

table.grid.week th.col-player,
table.grid.week td.col-player {
    left: 2.25rem;
    border-right: 1px solid var(--border);
}

/* The two pinned header cells are the corner — above both the sticky row and the sticky columns. */
table.grid.week thead th.col-rank,
table.grid.week thead th.col-player {
    z-index: 3;
    background: var(--surface);
}

table.grid.week tbody tr:hover td.col-rank,
table.grid.week tbody tr:hover td.col-player {
    background: var(--surface);
}

table.grid.week th.day {
    padding: 0;
}

.day-open {
    display: block;
    width: 100%;
    padding: 0.5rem 0.6rem;
    background: none;
    border: none;
    border-radius: 6px;
    color: var(--text);
    font: inherit;
    text-align: right;
    cursor: pointer;
}

.day-open:hover {
    background: var(--border);
}

table.grid.week td.total {
    font-weight: 600;
}

table.grid tr.add select {
    color: var(--muted);
}

/* The editable entry grid: each row's player and points are edited inline. */
table.grid select,
table.grid input.points {
    width: 100%;
    box-sizing: border-box;
    padding: 0.4rem 0.6rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--text);
    font: inherit;
}

table.grid td {
    vertical-align: middle;
}

/* The editable entry grid (Donations, and the VS day overlay) pins its # and Player columns too, so
   the person being scored stays visible while the value/remove columns scroll on a narrow screen.
   Same tightening as the week grid above, and for the same reason: every pixel of padding here is a
   pixel of the score input pushed off a phone.

   The rank column needs its width fixed, not merely pinned. Pinned columns are positioned with a
   hard-coded `left`, so the name column's offset has to equal the rank column's actual width — and
   while that width was left to the content, the two disagreed: a two-digit rank overlapped the
   name, a one-digit rank left a gap. Fixing the width is what makes the offset below correct. */
.table-scroll table.grid th.pin-num,
.table-scroll table.grid td.pin-num,
.table-scroll table.grid th.pin-name,
.table-scroll table.grid td.pin-name {
    position: sticky;
    z-index: 1;
    background: var(--bg);
    padding-left: 0.5rem;
    padding-right: 0.5rem;
}

.table-scroll table.grid th.pin-num,
.table-scroll table.grid td.pin-num {
    left: 0;
    box-sizing: border-box;
    width: 2.25rem;
    min-width: 2.25rem;
    text-align: right;
}

.table-scroll table.grid th.pin-name,
.table-scroll table.grid td.pin-name {
    left: 2.25rem;
    border-right: 1px solid var(--border);
}

.table-scroll table.grid thead th.pin-num,
.table-scroll table.grid thead th.pin-name {
    z-index: 3;
    background: var(--surface);
}

.table-scroll table.grid tbody tr:hover td.pin-num,
.table-scroll table.grid tbody tr:hover td.pin-name {
    background: var(--surface);
}

table.grid tr.empty td {
    text-align: center;
    padding: 1.25rem;
}

/* ---- Type-to-search player picker ---- */

.picker {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.25rem;
    min-width: 12rem;
}

.picker-input {
    flex: 1;
    min-width: 0;
    box-sizing: border-box;
    padding: 0.4rem 0.6rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--text);
    font: inherit;
}

.picker-clear {
    padding: 0.2rem 0.4rem;
    border: none;
    border-radius: 4px;
    background: none;
    color: var(--muted);
    font: inherit;
    cursor: pointer;
}

.picker-clear:hover {
    background: var(--border);
    color: var(--text);
}

/* Absolute, so opening the list does not reflow the table row it sits in — and above the sticky
   pinned columns, which otherwise paint over it. */
.picker-list {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 10;
    max-height: 14rem;
    overflow-y: auto;
    margin: 0.15rem 0 0;
    padding: 0.25rem;
    list-style: none;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    box-shadow: 0 6px 18px rgb(0 0 0 / 18%);
}

.picker-option {
    display: block;
    width: 100%;
    padding: 0.35rem 0.5rem;
    border: none;
    border-radius: 4px;
    background: none;
    color: var(--text);
    font: inherit;
    text-align: left;
    white-space: normal;
    cursor: pointer;
}

.picker-option:hover {
    background: var(--border);
}

.picker-empty {
    padding: 0.35rem 0.5rem;
    font-size: 0.85rem;
}

.actions .spacer {
    flex: 1;
}

.paste {
    width: 100%;
    box-sizing: border-box;
    padding: 0.7rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg);
    color: var(--text);
    font-family: ui-monospace, "Cascadia Code", Consolas, monospace;
    font-size: 0.9rem;
    resize: vertical;
}

.vs-day {
    margin-bottom: 2rem;
}

.vs-day h2 {
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
}

.vs-day .total {
    font-size: 0.85rem;
    font-weight: 400;
}

td.rank,
th.rank {
    width: 2.5rem;
    text-align: right;
}

.ocr {
    margin-top: 1.5rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--border);
}

.raw-ocr {
    margin: 0.5rem 0 1rem;
    font-size: 0.85rem;
}

.raw-ocr summary {
    cursor: pointer;
    color: var(--muted);
}

.raw-ocr pre {
    margin-top: 0.5rem;
    padding: 0.7rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg);
    overflow-x: auto;
    white-space: pre-wrap;
    word-break: break-word;
}

.ocr .upload {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-top: 0.5rem;
}

.ocr .upload.busy {
    opacity: 0.6;
}

.actions {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-top: 1rem;
}

.actions .primary {
    width: auto;
    margin: 0;
}

.points {
    width: 7rem;
    padding: 0.3rem 0.4rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--text);
    font: inherit;
    text-align: right;
}

/* ---- At-risk filters & sortable columns ---- */

.filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem 1.5rem;
    margin: 0.75rem 0;
}

.filter-group {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
}

.filter-label {
    color: var(--muted);
    font-size: 0.8rem;
    margin-right: 0.15rem;
}

.chip {
    padding: 0.2rem 0.6rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg);
    color: var(--text);
    font: inherit;
    font-size: 0.8rem;
    cursor: pointer;
}

.chip {
    background: var(--raised);
}

.chip:hover {
    border-color: var(--accent-fill);
}

.chip.active {
    border-color: var(--accent-fill);
    background: var(--accent-fill);
    color: var(--accent-ink);
    font-weight: 600;
}

th.sortable {
    cursor: pointer;
    user-select: none;
}

th.sortable:hover {
    color: var(--accent);
}

.sort-indicator {
    display: inline-block;
    width: 0.8em;
    font-size: 0.75em;
    color: var(--accent);
}

/* ---- Mentor ---- */

/* Wraps the name, the control that changes it, and anything the control has to say when a change
   fails. Column rather than row: in the at-risk table this sits in a cell alongside two or three
   badges, and an error laid out beside a dropdown would push the whole row wider than the phone
   it is being read on. */
.mentor {
    display: inline-flex;
    flex-direction: column;
    gap: 0.25rem;
    align-items: flex-start;
}

.mentor select {
    max-width: 12rem;
}

.mentor-name {
    font-size: 0.85rem;
    white-space: nowrap;
}

/* ---- Phones ----
   One breakpoint, set where the at-risk table stops fitting rather than at a round number off a
   device chart. Everything above it keeps the layout the rest of this file describes.

   The rule this encodes: a table whose rows are *records* stops being a table down here and becomes
   one card per row, because a column the reader cannot see is a feature they do not know exists.
   A table whose rows are a *matrix* — the VS and donations grids, where the point is comparing the
   same day down a column — keeps scrolling sideways in its box, which is the right answer there and
   the wrong one for a list of people with two buttons each. */
/* ---- Stat tiles ----
   The headline figures a report leads with. Deliberately not a chart: these are four single
   current values, and a bar chart of four unrelated numbers is a chart of nothing.

   The status colour rides on a rule down the left edge, never on the figure. A number is text and
   text wears text colours; the colour beside it carries the state. Each tile also names its state
   in words, so the hue reinforces the label rather than replacing it — the same rule the badges
   follow, and the reason this palette's light-mode amber and red are allowed to sit as close as
   they do.

   Proportional figures, not tabular: at this size `tabular-nums` gives every digit the width of a
   zero and a number like 121 goes visibly loose. Tabular is for columns, which is where the table
   rules apply it. */
.tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
    gap: 0.75rem;
    margin: 0 0 1.5rem;
}

.tile {
    position: relative;
    overflow: hidden;
    padding: 0.8rem 0.9rem 0.85rem 1.1rem;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--raised);
    box-shadow: var(--shadow-sm);
}

.tile::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: 3px;
    background: var(--muted);
}

.tile.warn::before { background: var(--accent-fill); }
.tile.good::before { background: var(--pos); }
.tile.bad::before { background: var(--neg); }
.tile.info::before { background: var(--info); }

.tile-label {
    display: block;
    color: var(--muted);
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

.tile-value {
    display: block;
    margin-top: 0.3rem;
    font-size: 1.9rem;
    font-weight: 650;
    line-height: 1.05;
    letter-spacing: -0.02em;
}

.tile-note {
    display: block;
    margin-top: 0.15rem;
    color: var(--muted);
    font-size: 0.78rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

@media (max-width: 44rem) {

    /* The one thing making the *page* slide sideways rather than a table inside it: .nav wraps, but
       the link list inside it did not, so seven links held one 455px line open on a 390px screen.

       Wrapping alone is not enough. The list is a flex item sharing a line with the brand and the
       account block, so it wraps inside whatever narrow column is left over — seven links, seven
       lines, a third of the screen gone before the page starts. Taking the whole width puts it
       under them, where the links flow across two comfortable lines. */
    .nav .links {
        flex-wrap: wrap;
        flex-basis: 100%;
    }

    /* Un-box the scroller. Leaving it would nest a scrolling region inside the page's own scroll —
       and cap the cards at 75vh, which on a phone is most of what there is. */
    .stacks {
        overflow: visible;
        max-height: none;
        border: 0;
        border-radius: 0;
    }

    .stacks table,
    .stacks tbody,
    .stacks tr,
    .stacks td {
        display: block;
        width: auto;
    }

    .stacks tr {
        margin-bottom: 0.75rem;
        padding: 0.6rem 0.75rem;
        border: 1px solid var(--border);
        border-radius: 8px;
    }

    .stacks td {
        padding: 0.15rem 0;
        border: 0;
    }

    /* The name led every row and now heads its card. Un-pinned: there is nothing to stay put
       relative to once the row is not a row. */
    .stacks td.pin,
    .stacks tbody tr:hover td.pin {
        position: static;
        padding-bottom: 0.4rem;
        border-right: 0;
        background: none;
        font-size: 1.05rem;
        font-weight: 600;
    }

    /* Each remaining cell says what it is, since the header is no longer above it to explain. */
    .stacks td[data-label]::before {
        content: attr(data-label);
        display: block;
        color: var(--muted);
        font-size: 0.75rem;
        letter-spacing: 0.03em;
        text-transform: uppercase;
    }

    /* The header row is repurposed, not dropped: its sortable cells were always buttons, so down
       here they read as the sort control they are. The rest only labelled columns that now label
       themselves. A header with nothing sortable in it has no reason to be here at all. */
    .stacks thead:not(:has(th.sortable)) {
        display: none;
    }

    .stacks thead,
    .stacks thead tr {
        display: block;
    }

    .stacks thead tr {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 0.4rem;
        margin-bottom: 0.75rem;
        padding: 0;
        border: 0;
    }

    .stacks thead tr::before {
        content: "Sort";
        color: var(--muted);
        font-size: 0.75rem;
        text-transform: uppercase;
    }

    .stacks thead th {
        position: static;
        display: inline-block;
        padding: 0.2rem 0.6rem;
        border: 1px solid var(--border);
        border-radius: 999px;
        font-size: 0.8rem;
        white-space: nowrap;
    }

    .stacks thead th:not(.sortable) {
        display: none;
    }

    /* Full-width controls: a thumb-sized target, and an unambiguous end to the card. */
    .stacks .card-actions {
        padding-top: 0.5rem;
    }

    .stacks .card-actions button:not(.link-button) {
        width: 100%;
    }

    .stacks .mentor {
        display: flex;
        align-items: stretch;
        width: 100%;
    }

    .stacks .mentor select {
        max-width: none;
    }

    /* Not a card — a label/value/state table reads fine narrow. It only has to stop sizing itself
       to its content inside a scrolling box, which is what pushed the state column, the whole point
       of the row, off the right-hand edge. */
    .summary-table {
        width: 100%;
    }

    /* Two up rather than four across a 390px screen, where auto-fit would give each tile 87px and
       a three-digit figure would wrap under its own label. */
    .tiles {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 0.5rem;
    }

    .tile-value {
        font-size: 1.55rem;
    }
}
