/* =============================================================================
   Tournament display board — /Display
   =============================================================================

   A clubhouse TV. 1920x1080, viewed from 3-5 metres, nobody touching it after
   the code goes in. That single fact drives every decision below.

   DESIGN DIRECTION — the manual leaderboard.
   The artifact this screen descends from is the hand-set scoreboard behind the
   18th green: a painted board with numbered plates dropped into channels.

   The plate is used SPARINGLY, on exactly two things: the side totals and the
   pairing code. Both are single headline numbers with room around them. It is
   deliberately not applied to the individual standings table — five session
   columns across twenty rows would be a hundred boxes, and that reads as a
   spreadsheet grid rather than as a leaderboard. Those rows sit flat on the
   board face and take their hierarchy from size and weight instead.

   Where the plate IS used it carries meaning rather than decorating: a side
   that has not played shows an empty channel — the slot at full size with no
   plate seated in it. On the individual table, where there is no plate to
   remove, an unplayed cell is a dimmed dash and nothing more. Two treatments,
   one rule: not played is not zero, and a genuine 0 keeps full ink and full
   weight either way.

   Everything else is deliberately quiet. One accent (flag amber), used on the
   leader and on states the room must notice; no gradients; no ambient motion.

   SCOPE. This repo has no dark mode — no prefers-color-scheme, no data-theme,
   nowhere. So the board carries its own palette, declared on .db-root and
   nowhere else. Nothing here leaks to the rest of the app and nothing here
   redeclares a design-system global; every selector is db-prefixed.

   TYPE. No new webfont. An unattended TV on clubhouse wifi that fails a font
   request shows a board in fallback for the rest of the day, so the board takes
   its character from weight, width, scale and the plate treatment instead of
   from a novel family. Three voices out of two families already loaded:
     - names and headlines: Plus Jakarta Sans, 700-800
     - stencil labels:      Jakarta 600, uppercase, wide tracking, dim
     - counters and code:   the system mono stack

   SIZE. Body text never below 28px at 1080p; every size is a clamp() against
   vw so a 4K panel scales instead of stranding the board in one corner.

   DIGITS. Scores change under a 60-second poll. Every numeric cell is
   tabular-nums so a 9 becoming a 10 does not shift the column.
   ========================================================================== */

.db-root {
    /* --- palette -------------------------------------------------------- */
    /* Built out from the app's existing dark vocabulary: --surface-primary
       (#1A2332) becomes the board face, --text-on-dark (#E8E6E1) the ink. */

    --db-bg: #0E1620;           /* the room — deeper than the board, so the board reads as lit */
    --db-board: #1A2332;        /* the board face */
    --db-channel: #223142;      /* a seated plate */
    --db-channel-edge: rgba(232, 230, 225, 0.14);
    --db-rule: rgba(232, 230, 225, 0.10);

    --db-ink: #E8E6E1;          /* 12.7:1 on the board */
    --db-ink-dim: #8B9BAE;      /* stencil labels, secondary data — 5.6:1 */

    /* Empty channels, "Not played", the disabled Pair label. Recessive, but it
       carries WORDS, so it clears 3:1 for large text on BOTH backgrounds it is
       used against: 4.2:1 on the board, 3.6:1 on a channel. The first draft was
       #5C6B7D — 2.9:1 and 2.4:1, under the bar on both. Same argument that
       brightened the app green below; it should have been applied here first
       time. */
    --db-ink-faint: #75869B;

    /* The app green is #2D7D46. Against #1A2332 that lands near 2.3:1, which is
       fine on a laptop and illegible at four metres. Brightened for the medium;
       same hue, same intent. */
    --db-green: #3E9C5C;

    /* Golf's other real colour — the flagstick. The single accent: leader,
       FINAL, and the states the room needs to notice. */
    --db-flag: #E3A93C;

    /* --- type ----------------------------------------------------------- */
    --db-font: 'Plus Jakarta Sans Variable', 'Helvetica Neue', Helvetica, Arial, sans-serif;
    --db-font-mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;

    --db-size-title: clamp(34px, 2.8vw, 62px);
    --db-size-panel: clamp(24px, 1.5vw, 34px);   /* stencil labels */
    --db-size-body: clamp(28px, 1.9vw, 44px);    /* the floor — never go under this */
    --db-size-lead: clamp(32px, 2.2vw, 50px);
    --db-size-side: clamp(40px, 3.4vw, 76px);
    --db-size-side-total: clamp(48px, 4.6vw, 100px);

    --db-gap: clamp(16px, 1.4vw, 32px);
    --db-pad: clamp(24px, 2.2vw, 52px);

    /* Fixed rather than flowed: EmptyLayout renders nothing but @Body, so the
       board would otherwise inherit the app's `body { padding-bottom: 12px }`
       and sit 12px short of the bottom bezel. Escaping the document flow keeps
       that fix here instead of in app.css. */
    position: fixed;
    inset: 0;
    overflow: hidden;

    display: flex;
    flex-direction: column;

    background-color: var(--db-bg);
    color: var(--db-ink);
    font-family: var(--db-font);
    font-size: var(--db-size-body);
    line-height: 1.25;

    /* Nothing on the BOARD is selectable by design — there is no pointer in the
       room, and a stray highlight from a cleaning cloth on a touch panel would
       stay up until someone noticed. The code input opts back in below; this
       inherits, and WebKit honours it on form fields, which would have blocked
       select-and-retype on the one element anyone actually touches. */
    -webkit-user-select: none;
    user-select: none;
}

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

/* --- app.css bleed-through ---------------------------------------------------
   app.css styles bare `table`, `th, td` and `tr:nth-child(even)` with no class
   scope, so those rules reach every table in the application — this board
   included. The striping rule is the dangerous one: `tr:nth-child(even)` is
   specificity (0,1,1), which OUTRANKS every single-class row selector in this
   file (0,1,0). Without the reset below, every even row paints #f2f2f2 and this
   board's light ink lands near-white on near-white — the second team, the second
   player, every alternate row, silently unreadable. That is what shipped, and no
   test in this repo can see it: bUnit has no cascade.

   Neutralised here rather than in app.css, which every other page depends on.

   The specificities are deliberate, not incidental:
   - `:where(...)` contributes nothing, so the table/cell resets sit at (0,1,0) —
     enough to beat the bare element rules, but a plain tie with this file's own
     `.db-*` selectors. They are declared FIRST so the board's rules, coming
     later, win the tie. Do not move them below the component sections.
   - The striping reset needs real weight to clear (0,1,1), so it is written
     without `:where` at (0,2,1). Nothing in this file targets even rows, so it
     cannot clobber a board rule.
   ------------------------------------------------------------------------- */

.db-root :where(table) {
    border: 0;
}

.db-root :where(th, td) {
    padding: 0;
    border: 0;
    text-align: left;
}

.db-root tr:nth-child(even) {
    background-color: transparent;
}


/* =============================================================================
   Pairing — the only screen anyone interacts with
   ========================================================================== */

.db-pairing {
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--db-gap);
    padding: var(--db-pad);
    text-align: center;
}

.db-pairing__title {
    margin: 0;
    font-size: var(--db-size-title);
    font-weight: 800;
    letter-spacing: -0.02em;
}

.db-pairing__instructions {
    margin: 0;
    max-width: 24ch;
    color: var(--db-ink-dim);
    font-size: var(--db-size-body);
    font-weight: 500;
}

/* The code box is the one plate on this screen — same channel treatment as a
   score, so the board's vocabulary is established before the board appears. */
.db-pairing__code {
    width: 8ch;
    padding: clamp(12px, 1vw, 24px) clamp(16px, 1.4vw, 32px);

    background-color: var(--db-channel);
    border: 2px solid var(--db-channel-edge);
    border-radius: 12px;
    box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.05),
                inset 0 -2px 0 rgba(0, 0, 0, 0.35);

    color: var(--db-ink);
    font-family: var(--db-font-mono);
    font-size: clamp(56px, 6.5vw, 140px);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.12em;
    text-align: center;

    /* Two jobs. It tells the viewer case does not matter before they find out
       the hard way — and it makes a known rendering artefact invisible. Blazor
       diffs the render tree, not the DOM, so pasting a code that differs from
       the model only in case leaves the box showing what was typed while the
       redeemed value is already normalised. Uppercasing in CSS means the two
       agree on screen. No @key, no interop, no behaviour change. */
    text-transform: uppercase;

    /* Opts back out of the board's blanket user-select: none, which this would
       otherwise inherit. WebKit honours that on form fields, so a viewer could
       not select-and-retype a half-entered code — on the only element on this
       entire surface a human touches. */
    -webkit-user-select: text;
    user-select: text;
}

.db-pairing__code:focus {
    outline: none;
    border-color: var(--db-green);
    box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.05),
                inset 0 -2px 0 rgba(0, 0, 0, 0.35),
                0 0 0 5px rgba(62, 156, 92, 0.28);
}

.db-pairing__submit {
    padding: clamp(14px, 1.1vw, 26px) clamp(44px, 4vw, 88px);

    background-color: var(--db-green);
    border: none;
    border-radius: 999px;

    color: #0B1219;
    font-family: inherit;
    font-size: var(--db-size-body);
    font-weight: 800;
    letter-spacing: 0.01em;
    cursor: pointer;

    transition: background-color 140ms ease, opacity 140ms ease;
}

.db-pairing__submit:hover:not(:disabled) {
    background-color: #4CB56B;
}

.db-pairing__submit:focus-visible {
    outline: 4px solid var(--db-ink);
    outline-offset: 4px;
}

.db-pairing__submit:disabled {
    background-color: var(--db-channel);
    color: var(--db-ink-faint);
    cursor: default;
}

.db-pairing__busy {
    color: var(--db-ink-dim);
}

/* Errors state what happened and stay in the board's voice. Amber, not red:
   nothing here is destructive, and red on a TV in a bar reads as an alarm. */
.db-pairing__error {
    margin: 0;
    max-width: 30ch;
    padding: clamp(12px, 1vw, 22px) clamp(20px, 1.6vw, 36px);

    background-color: rgba(227, 169, 60, 0.10);
    border-left: 6px solid var(--db-flag);
    border-radius: 6px;

    color: var(--db-flag);
    font-size: var(--db-size-body);
    font-weight: 600;
}


/* =============================================================================
   Shell states — ended, loading, retry
   ========================================================================== */

.db-ended,
.db-loading,
.db-retry {
    margin: 0;
    padding: var(--db-pad);
    text-align: center;
}

/* Sits above the code box rather than replacing it: the remedy for a revoked
   display is a new code, so this has to read as context for the box below, not
   as the screen's headline. */
.db-ended {
    margin: 0 auto;
    max-width: 40ch;
    color: var(--db-ink-dim);
    font-size: var(--db-size-body);
    font-weight: 600;
}

/* The notice and the code box are one group and must centre as one. Auto
   margins on the outermost two rather than `margin: auto` on each: that would
   push the whole viewport's slack between them and strand the notice at the top
   of a 1080p screen, metres from the box it is explaining. The pair of
   selectors covers both arrangements — notice present, and pairing alone. */
.db-root > .db-ended:first-child,
.db-root > .db-pairing:first-child {
    margin-top: auto;
}

.db-root > .db-pairing:last-child {
    margin-bottom: auto;
}

.db-loading,
.db-retry {
    margin: auto;
    color: var(--db-ink-dim);
}

.db-retry__message {
    margin: 0 0 0.4em;
    color: var(--db-ink);
    font-size: var(--db-size-lead);
    font-weight: 700;
}

.db-retry__detail {
    margin: 0;
    font-size: var(--db-size-body);
    font-weight: 500;
}


/* =============================================================================
   Start display — the one control on a paired screen
   =============================================================================

   Fullscreen is refused without a user gesture, so this button exists because of
   a browser rule rather than a product decision. It is for the person standing
   at the TV during setup and for nobody afterwards, which settles both its
   position and its weight: pinned to a corner so it never displaces a score, and
   in the stencil voice at panel size rather than in the pairing screen's green —
   that green is the primary action on a screen with one thing to do, and reusing
   it here would put a second focal point on a board being read from 4 metres.

   It leaves the screen on the first click and does not come back. */

.db-start {
    position: absolute;
    right: var(--db-pad);
    bottom: var(--db-pad);
    z-index: 1;

    padding: clamp(10px, 0.8vw, 18px) clamp(22px, 1.8vw, 40px);

    background-color: var(--db-channel);
    border: 2px solid var(--db-channel-edge);
    border-radius: 999px;

    color: var(--db-ink);
    font-family: inherit;
    font-size: var(--db-size-panel);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    cursor: pointer;

    transition: background-color 140ms ease, color 140ms ease;
}

.db-start:hover {
    background-color: var(--db-green);
    border-color: var(--db-green);
    color: #0B1219;
}

/* Setup happens on a TV remote as often as on a mouse, so the focused state has
   to be visible from across the room rather than a hairline ring. */
.db-start:focus-visible {
    outline: 4px solid var(--db-ink);
    outline-offset: 4px;
}


/* =============================================================================
   Unpair — the way back to the code box
   =============================================================================

   The one control that stays on a running board, and the only one on this
   surface with a consequence behind it. Its two requirements pull against each
   other and the styling is where that gets settled:

   FINDABLE. An organiser setting the same screen up for the next tournament has
   to be able to spot it without being told. So it is a real word in the corner
   opposite Start display, not a hover target and not an icon.

   NOT SPENDABLE BY ACCIDENT. It also has to lose to the scores. At rest it is
   the quietest thing on the board — faint ink, no fill, no border, panel-label
   voice — and it carries the smallest type here for the same reason. Everything
   heavier lives one click deeper, in the prompt, where the person reading it has
   already declared intent. The confirm step itself is in the component; the job
   of this block is to make sure the resting state never *looks* like a button
   worth pressing to somebody walking past.

   Bottom-left, mirroring Start display bottom-right: the two coexist on a paired
   screen that has not been started yet, and neither may displace a score. */

.db-unpair {
    position: absolute;
    left: var(--db-pad);
    bottom: var(--db-pad);
    z-index: 1;
}

.db-unpair__trigger {
    padding: clamp(6px, 0.5vw, 12px) clamp(10px, 0.9vw, 20px);

    background: none;
    border: none;

    color: var(--db-ink-faint);
    font-family: inherit;

    /* Deliberately under the board's body floor. That floor exists so scores are
       legible from four metres; this is the one element that should NOT be, and
       it is a word rather than data. Still comfortably readable at arm's length,
       which is where the person who wants it is standing. */
    font-size: clamp(16px, 1.1vw, 24px);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    cursor: pointer;

    transition: color 140ms ease;
}

.db-unpair__trigger:hover {
    color: var(--db-ink);
}

.db-unpair__trigger:focus-visible,
.db-unpair__cancel:focus-visible,
.db-unpair__go:focus-visible {
    outline: 4px solid var(--db-ink);
    outline-offset: 4px;
}

/* One click deeper, and now it may take room — this is the state where the
   consequence has to be legible before anyone commits to it. Sits on the board
   face rather than the room so it reads as lifted off the background. */
.db-unpair__prompt {
    max-width: 34ch;
    padding: clamp(16px, 1.2vw, 28px);

    background-color: var(--db-board);
    border: 2px solid var(--db-channel-edge);
    border-radius: 12px;

    display: flex;
    flex-direction: column;
    gap: clamp(12px, 1vw, 20px);
}

.db-unpair__question {
    margin: 0;
    color: var(--db-ink);
    font-size: var(--db-size-panel);
    font-weight: 600;
    line-height: 1.3;
}

.db-unpair__actions {
    display: flex;
    gap: clamp(10px, 0.8vw, 18px);
}

.db-unpair__cancel,
.db-unpair__go {
    padding: clamp(8px, 0.7vw, 16px) clamp(18px, 1.4vw, 32px);

    border-radius: 999px;

    font-family: inherit;
    font-size: var(--db-size-panel);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    cursor: pointer;
}

/* Cancel carries the weight, not Unpair. On a screen where the expensive
   mistake is unpairing a live tournament, the safe answer is the one that should
   be easiest to hit and easiest to see. */
.db-unpair__cancel {
    background-color: var(--db-channel);
    border: 2px solid var(--db-channel-edge);
    color: var(--db-ink);
}

.db-unpair__cancel:hover {
    background-color: var(--db-green);
    border-color: var(--db-green);
    color: #0B1219;
}

.db-unpair__go {
    background: none;
    border: 2px solid var(--db-ink-faint);
    color: var(--db-ink-faint);

    transition: border-color 140ms ease, color 140ms ease;
}

/* Amber, not red: the board has one accent and adding a second for a control
   nobody should be looking at would be the loudest thing on a scoreboard. */
.db-unpair__go:hover {
    border-color: var(--db-flag);
    color: var(--db-flag);
}


/* =============================================================================
   Board frame
   ========================================================================== */

.db-board {
    flex: 1;
    min-height: 0;             /* lets the rotator scroll-clip instead of pushing the frame */
    display: flex;
    flex-direction: column;
    padding: var(--db-pad);
    gap: var(--db-gap);
}

.db-board__title {
    margin: 0;
    padding-bottom: calc(var(--db-gap) * 0.6);
    border-bottom: 3px solid var(--db-rule);

    font-size: var(--db-size-title);
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.1;

    /* A long tournament name must not eat the board. One line, then ellipsis. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The room needs to know the numbers may be old — but a TV nobody can reach
   should not blink at them about it. Static amber strip, no pulse. */
.db-board__stale {
    margin: 0;
    padding: clamp(10px, 0.8vw, 18px) clamp(18px, 1.4vw, 30px);
    align-self: flex-start;

    background-color: rgba(227, 169, 60, 0.10);
    border-left: 6px solid var(--db-flag);
    border-radius: 6px;

    color: var(--db-flag);
    font-size: var(--db-size-panel);
    font-weight: 700;
}

.db-board__body {
    flex: 1;
    min-height: 0;
    display: flex;
}

/* Publishing — the publish worker is still materializing sessions. Sits where
   the rotator would, in the board frame rather than with the shell states
   above, because the tournament name is already up and this is a line about a
   board that exists. Quiet and centred: nobody is going to dismiss it, and it
   clears itself on a poll. */
.db-setup {
    margin: auto;
    text-align: center;
    color: var(--db-ink-dim);
}

.db-setup__message {
    margin: 0 0 0.4em;
    color: var(--db-ink);
    font-size: var(--db-size-lead);
    font-weight: 700;
}

.db-setup__detail {
    margin: 0;
    font-size: var(--db-size-body);
    font-weight: 500;
}


/* =============================================================================
   Rotator
   ========================================================================== */

.db-rotator {
    flex: 1;
    min-height: 0;
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: var(--db-gap);
}

.db-rotator__empty {
    margin: auto;
    color: var(--db-ink-dim);
    font-size: var(--db-size-lead);
    font-weight: 600;
}

.db-rotator__panel {
    flex: 1;
    min-height: 0;
    display: flex;

    /* BROWSER-QA ONLY. This animation is the reason the panel carries @key in
       BoardRotator.razor — without the key Blazor reuses one element and swaps
       the content underneath, so a panel would inherit the previous panel's
       finished animation and never play its own. bUnit has no CSS animation
       engine and cannot observe this; no test protects it, and none can. */
    animation-name: db-panel-in;
    animation-duration: 420ms;
    animation-timing-function: cubic-bezier(0.22, 0.7, 0.3, 1);
    animation-fill-mode: both;
}

@keyframes db-panel-in {
    from {
        opacity: 0;
        transform: translateY(18px);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

.db-rotator__progress {
    height: 8px;
    border-radius: 999px;
    background-color: var(--db-rule);
    overflow: hidden;
}

.db-rotator__progress-fill {
    height: 100%;
    border-radius: inherit;
    background-color: var(--db-green);
    transform-origin: left center;

    /* Longhands, never the `animation` shorthand: BoardRotator sets
       animation-duration inline from the dwell, and a shorthand here would be
       fighting an inline declaration for no reason.

       scaleX rather than width so an unattended board animates on the
       compositor and never lays out — this runs every dwell, all day.

       BROWSER-QA ONLY, same as the panel above: the restart depends on @key
       replacing the element each rotation. bUnit cannot observe an animation
       restarting. */
    animation-name: db-progress-fill;
    animation-timing-function: linear;
    animation-fill-mode: forwards;
    animation-iteration-count: 1;
}

@keyframes db-progress-fill {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
}

.db-rotator__dots {
    display: flex;
    justify-content: center;
    gap: clamp(10px, 0.8vw, 18px);
}

.db-rotator__dot {
    width: clamp(10px, 0.7vw, 16px);
    height: clamp(10px, 0.7vw, 16px);
    border-radius: 50%;
    background-color: var(--db-ink-faint);
    transition: background-color 220ms ease, transform 220ms ease;
}

.db-rotator__dot--active {
    background-color: var(--db-ink);
    transform: scale(1.35);
}


/* =============================================================================
   Panel chrome — shared by all three panels
   ========================================================================== */

.db-panel {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: var(--db-gap);

    padding: var(--db-pad);
    background-color: var(--db-board);
    border-radius: clamp(12px, 1vw, 22px);

    /* Clipped, not scrolled. There is no one in the room to scroll a TV, so an
       overlong panel must be cut at the frame rather than hide rows below a
       fold nobody can reach.

       This is a backstop, not the row policy — and it used to be the row policy
       by accident, which silently cost a 30-player board its tail. What actually
       handles row count is StandingsPanel.RowsPerPage: the panel renders at most
       one page of the field and DisplayBoard puts one panel per page into the
       rotation, so the whole field is seen over a cycle and the header carries an
       "n / m" so the room knows it is looking at part of one. Anything reaching
       this rule is a page that overflowed its own estimate, which means
       RowsPerPage is too high for the screen in front of you. */
    overflow: hidden;
}

.db-panel__header {
    display: flex;
    align-items: center;
    gap: var(--db-gap);
}

/* The stencil voice — the painted headers on a real board. */
.db-panel__title {
    margin: 0;
    color: var(--db-ink-dim);
    font-size: var(--db-size-panel);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.18em;
}

.db-panel__badge {
    padding: 0.25em 0.9em;
    border-radius: 999px;
    font-size: var(--db-size-panel);
    font-weight: 800;
    letter-spacing: 0.14em;
}

.db-panel__empty {
    margin: auto;
    color: var(--db-ink-dim);
    font-size: var(--db-size-lead);
    font-weight: 600;
    text-align: center;
}


/* =============================================================================
   Standings
   ========================================================================== */

.db-standings__final {
    background-color: var(--db-flag);
    color: #0B1219;
}

/* "2 / 4" — which slice of the field is on screen. Seated in a channel rather
   than on the accent: it is orientation, not news, and the amber belongs to
   FINAL and the leader. Mono so the pair of digits does not shuffle as the
   rotation moves on. */
.db-standings__page {
    background-color: var(--db-channel);
    border: 1px solid var(--db-channel-edge);
    color: var(--db-ink-dim);
    font-family: var(--db-font-mono);
    font-variant-numeric: tabular-nums;
}

.db-standings {
    width: 100%;
    border-collapse: collapse;
    table-layout: auto;
}

.db-standings__head th {
    padding: 0 clamp(10px, 0.8vw, 20px) calc(var(--db-gap) * 0.5);
    border-bottom: 2px solid var(--db-rule);

    color: var(--db-ink-dim);
    font-size: var(--db-size-panel);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    text-align: right;
    white-space: nowrap;
}

.db-standings__head .db-standings__rank,
.db-standings__head .db-standings__player,
.db-standings__head .db-standings__side-name {
    text-align: left;
}

/* --- sides: this table leads the rotation ---------------------------------
   For a Ryder-Cup-style event the team number is what the room is following.
   It is set roughly twice the size of an individual row so the hierarchy holds
   from the back of the room, where relative size is the only cue that survives.

   That size is also why it has a panel of its own rather than a header slot on
   the standings panel: at twice the row height it was spending about five of the
   individual table's ranks, and a clipped panel lost them from every page of the
   cycle. No bottom margin any more — there is no longer a table underneath it. */

.db-standings__side td {
    padding: clamp(8px, 0.7vw, 16px) clamp(10px, 0.8vw, 20px);
    border-bottom: 1px solid var(--db-rule);
}

.db-standings__side .db-standings__side-name {
    font-size: var(--db-size-side);
    font-weight: 800;
    letter-spacing: -0.01em;
}

/* A side with no rounds in. Named rather than inferred from the dash, because
   "Not played" and a total of nothing are two different pieces of information
   and the room should not have to work out which it is looking at. */
.db-standings__side-status {
    margin-left: 0.8em;
    color: var(--db-ink-faint);
    font-size: var(--db-size-panel);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    vertical-align: middle;
}

/* The plate. The team number is what the room is following, so this is where the
   channel treatment goes — a seated plate, lit along its top edge. Sparingly:
   there are two to twelve of these, and the same treatment on every cell of the
   individual table below would be a hundred boxes reading as a grid rather than
   as a leaderboard. The individual rows stay flat on the board face. */
.db-standings__side .db-standings__side-total {
    width: 1%;
    padding: clamp(6px, 0.5vw, 14px) clamp(16px, 1.2vw, 28px);

    background-color: var(--db-channel);
    border-radius: 10px;
    box-shadow: inset 0 2px 0 var(--db-channel-edge),
                inset 0 -2px 0 rgba(0, 0, 0, 0.35);

    font-size: var(--db-size-side-total);
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    text-align: right;
    white-space: nowrap;
}

/* The leader. The server ranks the sides — including forcing unplayed ones
   last — so first-listed is the leading side, and reading position rather than
   re-deriving the rule keeps CSS from being free to disagree with the service.
   With a tie this lights the first of the tied sides, which is the order the
   board is already presenting them in.

   The :not() is load-bearing, not defensive. Before a ball is struck EVERY side
   is unplayed, so first-child is simply the first team listed — and lighting it
   would put a leader on the board in a tournament nobody has started. That is
   the not-played-is-not-zero rule failing in CSS instead of in C#, which is a
   worse place to have it fail because no test can see it.

   Only the total is lit. The name stays ink: one amber number per table is the
   whole signal, and colouring the name too would spend the accent twice to say
   one thing.

   --sides-lead is on the FIRST page only, and is why this reads "first row of
   page one" rather than "first row". A twelve-side event pages, and without the
   extra class :first-child would light the top of every page — putting three
   different leaders on the board over one cycle. */
.db-standings--sides-lead .db-standings__side:first-child .db-standings__side-total:not(.db-standings__value--unplayed) {
    color: var(--db-flag);
}

/* --- individual rows ---------------------------------------------------- */

.db-standings__row td {
    padding: clamp(6px, 0.55vw, 14px) clamp(10px, 0.8vw, 20px);
    border-bottom: 1px solid var(--db-rule);
    font-size: var(--db-size-body);
}

.db-standings__row .db-standings__rank {
    width: 1%;
    color: var(--db-ink-dim);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    text-align: left;
    white-space: nowrap;
}

/* Absorbs the slack so the score columns stay packed at the right edge, where
   they can be read as a column rather than hunted for.

   max-width: 0 is load-bearing and must not be tidied away as a no-op. Under
   `table-layout: auto` a cell's minimum width is its min-content width, and
   `white-space: nowrap` makes that the WHOLE name — so `overflow: hidden` never
   gets to act, the ellipsis never fires, and the table grows past 100% instead.
   `.db-panel { overflow: hidden }` then clips the right edge, taking the session
   and total columns off a screen nobody can scroll. One long name would silently
   remove the scores from a clubhouse TV.

   max-width: 0 drops that min-content floor, so `width: 100%` distributes the
   leftover space and the cell truncates. Preferred over `table-layout: fixed`
   because the score columns are `width: 1%` — under fixed layout that is read as
   a literal 1% of the table rather than "shrink to content", which would crush
   them. */
.db-standings__row .db-standings__player {
    width: 100%;
    max-width: 0;
    font-weight: 700;
    text-align: left;

    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* --- numeric columns ------------------------------------------------------
   Right-aligned and tabular so they read as a column under a polling board. The
   plate treatment is NOT here — it belongs to the side totals above and to the
   pairing code, and nowhere else. */

.db-standings__value,
.db-standings__row .db-standings__total,
.db-standings__side .db-standings__side-total {
    font-variant-numeric: tabular-nums;
    text-align: right;
    white-space: nowrap;
}

.db-standings__value,
.db-standings__row .db-standings__total {
    width: 1%;
    min-width: 4ch;
    font-weight: 700;
}

/* Size and weight carry the total's importance, not colour. Accenting every
   total would spend the accent twenty times over and leave nothing to mark the
   one row the accent is for. */
.db-standings__row .db-standings__total {
    font-weight: 800;
    font-size: var(--db-size-lead);
}

/* The leader row. Rank 1 is the first row because the server ranks them.
   Unlike the sides table this needs no unplayed guard: a player's Total is a
   non-nullable decimal that is always rendered, so there is no pre-play state
   in which this row is showing nothing. */
.db-standings__row:first-child .db-standings__rank,
.db-standings__row:first-child .db-standings__player,
.db-standings__row:first-child .db-standings__total {
    color: var(--db-flag);
}

/* --- not played is not zero ---------------------------------------------
   A genuine 0 keeps full ink and full weight; an unplayed slot does not. Level
   par and "hasn't teed off" are not the same fact, and this board is the only
   place the room finds that out.

   Scoped through .db-standings deliberately. The bare class (0,1,0) would lose
   to `.db-standings__side .db-standings__side-total` (0,2,0) above and an
   unplayed side would keep full weight and its plate — the marker silently
   doing nothing on exactly one of the two places it is applied. Matching that
   specificity and declaring it last is what makes it win in both.

   Two different treatments, because the two tables carry different weight:

   - On a SIDE TOTAL the plate is removed and the padding kept, so the slot
     stays the size it would be with a score in it and is visibly empty. That
     is the metaphor doing the work — no plate seated in the channel.
   - On an individual session cell there is no plate to remove, so this is a
     dimmed, lightened dash and nothing more. Said plainly because a comment
     claiming otherwise would be describing a board that does not exist. */
.db-standings .db-standings__value--unplayed {
    color: var(--db-ink-faint);
    font-weight: 400;
    background-color: transparent;
    box-shadow: none;
}


/* =============================================================================
   On the course
   ========================================================================== */

/* "2 / 3" — which slice of the cards is on screen. Same treatment as the
   standings and Up Next badges and for the same reason: orientation, not news. */
.db-on-course__page {
    background-color: var(--db-channel);
    border: 1px solid var(--db-channel-edge);
    color: var(--db-ink-dim);
    font-family: var(--db-font-mono);
    font-variant-numeric: tabular-nums;
}

.db-on-course__group {
    padding-bottom: calc(var(--db-gap) * 0.7);
    margin-bottom: calc(var(--db-gap) * 0.7);
    border-bottom: 1px solid var(--db-rule);
}

.db-on-course__group:last-child {
    padding-bottom: 0;
    margin-bottom: 0;
    border-bottom: none;
}

/* A round that finished while its session is still out. It keeps its place and its result — losing
   the match won 4 & 3 an hour ago would be losing the thing the room came for — but it steps back so
   the eye lands on what is still being decided. Recession only: the numbers stay full strength,
   because a settled result is not a provisional one. */
.db-on-course__group--complete .db-on-course__group-label,
.db-on-course__group--complete .db-on-course__course {
    color: var(--db-ink-faint);
}

/* The card's state stamp — "Final" for a settled round, "Yet to tee off" for one still on the tee.
   A stamp, not a badge: same stencil voice as the panel labels, because it is a state rather than an
   accent, and the flag amber is reserved for the leader and for things the room must notice. */
.db-on-course__stamp {
    margin-left: var(--db-gap);

    color: var(--db-ink-faint);
    font-size: var(--db-size-panel);
    font-weight: 600;
    letter-spacing: 0.18em;
    text-transform: uppercase;
}

.db-on-course__group-header {
    display: flex;
    align-items: baseline;
    gap: clamp(10px, 0.9vw, 20px);
    margin-bottom: calc(var(--db-gap) * 0.5);
}

.db-on-course__group-label {
    font-size: var(--db-size-lead);
    font-weight: 800;
    letter-spacing: -0.01em;
}

/* Course and format are context, not content. Stencil voice, and separated by
   a dot rather than by punctuation in the markup — either may be absent, and a
   separator baked into the component becomes a stray character when it is. */
.db-on-course__course,
.db-on-course__format {
    color: var(--db-ink-dim);
    font-size: var(--db-size-panel);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.12em;
}

.db-on-course__course::before,
.db-on-course__format::before {
    content: '';
    display: inline-block;
    width: 0.4em;
    height: 0.4em;
    margin-right: 0.7em;
    border-radius: 50%;
    background-color: var(--db-ink-faint);
    vertical-align: middle;
}

/* Round-level progress on a format-native card. Mono and dim for the same
   reasons as the per-player "thru" below — it qualifies the card rather than
   competing with the values in it — but it sits in the header because on these
   formats the whole card is at one hole, not one hole per row. */
.db-on-course__progress {
    color: var(--db-ink-dim);
    font-family: var(--db-font-mono);
    font-size: var(--db-size-panel);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    white-space: nowrap;
}

.db-on-course__players,
.db-on-course__rows {
    margin: 0;
    padding: 0;
    list-style: none;
}

.db-on-course__row {
    display: flex;
    align-items: baseline;
    gap: clamp(10px, 0.9vw, 20px);
    padding: clamp(5px, 0.45vw, 12px) 0;
    font-size: var(--db-size-body);
}

.db-on-course__row-value {
    min-width: 5ch;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    text-align: right;
    white-space: nowrap;
}

/* The one accent, and unlike the ranked field this card earns it. A stroke-play
   card is a dozen players mid-round with no leader to mark — hence the plain ink
   on .db-on-course__value below — but a match card has a side that is up right
   now, and which one is the entire content. The server withholds IsLeader on an
   all-square or tied card, so the amber cannot appear where there is no leader. */
.db-on-course__row--leader .db-on-course__name,
.db-on-course__row--leader .db-on-course__row-value {
    color: var(--db-flag);
}

/* The card's own line — a match that is over, a pot that is carrying. Set below
   the rows and dimmer than them: it is context for the card, not another row. */
.db-on-course__status {
    margin: calc(var(--db-gap) * 0.35) 0 0;
    color: var(--db-ink-dim);
    font-size: var(--db-size-panel);
    font-weight: 600;
    letter-spacing: 0.04em;
}

.db-on-course__player {
    display: flex;
    align-items: baseline;
    gap: clamp(10px, 0.9vw, 20px);
    padding: clamp(5px, 0.45vw, 12px) 0;
    font-size: var(--db-size-body);
}

.db-on-course__name {
    flex: 1;
    min-width: 0;
    font-weight: 700;

    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Mono, because "thru 7" and "thru 17" sitting in a stack want the same width.
   Dim: it qualifies the score rather than competing with it. */
.db-on-course__thru {
    color: var(--db-ink-dim);
    font-family: var(--db-font-mono);
    font-size: var(--db-size-panel);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    white-space: nowrap;
}

/* Ink, not accent. Nobody on this panel is winning — these are players mid-round
   — so there is no leader for the accent to mark, and amber down a column of
   twelve would be decoration pretending to be information. */
.db-on-course__value {
    min-width: 5ch;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    text-align: right;
    white-space: nowrap;
}

/* A finished card is worth marking and must not outshout a player still out
   there — so the row recedes rather than the tick shouting. */
.db-on-course__player--finalized .db-on-course__name,
.db-on-course__player--finalized .db-on-course__value {
    color: var(--db-ink-dim);
}

.db-on-course__finalized {
    color: var(--db-green);
    font-size: var(--db-size-panel);
    font-weight: 700;
}


/* =============================================================================
   Up next
   ========================================================================== */

/* "2 / 3" — which slice of the schedule is on screen. Same treatment as the
   standings badge and for the same reason: orientation, not news. */
.db-up-next__page {
    background-color: var(--db-channel);
    border: 1px solid var(--db-channel-edge);
    color: var(--db-ink-dim);
    font-family: var(--db-font-mono);
    font-variant-numeric: tabular-nums;
}

.db-up-next__session {
    padding-bottom: calc(var(--db-gap) * 0.7);
    margin-bottom: calc(var(--db-gap) * 0.7);
    border-bottom: 1px solid var(--db-rule);
}

.db-up-next__session:last-child {
    padding-bottom: 0;
    margin-bottom: 0;
    border-bottom: none;
}

.db-up-next__session-header {
    display: flex;
    align-items: baseline;
    gap: clamp(10px, 0.9vw, 20px);
    margin-bottom: calc(var(--db-gap) * 0.5);
}

.db-up-next__name {
    font-size: var(--db-size-lead);
    font-weight: 800;
    letter-spacing: -0.01em;
}

/* The time is the thing anyone reads this panel for. */
.db-up-next__when {
    color: var(--db-flag);
    font-size: var(--db-size-lead);
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Course and format share one voice: both are context for the round rather than the round itself, and
   splitting them across two treatments would make the reader weigh one against the other. The
   separator before each is drawn by the header's own flex gap and dot, so they compose in either
   order and a missing one leaves no orphan. */
.db-up-next__course,
.db-up-next__format {
    color: var(--db-ink-dim);
    font-size: var(--db-size-panel);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.12em;
}

.db-up-next__groups {
    margin: 0;
    padding: 0;
    list-style: none;
}

.db-up-next__group {
    padding: clamp(4px, 0.4vw, 10px) 0 clamp(4px, 0.4vw, 10px) clamp(16px, 1.2vw, 28px);
    border-left: 3px solid var(--db-rule);
    color: var(--db-ink);
    font-size: var(--db-size-body);
    font-weight: 600;
}


/* =============================================================================
   Motion preference
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
    /* The panel entry is decoration and goes. The progress fill stays: it is
       the only thing telling the room how long this panel has left, and a
       linear bar is not what the preference is guarding against. */
    .db-rotator__panel {
        animation: none;
    }

    .db-rotator__dot {
        transition: none;
    }

    .db-pairing__submit,
    .db-start,
    .db-unpair__trigger,
    .db-unpair__go {
        transition: none;
    }
}
