/* ===========================================================================
   awards.css — voting, nominations and the reveal, on the programme surface.
   Loaded after tokens.css. Every size scales off var(--scale) so the big-screen
   layer rescales these too.

   No raw colours below this line — scripts/check-ui-tokens.js enforces it.
   =========================================================================== */

/* ===== Sign-in strip ===== */

.signin-row { display: flex; align-items: center; justify-content: space-between;
  gap: 0.8rem; flex-wrap: wrap; }
.who { display: flex; align-items: center; gap: 0.5rem; font-weight: 600; }
.who-avatar { width: 32px; height: 32px; flex: none; border-radius: 50%;
  display: grid; place-items: center; background: var(--sport-grad);
  color: var(--on-sport); font-size: var(--fs-xs); font-weight: 700; }

/* Contracts forces a PIN change on first use, and that can only be done there.
   This has to be impossible to miss, or a member gets stuck at the door. */
.pin-warning { border: 1px solid var(--warning); border-radius: var(--radius);
  background: color-mix(in srgb, var(--warning) 10%, var(--bg-card));
  padding: 0.9rem 1.1rem; font-size: var(--fs-sm); }
.pin-warning a { color: var(--warning); font-weight: 700; }

/* ===== Award cards ===== */

/* auto-fit rather than a hard 1-or-2: the ballot is read on a phone, a tablet
   in the hall and a laptop at the desk, and a fixed two columns meant a 1920
   screen showed the same two 531px cards it showed at 1180 with the rest of
   the width empty. min(340px, 100%) keeps auto-fit from forcing a column
   wider than a narrow viewport. */
.award-list { display: grid; gap: 1.25rem;
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)); }

.award { background: var(--bg-card); border: 1px solid var(--border-color);
  border-radius: var(--radius); padding: 1.4rem 1.5rem;
  display: flex; flex-direction: column; gap: 1rem; box-shadow: var(--shadow-sm); }
.award.is-fun { border-left: 4px solid var(--sport); }
.award.is-serious { border-left: 4px solid var(--border-strong); }
.award-head { display: flex; align-items: flex-start; justify-content: space-between;
  gap: 0.7rem; }
.award-title { font-family: 'Oswald', sans-serif; font-size: var(--fs-md);
  font-weight: 700; letter-spacing: 0.01em; }
.award-desc { font-size: var(--fs-sm); color: var(--text-muted); }

/* ===== Nominee choices ===== */

.choices { display: flex; flex-direction: column; gap: 0.55rem; }
.choice { display: flex; align-items: center; gap: 0.8rem; width: 100%;
  padding: 0.8rem 0.95rem; border-radius: 11px; cursor: pointer; text-align: left;
  background: var(--bg-inset); border: 1.5px solid var(--border-strong);
  color: var(--text-main); font-size: var(--fs-base); font-weight: 500;
  transition: border-color 0.14s, background 0.14s; }
.choice:hover:not(:disabled) { border-color: var(--sport-ring); background: var(--sport-soft); }
.choice.chosen { border-color: var(--sport); background: var(--sport-soft);
  color: var(--sport-on-soft); font-weight: 700; }
.choice.winner { border-color: var(--success);
  background: color-mix(in srgb, var(--success) 16%, var(--bg-card));
  color: color-mix(in srgb, var(--success) 78%, var(--text-main)); font-weight: 700; }
.choice:disabled { cursor: default; opacity: 0.9; }
.choice-name { flex: 1; min-width: 0; }
.choice-backers { font-size: var(--fs-xs); color: var(--text-faint);
  white-space: nowrap; }

/* ===== Tally bars ===== */

.bars { display: flex; flex-direction: column; gap: 0.6rem; }
.bar-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0.5rem 0.7rem;
  align-items: center; font-size: var(--fs-sm); }
.bar-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bar-count { font-variant-numeric: tabular-nums; font-weight: 700; }
.bar-track { grid-column: 1 / -1; height: 10px; border-radius: 9999px;
  background: var(--bg-subtle); overflow: hidden; }
.bar-fill { display: block; height: 100%; border-radius: 9999px;
  background: var(--sport-grad); transition: width 0.6s ease; }
.bar-row.is-winner .bar-fill { background: var(--success); }
.bar-row.is-winner .bar-count { color: var(--success); }

/* ===== Turnout + closeness ===== */

.turnout { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
  font-size: var(--fs-sm); color: var(--text-muted); }
.turnout-bar { flex: 1 1 120px; height: 6px; border-radius: 9999px;
  background: var(--bg-subtle); overflow: hidden; min-width: 80px; }
.turnout-fill { display: block; height: 100%; background: var(--sport); }

/* The one signal the room gets while voting is open. Names nobody, on purpose. */
.closeness { display: inline-flex; align-items: center; gap: 0.45rem;
  padding: 0.25rem 0.7rem; border-radius: 9999px;
  font-size: var(--fs-xs); font-weight: 700;
  border: 1px solid var(--border-strong); color: var(--text-muted); }
.closeness.tight { color: var(--warning);
  border-color: color-mix(in srgb, var(--warning) 40%, var(--bg-card));
  background: color-mix(in srgb, var(--warning) 12%, var(--bg-card)); }

/* ===== Crowd nominations ===== */

.nominate-row { display: flex; gap: 0.5rem; }
.nominate-row input { flex: 1; min-width: 0; }
.suggestions { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.suggestion { display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.3rem 0.7rem; border-radius: 9999px;
  font-size: var(--fs-sm); font-weight: 600;
  border: 1px solid var(--border-strong); background: var(--bg-inset);
  color: var(--text-muted); }
.suggestion .count { color: var(--sport-ink); font-variant-numeric: tabular-nums; }
.suggestion.approved { border-color: color-mix(in srgb, var(--success) 40%, var(--bg-card));
  color: color-mix(in srgb, var(--success) 75%, var(--text-main)); }
.suggestion.rejected { opacity: 0.5; text-decoration: line-through; }

/* ===== Admin ===== */

.admin-grid { display: grid; gap: 1.25rem; grid-template-columns: 1fr; }
@media (min-width: 1000px) { .admin-grid { grid-template-columns: 340px 1fr; } }

.award-row { display: flex; align-items: center; gap: 0.6rem; width: 100%;
  padding: 0.6rem 0.75rem; border-radius: 9px; cursor: pointer; text-align: left;
  background: none; border: 1px solid transparent; color: var(--text-muted);
  font-size: var(--fs-sm); }
.award-row:hover { background: var(--bg-subtle); color: var(--text-main); }
.award-row.active { background: var(--sport-soft); border-color: var(--sport-ring);
  color: var(--sport-on-soft); font-weight: 600; }
.award-row .seq { flex: none; font-family: 'Oswald', sans-serif;
  color: var(--text-faint); min-width: 1.5em; }

.status-bar { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.status-step { padding: 0.3rem 0.65rem; border-radius: 7px; cursor: pointer;
  font-size: var(--fs-xs); font-weight: 600;
  background: var(--bg-inset); color: var(--text-faint);
  border: 1px solid var(--border-color); }
.status-step:hover { color: var(--text-main); border-color: var(--border-strong); }
.status-step.active { background: var(--sport); color: var(--on-sport);
  border-color: var(--sport); }

/* .toggle and .toggle-row moved to tokens.css — both surfaces use them. */

.outstanding { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.outstanding .name { padding: 0.2rem 0.55rem; border-radius: 6px;
  font-size: var(--fs-xs); background: var(--bg-inset);
  color: var(--text-faint); border: 1px solid var(--border-color); }

/* ===== PIN sheet =====
   Shown once, immediately after a bulk reset. Contracts stores only the hash,
   so this really is the only copy that will ever exist — the panel says so. */

.pin-sheet { border: 1px solid var(--warning); border-radius: var(--radius);
  background: color-mix(in srgb, var(--warning) 8%, var(--bg-card));
  padding: 1rem 1.1rem; display: flex; flex-direction: column; gap: 0.7rem;
  margin-top: 0.9rem; }
.pin-sheet-head { font-size: var(--fs-micro); text-transform: uppercase;
  letter-spacing: 0.12em; font-weight: 800; color: var(--warning); }
.pin-sheet textarea { min-height: 220px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-sm); line-height: 1.5; resize: vertical; }

/* ===== Roles & teams admin ===== */

.role-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);
  font-size: var(--fs-sm); }
.role-row:last-child { border-bottom: none; }
.role-row .who-name { font-weight: 600; flex: 1; min-width: 0; }

.team-row { display: flex; align-items: center; gap: 0.7rem;
  padding: 0.6rem 0; border-bottom: 1px solid var(--border-color); }
.team-row:last-child { border-bottom: none; }
.team-crest { width: 42px; height: 42px; flex: none; border-radius: 10px;
  object-fit: contain; background: var(--bg-inset);
  border: 1px solid var(--border-color); }
.team-crest-empty { width: 42px; height: 42px; flex: none; border-radius: 10px;
  display: grid; place-items: center; background: var(--bg-inset);
  border: 1px dashed var(--border-strong); color: var(--text-faint);
  font-size: var(--fs-micro); }
.team-row .team-name-cell { flex: 1; min-width: 0; font-weight: 600; }

/* ===== Vote growth, at the reveal ===== */

.growth { display: flex; flex-direction: column; gap: 0.4rem; }
.growth-track { display: flex; align-items: flex-end; gap: 2px; height: 64px;
  padding: 0.3rem; background: var(--bg-inset); border-radius: 9px;
  border: 1px solid var(--border-color); overflow: hidden; }
.growth-bar { flex: 1 1 0; min-width: 2px; border-radius: 2px 2px 0 0;
  background: var(--team-color, var(--sport)); opacity: 0.85; }
.growth-legend { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem;
  font-size: var(--fs-xs); color: var(--text-muted); }
.growth-key { display: inline-flex; align-items: center; gap: 0.35rem; }
.growth-swatch { width: 10px; height: 10px; border-radius: 3px;
  background: var(--team-color, var(--sport)); flex: none; }

/* Marks the nominee you currently have voted for. Votes are changeable, so the
   chosen row must stay visibly chosen while the other rows stay tappable —
   "disabled everything else" would misrepresent the rule. */
.choice-mark { flex: none; font-size: var(--fs-micro); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--sport-on-soft); border: 1px solid var(--sport-ring);
  background: var(--sport-soft); border-radius: 999px; padding: 0.1rem 0.5rem; }

/* ===== Teams panel =====
   Squads are created and edited here. This is the only place in the app where
   a team can be brought into existence — before this panel there was no
   create-team path at all outside the dev seed script. */

.team-block { border: 1px solid var(--border-color); border-radius: 11px;
  padding: 0.8rem; margin-bottom: 0.8rem; background: var(--bg-subtle); }
/* This row carries crest, name, colour, upload and delete inside a 340px
   sidebar, so it MUST be allowed to wrap — without it the delete button is
   pushed past the edge of the card and cannot be reached at all. */
.team-block .team-row { margin-bottom: 0.6rem; padding-top: 0;
  flex-wrap: wrap; row-gap: 0.5rem; }
.team-block .team-name-input { flex: 1 1 6rem; }
.team-block .team-row > .btn { flex: none; }

.team-name-input { flex: 1; min-width: 6rem; background: var(--bg-card);
  border: 1px solid var(--border-color); border-radius: 8px;
  padding: 0.35rem 0.55rem; color: var(--text-main); font-weight: 600;
  font-size: var(--fs-sm); }
.team-name-input:focus { outline: none; border-color: var(--sport-ring); }

/* The native colour swatch: small, square, and stripped of the chrome that
   makes it look like a text field. */
.team-colour { flex: none; width: 34px; height: 30px; padding: 2px;
  background: var(--bg-card); border: 1px solid var(--border-color);
  border-radius: 8px; cursor: pointer; }

.team-code-row { display: flex; align-items: center; gap: 0.55rem;
  flex-wrap: wrap; margin-bottom: 0.6rem; }
.team-code-row .mini-h { margin-bottom: 0; }

.squad { border-top: 1px solid var(--border-color); padding-top: 0.6rem; }
.squad .mini-h { margin-bottom: 0.45rem; }
.squad-row { display: flex; align-items: center; gap: 0.5rem;
  padding: 0.3rem 0; font-size: var(--fs-sm); }
.squad-row > span:first-child { flex: 1; min-width: 0; }
.squad-add { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem; }
.squad-add select, .squad-add input { flex: 1; min-width: 0; }

/* ===== Agenda =====
   The running order. A list rather than a table: four columns on a phone put
   the status off the right edge and wrapped every name onto three lines, and
   this is the most-read thing on the night.

   Time sits in a fixed left rail so the eye runs straight down it; everything
   else flows in the remaining width at any screen size. */
.agenda { list-style: none; display: flex; flex-direction: column; }
.agenda-item { display: grid; grid-template-columns: 4.5rem minmax(0, 1fr);
  gap: var(--sp-3); padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--border-color); }
.agenda-item:last-child { border-bottom: none; }

.agenda-time { font-family: 'Oswald', sans-serif; font-weight: 600;
  font-size: var(--fs-sm); color: var(--text-faint);
  font-variant-numeric: tabular-nums; padding-top: 0.1em; }
.agenda-main { min-width: 0; display: flex; flex-direction: column; gap: 0.2rem; }
.agenda-name { font-size: var(--fs-base); font-weight: 600; color: var(--text-main); }
.agenda-desc { font-size: var(--fs-xs); color: var(--text-faint); line-height: 1.5; }
.agenda-meta { display: flex; align-items: center; gap: var(--sp-2);
  flex-wrap: wrap; margin-top: 0.15rem; }
.agenda-kind { font-size: var(--fs-micro); text-transform: uppercase;
  letter-spacing: 0.07em; font-weight: 700; color: var(--text-faint); }

/* What is happening NOW is the one thing someone opening this page wants, so
   it is the one row that is allowed to shout. */
.agenda-item.is-live { background: var(--sport-soft); border-radius: 10px;
  padding-left: var(--sp-3); padding-right: var(--sp-3);
  border-bottom-color: transparent; }
.agenda-item.is-live .agenda-time,
.agenda-item.is-live .agenda-name { color: var(--sport-on-soft); }
.agenda-item.is-done { opacity: 0.55; }

/* ===== Fun vs serious =====
   These were separated only by a 4px left border — gold against grey, a colour
   with no legend. Voting for Goalkeeper of the Season and voting for Best
   Beard should not look like the same act, so the list is now grouped under
   headings and every card says which it is. */
.award-section { grid-column: 1 / -1; margin-top: var(--sp-3); }
.award-section:first-child { margin-top: 0; }
.award-section-title { font-family: 'Oswald', sans-serif; font-weight: 600;
  font-size: var(--fs-md); color: var(--sport-ink); text-transform: uppercase;
  letter-spacing: 0.06em; }
.award-section-blurb { font-size: var(--fs-xs); color: var(--text-faint);
  margin-top: 0.15rem; }

.award-tags { display: flex; align-items: center; gap: var(--sp-2);
  flex-wrap: wrap; flex: none; }

/* The list is grouped by kind now, so a title needs to take the slack rather
   than let the status chip drift away from it. */
.award-row-title { flex: 1; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }

/* ===== Admin panes =====
   Four jobs — awards, teams, people, settings — that used to share one 340px
   rail while the detail pane beside it sat empty. Only Awards keeps the
   list-and-detail split, because that is genuinely how an award is worked;
   everything else gets the width it always needed. */
.admin-pane { display: block; }

/* Cards side by side where the content suits it, stacking on narrow screens.
   auto-fit rather than a fixed count: two full-width cards read better than
   three squeezed ones. */
.admin-cols { display: grid; gap: var(--sp-5);
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
  align-items: start; }

/* The tab bar sits under the top bar on this screen, so it needs to clear it
   rather than sit flush against the gold rule. */
#adminTabs { position: sticky; top: 0; z-index: 9; }

/* ===========================================================================
   The awards big screen
   ---------------------------------------------------------------------------
   Everything below applies only under .stage-awards, so the quiz display is
   untouched. display.css loads after this file and rescales it all off
   --scale; these rules set roles and proportions, never pixel sizes.

   The screen had one style, .display-category, doing three different jobs at
   once — the eyebrow above the title, the instruction below it, and the live
   counter — so three unrelated things arrived in the room at the same size and
   the same gold. Each has its own role now, and the sizes step.
   =========================================================================== */

/* ---- The pitch ----
   Mown stripes and a halfway line, at a few percent over near-black. A
   projector bands a gradient and blooms anything bright, so this is the most
   the ground can carry and still read as black from the back of a hall. The
   stripe width is in vw so it scales with the beam rather than the type. */
.stage-awards { position: relative; }
.stage-awards::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background:
    repeating-linear-gradient(90deg,
      var(--turf-stripe) 0 8vw, transparent 8vw 16vw),
    linear-gradient(180deg, transparent 0%, var(--turf-wash) 100%);
}
/* A halfway line and centre circle, drawn rather than imported: one element,
   no request, and it scales to any beam. */
.stage-awards::after {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  left: 50%; top: 50%; width: 46vh; height: 46vh; transform: translate(-50%, -50%);
  border: 2px solid var(--turf-line); border-radius: 50%;
}
.stage-awards > * { position: relative; z-index: 1; }

/* ---- Award identity ----
   Gold is the trophy: it marks the real categories. Turf green marks the fun
   ones. The two never share a treatment, because voting for Golden Glove and
   voting for Best Beard are not the same act. */
.award-kind { display: inline-flex; align-items: center; gap: 0.6em;
  font-family: 'Oswald', sans-serif; font-size: var(--fs-md); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.22em; }
.award-kind::before { content: ""; width: 2.4em; height: 2px; background: currentColor; }
.award-kind::after { content: ""; width: 2.4em; height: 2px; background: currentColor; }
.award-kind.is-serious { color: var(--sport-ink); }
.award-kind.is-fun { color: var(--turf-on-soft); }

/* ---- Two columns while the room is being asked to do something ----
   The nominees and the way to vote for them belong side by side. Stacked, the
   address sat below the fold of attention and the beam was half empty. */
.award-split { display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--sp-6); align-items: center; width: 100%; text-align: left; }

/* ---- The nominees, as a team sheet ----
   They were chips, which is how you render tags. These are people, and a
   numbered line-up is the form a football club already reads. */
.teamsheet { display: flex; flex-direction: column; gap: 0.5rem; min-width: 0; }
.teamsheet-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: var(--sp-4);
  padding: 0.5rem 1rem 0.5rem 0.6rem; border-radius: var(--radius);
  background: var(--bg-card); border-left: 4px solid var(--sport); }
.stage-awards .is-fun .teamsheet-row { border-left-color: var(--turf); }
.teamsheet-num { font-family: 'Oswald', sans-serif; font-weight: 700;
  font-size: var(--fs-lg); color: var(--text-faint);
  font-variant-numeric: tabular-nums; min-width: 1.6em; text-align: center; }
.teamsheet-name { font-family: 'Oswald', sans-serif; font-weight: 600;
  font-size: var(--fs-lg); letter-spacing: 0.02em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* How many people put this name forward, during nominations. */
.teamsheet-backers { font-family: 'Oswald', sans-serif; font-weight: 600;
  font-size: var(--fs-md); color: var(--sport-ink); font-variant-numeric: tabular-nums; }

/* ---- Scan to vote ----
   A QR has to sit on white. Not "a light card" — white, with a quiet zone,
   because a projector lifts black toward grey and eats whatever contrast the
   code had left. This is the one deliberately light object on the screen. */
.scan-card { display: flex; flex-direction: column; align-items: center;
  gap: 0.8rem; padding: 1.2rem; border-radius: var(--radius);
  background: var(--scan-ground); box-shadow: 0 0 0 6px var(--sport-ring); }
.scan-qr { display: block; width: 26vh; height: 26vh; }
.scan-lead { font-family: 'Oswald', sans-serif; font-weight: 700;
  font-size: var(--fs-md); text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--on-sport); }
.scan-url { font-size: var(--fs-sm); font-weight: 600; color: var(--on-sport);
  letter-spacing: 0.02em; }

/* The instruction under the title. A sentence, not an eyebrow: it is read
   once, by people deciding whether to take their phone out.

   margin-inline: auto is load-bearing. Inside the reveal stack every child is
   width:100%, so a max-width alone capped the box at 44ch FROM THE LEFT EDGE
   and the centred text inside it then centred on the box, not on the screen.
   Measured on the live 1920 beam: every other block centred on 960 and this
   one on 715 — the visible misalignment, from one missing margin. */
.award-instruction { font-size: var(--fs-lg); color: var(--text-muted);
  max-width: 44ch; margin-inline: auto; text-wrap: balance; }

/* ---- The winner ----
   The one moment the whole screen exists for, so it gets the gold, the glow
   and the only piece of motion on the page. */
.winner-name { font-family: 'Oswald', sans-serif; font-weight: 700;
  font-size: var(--fs-2xl); line-height: 1.05; color: var(--sport-ink);
  text-transform: uppercase; letter-spacing: 0.02em;
  text-shadow: 0 0 40px var(--sport-glow);
  animation: winner-in 700ms cubic-bezier(0.16, 1, 0.3, 1) both; }
@keyframes winner-in {
  from { opacity: 0; transform: scale(0.94); }
  to   { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .winner-name { animation: none; }
}

/* This screen shows two columns and a QR, not a paragraph, so it takes more
   width than display.css gives a question. --stage-measure is a reading
   measure, and there is nothing to read across here.

   The child selector has to name .stage-fit, not .stage-body. When the fit
   wrapper went in it silently stopped matching, and the reveal — the bars, the
   winner, the whole result — rendered in a shrink-to-fit 425px column in the
   middle of a 1824px beam. A direct-child selector is only as stable as the
   DOM above it. */
.stage-awards .stage-fit > .stack { width: min(100%, 1600px); }
.stage-awards .stage-fit > .stack > * { width: 100%; }

/* The single wrapper fitStage() scales. It carries the layout .stage-body used
   to apply directly, because a transformed element must be the only child for
   the scale to mean anything. */
.stage-fit { display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 1.5rem; width: 100%;
  transform-origin: center center; }

/* ===========================================================================
   The day, in two columns
   ---------------------------------------------------------------------------
   The programme and the standings were stacked in a 1180px column, so on a
   1920px screen 740px sat empty beside them and the board was below the fold.
   They are different questions — "what is on now" and "who is winning" — and
   people ask both at once, so they belong side by side where there is room.

   The programme takes the larger share: its rows carry a time, a name, a
   description and a status, while a standings row is a rank, a name and a
   number. auto-fit would give them equal widths, which is why this is an
   explicit ratio.
   =========================================================================== */
.day-grid { display: grid; gap: var(--sp-5); align-items: start; }
@media (min-width: 1080px) {
  .day-grid { grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr); }
}

/* ===== Standings rows carry their team's colour =====
   A rank, a name and a number in plain text is a list of strings. A team is
   recognised by its colour and its badge before anyone reads the name, and
   both were already in the database and simply not asked for. */
.standing-row td { border-left: 3px solid transparent; }
.standing-row td:first-child { border-left-color: var(--team-color);
  font-variant-numeric: tabular-nums; color: var(--text-faint); font-weight: 700; }
.standing-row.is-leader td:first-child { color: var(--sport-ink); }
.standing-row.is-leader .standing-team { font-weight: 700; }

.standing-team { display: inline-flex; align-items: center; gap: var(--sp-3);
  min-width: 0; }
/* Fixed box and object-fit: crests arrive as whatever shape someone uploaded,
   and a wide badge stretched to a square is worse than no badge. */
.standing-crest { width: 1.6rem; height: 1.6rem; flex: none;
  object-fit: contain; border-radius: 5px; }

/* ===== Running order: state down the left edge =====
   Every row looked the same except the live one, so "where are we" meant
   reading the whole list. The rail answers it at a glance, and it encodes
   state — done, now, still to come — rather than decorating the row. */
.agenda-item { border-left: 3px solid var(--border-color);
  padding-left: var(--sp-4); border-radius: 0 8px 8px 0; }
.agenda-item.is-done { border-left-color: var(--border-strong); }
.agenda-item.is-live { border-left-color: var(--sport); }
.agenda-item.is-pending { border-left-color: var(--turf); }

/* ===== Squads, side by side =====
   Three teams stacked in one column ran to 1222px and pushed the page into a
   scroll, with 1664px of card width used for rows that need maybe 500. Each
   squad is a self-contained thing — crest, name, kit colour, join code, who is
   in it — which is the definition of a card, so they sit in a grid.

   minmax(420px, 1fr) rather than a fixed count: three teams get three columns
   on a desk monitor, two get two, and a phone gets one, without a breakpoint
   per squad count. */
/* min(420px, 100%) rather than a bare 420px: auto-fit honours the minimum
   even when the viewport is smaller than it, so a flat minmax(420px, 1fr)
   forced a 420px column onto a 375px phone and pushed the whole page into a
   horizontal scroll. Measured: document 448px wide inside a 375px window. */
.team-grid { display: grid; gap: var(--sp-5); align-items: start;
  grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr)); }
.team-grid > .team-block { border: 1px solid var(--border-color);
  border-left: 3px solid var(--team-color, var(--border-strong));
  border-radius: var(--radius); padding: var(--sp-4);
  background: var(--bg-inset); min-width: 0; }

/* Creating a team is not a team: full width, under the squads. */
.team-grid > .squad-add.is-new-team { grid-column: 1 / -1; }
/* A name is short. The input was 1526px wide for one, which is the same
   mistake the 1124px sign-in card made at the other end of the app. */
.squad-add.is-new-team input { max-width: 320px; }

/* ===========================================================================
   Tonight — the snapshot, and the panes that connect the three surfaces
   ---------------------------------------------------------------------------
   The programme, the quiz and the voting lived on two domains and knew nothing
   about each other, so "what is happening right now" could only be answered by
   opening three screens and reading them side by side. These panes are one
   answer built from one endpoint.

   Read by two audiences at once: a member holding a phone at the back, and an
   organiser at a laptop. So the panes flow rather than snapping at a
   breakpoint, and the live one is legible before any of the words are.
   =========================================================================== */
.tonight { display: flex; flex-direction: column; gap: var(--sp-4);
  padding: var(--sp-5); border-radius: var(--radius);
  background: var(--bg-card); border: 1px solid var(--border-color); }

/* The bar moves when an item STARTS, not only when it finishes — a live item
   counts as half. On a five-item night that is a bar that moves nine times
   instead of four, which is the difference between progress and a status. */
.tonight-bar { height: 6px; border-radius: 9999px; background: var(--bg-inset);
  overflow: hidden; }
.tonight-fill { display: block; height: 100%; border-radius: 9999px;
  background: var(--sport-grad); transition: width 600ms ease; }

.tonight-panes { display: grid; gap: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr)); }

.pane { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0;
  padding: var(--sp-4); border-radius: 10px; background: var(--bg-inset);
  border: 1px solid var(--border-color);
  border-left: 3px solid var(--border-strong); }
.pane-label { font-size: var(--fs-micro); text-transform: uppercase;
  letter-spacing: 0.12em; font-weight: 700; color: var(--text-faint); }
.pane-value { font-family: 'Oswald', sans-serif; font-weight: 600;
  font-size: var(--fs-md); color: var(--text-main);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pane-sub { font-size: var(--fs-xs); color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Live is gold and the rest is not. One accent, spent on the one pane that is
   actually happening. */
.pane.is-live { border-left-color: var(--sport); background: var(--sport-soft); }
.pane.is-live .pane-value { color: var(--sport-on-soft); }

/* A pane you can go to looks like one. The two that are only readouts do not
   get a pointer, because nothing happens when you press them. */
.pane-link { text-decoration: none; cursor: pointer;
  transition: border-color 0.14s, transform 0.1s; }
.pane-link:hover { border-color: var(--sport-ring); }
.pane-link:active { transform: scale(0.995); }
.pane-link::after { content: "→"; position: absolute; }
/* The arrow is positioned against the pane, so the pane has to be the
   containing block. Without this it lands relative to the page. */
.pane-link { position: relative; }
.pane-link::after { right: var(--sp-4); top: var(--sp-4);
  color: var(--text-faint); font-size: var(--fs-sm); }
.pane-link.is-live::after { color: var(--sport-ink); }

.tonight-count { font-size: var(--fs-xs); color: var(--text-faint); }

/* ===== The organiser's running order =====
   Same left-rail vocabulary as the member's programme, so the two views of the
   same night look like the same night. The difference is the buttons. */
.run-list { display: flex; flex-direction: column; gap: var(--sp-2); }

/* Four columns: number, time, name, actions. */
.run-row { display: grid; align-items: center; gap: var(--sp-3);
  grid-template-columns: auto auto minmax(0, 1fr) auto;
  padding: var(--sp-3); border-radius: 10px;
  background: var(--bg-inset); border: 1px solid var(--border-color);
  border-left: 3px solid var(--border-strong); }
.run-row.is-done { border-left-color: var(--border-strong); opacity: 0.6; }
.run-row.is-live { border-left-color: var(--sport); background: var(--sport-soft); }
.run-row.is-pending { border-left-color: var(--turf); }

.run-seq { font-family: 'Oswald', sans-serif; font-weight: 600;
  font-size: var(--fs-sm); color: var(--text-faint);
  font-variant-numeric: tabular-nums; min-width: 1.5em; text-align: center; }

.run-main { min-width: 0; display: flex; flex-direction: column; gap: 0.2rem; }
/* An input that does not look like a form field until you reach for it: the
   list is read far more often than it is edited, and a column of boxes reads
   as a form rather than as the night. */
.run-name { width: 100%; background: none; border: 1px solid transparent;
  border-radius: 6px; padding: 0.15rem 0.35rem; margin-left: -0.35rem;
  color: var(--text-main); font: inherit; font-weight: 600; }
.run-name:hover { border-color: var(--border-strong); }
.run-name:focus { border-color: var(--sport-ring); background: var(--bg-card);
  outline: none; }
.run-meta { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }

.run-actions { display: flex; align-items: center; gap: var(--sp-2);
  flex-wrap: wrap; justify-content: flex-end; }
/* Move buttons are square and quiet — they are pressed often and should not
   compete with Start, which is the consequential one. */
.run-actions .btn-secondary { min-width: 2.2em; }

/* Adding an item: a name, a kind, a button. Sized to the content rather than
   stretched across the card, same rule as the new-team row. */
.squad-add.is-new-item { display: flex; gap: var(--sp-3); flex-wrap: wrap;
  align-items: center; margin-top: var(--sp-4); }
/* width, not just flex-basis. Every input and select in the app is width:100%
   by default (tokens.css), so flex: 0 0 auto still resolves to a full-width
   box — the kind picker took 633px of its own line and pushed Add onto a
   third. The three controls are one sentence and belong on one line. */
.squad-add.is-new-item input { flex: 1 1 220px; width: auto; max-width: 360px; }
.squad-add.is-new-item select { flex: 0 0 auto; width: auto; min-width: 9rem; }

/* ===== The start time, editable in place =====
   A quiet field until you reach for it, like the name beside it: the running
   order is read far more often than it is edited, and a row of form controls
   reads as a form rather than as the night. */
.run-time-input { width: auto; min-width: 5.6rem; background: none;
  border: 1px solid transparent; border-radius: 6px;
  padding: 0.15rem 0.3rem; color: var(--text-faint);
  font-family: 'Oswald', sans-serif; font-weight: 600; font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums; }
.run-time-input:hover:not(:disabled) { border-color: var(--border-strong);
  color: var(--text-main); }
.run-time-input:focus { border-color: var(--sport-ring); background: var(--bg-card);
  color: var(--text-main); outline: none; }
.run-row.is-live .run-time-input { color: var(--sport-on-soft); }
/* No date on the event means a time has nothing to attach to. Dimmed and
   disabled, with the reason in the tooltip, rather than silently storing a
   time against today. */
.run-time.is-blocked .run-time-input { opacity: 0.4; cursor: not-allowed; }

/* The clock disagrees with the order. A flag, not a refusal — an item really
   can run past midnight. */
.run-row.is-outoforder .run-time-input { color: var(--warning); }
.run-row.is-outoforder .run-time::after { content: "earlier than the one above";
  display: block; font-size: var(--fs-micro); color: var(--warning);
  max-width: 7rem; line-height: 1.2; margin-top: 0.15rem; }

/* Four columns do not fit a phone. Measured at 390px: the name track — the
   1fr one — collapsed to ZERO and every item in the running order lost its
   name, leaving a column of anonymous rows of buttons.

   Organisers carry a phone and walk about with it, so this is not a
   theoretical width. The row breaks into three lines instead: when it is,
   then what it is, then what you can do about it. */
@media (max-width: 720px) {
  .run-row { display: flex; flex-wrap: wrap; align-items: center;
    row-gap: var(--sp-2); }
  .run-seq { order: 1; }
  .run-time { order: 2; }
  .run-main { order: 3; flex: 1 1 100%; }
  .run-actions { order: 4; flex: 1 1 100%; justify-content: flex-start; }
  /* The name is the row's headline once it has its own line. */
  .run-name { font-size: var(--fs-md); }
  /* The out-of-order note sits beside the time here rather than under it,
     where a 7rem block would push the row apart. */
  .run-row.is-outoforder .run-time::after { max-width: none; }
}

/* The programme line under the name. Same quiet-until-you-reach-for-it
   treatment as the name above it, a step smaller because that is what it is. */
.run-desc { width: 100%; background: none; border: 1px solid transparent;
  border-radius: 6px; padding: 0.1rem 0.35rem; margin-left: -0.35rem;
  color: var(--text-faint); font: inherit; font-size: var(--fs-xs); }
.run-desc:hover { border-color: var(--border-strong); }
.run-desc:focus { border-color: var(--sport-ring); background: var(--bg-card);
  color: var(--text-main); outline: none; }

/* ===========================================================================
   The reveal, turned down
   ---------------------------------------------------------------------------
   It was shouting over itself. The award title and the winner's name were both
   at --fs-2xl, so two things competed to be the biggest thing on the beam and
   neither won; the name carried a 40px glow on top of that; and the result
   bars were full-width 10px gold rules that read louder than the names above
   them, with the same gold on every row so the winner was not even the loud
   one. Everything was emphasised, which is the same as nothing being
   emphasised.

   One thing is the headline — the name — and the rest steps down to make room
   for it.
   =========================================================================== */

/* While a result is up, the category is context, not the headline. It keeps
   full size everywhere else, including during voting when it IS the headline. */
.stage-awards .stage-fit:has(.winner-name) .display-title,
.stage-awards .stage-fit:has(.tie-names) .display-title {
  font-size: var(--fs-xl); font-weight: 600; color: var(--text-muted); }

/* Enough glow to feel lit, not enough to bloom on a projector — a beam adds
   its own halo to anything bright, so 40px on screen is considerably more than
   40px in the room. */
.winner-name { text-shadow: 0 0 18px var(--sport-glow); }

/* The lead-in is a spoken line, not a banner. */
.stage-awards .award-instruction { font-size: var(--fs-md); color: var(--text-faint); }

/* Result bars: quiet track, quiet rows, and the gold spent only on the row
   that won. */
.stage-awards .bars { max-width: 48ch; margin-inline: auto; width: 100%; }
.stage-awards .bar-row { gap: 0.3rem 0.7rem; }
.stage-awards .bar-name { font-size: var(--fs-base); font-weight: 500;
  color: var(--text-muted); }
.stage-awards .bar-count { font-size: var(--fs-base); color: var(--text-faint); }
.stage-awards .bar-track { height: 5px; }
.stage-awards .bar-fill { background: var(--border-strong); }
.stage-awards .bar-row.is-winner .bar-name { color: var(--text-main); font-weight: 700; }
.stage-awards .bar-row.is-winner .bar-count { color: var(--sport-ink); }
.stage-awards .bar-row.is-winner .bar-fill { background: var(--sport); }

/* A tie: the names share the billing, because that is what a tie is. Smaller
   than a lone winner — nobody has won yet. */
.tie-names { display: flex; flex-wrap: wrap; gap: var(--sp-4) var(--sp-6);
  justify-content: center; align-items: baseline; }
.tie-name { font-family: 'Oswald', sans-serif; font-weight: 700;
  font-size: var(--fs-xl); line-height: 1.1; color: var(--sport-ink);
  text-transform: uppercase; letter-spacing: 0.02em; }

/* The turnout line under everything is a footnote and was set like a heading. */
.stage-awards .stage-foot .turnout { font-size: var(--fs-sm); color: var(--text-faint); }

/* ===========================================================================
   Reveal alignment
   ---------------------------------------------------------------------------
   .stage-body centres everything, which is right for a question and wrong for
   a result. Measured on a 1920 beam: each nominee's name sat centred in a
   980px box while its count sat 500px away at the right edge, with the bar on
   a line underneath. Nothing lined up with anything, and connecting a name to
   its number meant crossing half the screen.

   A result is a table. Name on the left, bar between, count on the right, all
   on ONE line — so the three columns line up down the list and the eye reads
   across a single row instead of hunting.
   =========================================================================== */
/* ONE grid for the whole list, not one per row.

   Per-row grids were the reason the bars still started in three different
   places after the names were left-aligned: grid columns are shared inside a
   grid container, and every .bar-row was its own container, so max-content
   sized itself to that row's name alone. display:contents dissolves the row
   box and lets all three cells join the parent grid, which is what makes the
   columns line up down the list. Safe here because every .is-winner rule
   targets the CHILDREN, so the row box was never painting anything. */
.stage-awards .bars { max-width: min(100%, 900px);
  display: grid; grid-template-columns: max-content minmax(0, 1fr) max-content;
  align-items: center; gap: var(--sp-3) var(--sp-5); }
.stage-awards .bar-row { display: contents; }
/* The base rule spans the track across the whole row, which is right when the
   bar sits on its own line under the name — and wrong here, where it is the
   middle column. With display:contents that span applied to the PARENT grid
   and scrambled every row. */
.stage-awards .bar-track { grid-column: auto; }
.stage-awards .bar-name { text-align: left; }
.stage-awards .bar-count { text-align: right; min-width: 2ch; }

/* ===== A tie is two people, not one phrase =====
   Two gold names side by side with a gap between them read as a single run of
   text — "Prem Vijay Nair" — which is the one thing a tie screen must not do.
   Each name gets its own card, identical to the other, so the billing is
   visibly equal and the boundary between them is unmistakable. */
/* em, not a spacing token: the gap between two names has to hold its
   proportion to the NAMES, and a token is a fixed length that knows nothing
   about the type sitting either side of it. 0.7em of a 92px name is 64px,
   which still reads as a gap from the back of a hall. */
.tie-names { gap: 0.7em; }
.tie-name { display: flex; align-items: center; justify-content: center;
  min-width: 8ch; padding: var(--sp-4) var(--sp-6);
  border: 1px solid var(--sport-ring); border-radius: var(--radius);
  background: var(--sport-soft); color: var(--sport-on-soft);
  text-shadow: none; }

/* The growth chart lines up with the result above it rather than running the
   full width of the beam — two blocks of unrelated width stacked on each other
   read as two unrelated things. */
.stage-awards .growth { max-width: min(100%, 900px); margin-inline: auto;
  width: 100%; gap: var(--sp-2); }
.stage-awards .growth-track { height: 6vh; }
.stage-awards .growth-legend { font-size: var(--fs-sm); color: var(--text-faint);
  justify-content: center; }

/* ===== Everything in the reveal shares one centre line =====
   A capped block inside a width:100% stack sits against the left edge unless
   it is told otherwise, and a stack of blocks each capped at a different width
   then reads as a ragged column even though each one is individually centred.
   Every measure-capped child gets the auto margins. */
.stage-awards .stage-fit > .stack > * { margin-inline: auto; }

/* ===== A leader is gold, whether or not anyone has won =====
   In a tie nothing carries .is-winner, so every bar was the same grey and the
   two nominees on one vote looked exactly like the three on none. The top of
   the poll is worth marking even when it is shared — that IS the result. */
.stage-awards .bar-row.is-top .bar-name { color: var(--text-main); font-weight: 700; }
.stage-awards .bar-row.is-top .bar-count { color: var(--sport-ink); }
.stage-awards .bar-row.is-top .bar-fill { background: var(--sport); }

/* The centre circle is atmosphere for a quiet screen. On the reveal it runs
   straight through the result — a faint ring crossing five rows of names and
   numbers reads as a smudge on the lens, not as a pitch. The mown stripes
   stay; they are behind everything and cross nothing. */
.stage-awards:has(.bars)::after { display: none; }

/* ===========================================================================
   The reveal, composed
   ---------------------------------------------------------------------------
   Seven roles were resolving to four sizes between 23px and 44px — a 1.9x
   range across the entire screen — so everything on the beam read as the same
   text at the same size and the moment landed like a table of results.

   Each role now takes a distinct step, and the range from the quietest thing
   to the loudest is about 5x. The order is deliberate: the name is the only
   thing the room is there for, the category tells them what it is for, and
   everything else is evidence.
   =========================================================================== */
.stage-awards .award-kind      { font-size: var(--fs-sm); letter-spacing: 0.32em; }
.stage-awards .display-title   { font-size: var(--fs-xl); }
.stage-awards .award-instruction { font-size: var(--fs-lg); font-weight: 300; }
.stage-awards .bar-name        { font-size: var(--fs-md); }
.stage-awards .bar-count       { font-size: var(--fs-md); font-weight: 700; }
.stage-awards .stage-foot .turnout { font-size: var(--fs-xs); }

/* The moment. Tighter tracking than the scale gives, because Oswald at this
   size opens up and a name should read as one object rather than as letters. */
.winner-name { font-size: var(--fs-3xl); letter-spacing: -0.015em; line-height: 1; }
.tie-name    { font-size: var(--fs-2xl); letter-spacing: -0.01em; }

/* ===========================================================================
   Staging
   ---------------------------------------------------------------------------
   A reveal is a sequence, not a page load: the category, then the name, then
   the evidence behind it. Measured end to end it settles in about 1.6s — the
   last bar starts at 0.9s and takes 0.7s to draw — which is long enough to
   feel staged and short enough that a compere talking over it is not waiting
   for the screen.

   Every one of these is `both`, so the element holds its final state. And
   every one is off under prefers-reduced-motion, where they all resolve to
   the finished frame.
   =========================================================================== */
@keyframes rise { from { opacity: 0; transform: translateY(0.4em); } }
@keyframes draw { from { transform: scaleX(0); } }
@keyframes pop  { from { opacity: 0; transform: scale(0.9); } }

.stage-awards .award-kind    { animation: rise 420ms ease-out both; }
.stage-awards .display-title { animation: rise 420ms ease-out 80ms both; }
.stage-awards .award-instruction { animation: rise 420ms ease-out 160ms both; }

/* The name arrives last and with the most travel, which is what makes it feel
   announced rather than rendered. */
.winner-name { animation: winner-in 700ms cubic-bezier(0.16, 1, 0.3, 1) 240ms both; }
.tie-name    { animation: pop 500ms cubic-bezier(0.16, 1, 0.3, 1) both;
  animation-delay: calc(240ms + var(--i, 0) * 110ms); }

/* The result builds under it, a row at a time. --i is set per row in JS: with
   display:contents the rows are not grid items, so nth-child would count the
   cells rather than the nominees. */
.stage-awards .bar-name,
.stage-awards .bar-count { animation: rise 380ms ease-out both;
  animation-delay: calc(420ms + var(--i, 0) * 70ms); }
/* Bars grow from the left, just behind their own row — the count is already
   there, so the bar reads as evidence for it rather than as a loading state. */
.stage-awards .bar-fill { transform-origin: left center;
  animation: draw 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
  animation-delay: calc(480ms + var(--i, 0) * 70ms); }

@media (prefers-reduced-motion: reduce) {
  .stage-awards .award-kind,
  .stage-awards .display-title,
  .stage-awards .award-instruction,
  .stage-awards .bar-name,
  .stage-awards .bar-count,
  .stage-awards .bar-fill,
  .winner-name,
  .tie-name { animation: none; }
}

/* The reveal is the tallest thing this screen ever shows — a name at --fs-3xl
   on top of a row per nominee — and fitStage pays for any overrun by scaling
   the whole composition down, which gives back the size the scale above just
   bought. Tighter vertical rhythm here is worth more than the air: measured
   with seven nominees on a 1080p beam, it moved the fit from 0.80 to 0.83 and
   the winner's name from 107 effective pixels to 111. */
.stage-awards .stage-fit:has(.bars) { gap: 0.9rem; }
.stage-awards .stage-fit:has(.bars) > .stack { gap: 0.9rem; }
.stage-awards .bars { row-gap: var(--sp-2); }

/* ===== The teams, along the bottom =====
   Footnote size and muted, so it can sit under a reveal without arguing with
   it. The team's own colour is the only colour here, and only as a 
   dot — enough to recognise a squad at a glance, not enough to pull the eye
   off whatever the screen is actually for.

   The stage is a fixed grid whose middle row is 1fr, so every pixel this takes
   is one fitStage gives back by scaling the composition. One line, and it
   wraps rather than growing a second one unless it truly has to. */
.stage-foot { flex-direction: column; gap: var(--sp-2); }

.teamline { display: flex; flex-wrap: wrap; justify-content: center;
  align-items: baseline; gap: var(--sp-2) var(--sp-5);
  font-size: var(--fs-xs); color: var(--text-faint); line-height: 1.2; }
.teamline-team { display: inline-flex; align-items: baseline; gap: 0.45em;
  min-width: 0; }
/* Baseline-aligned text beside a dot needs the dot nudged, or it floats. */
.teamline-dot { align-self: center; width: 0.5em; height: 0.5em; flex: none;
  border-radius: 50%; background: var(--team-color, var(--team-default)); }
.teamline-name { white-space: nowrap; text-transform: uppercase;
  letter-spacing: 0.1em; font-weight: 600; }
.teamline-score { font-family: 'Oswald', sans-serif; font-weight: 600;
  color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* It appears once and then stays; the numbers change inside it. Fading the
   whole strip on every score would be a light flickering under the ceremony. */
.teamline { animation: rise 500ms ease-out both; }
@media (prefers-reduced-motion: reduce) { .teamline { animation: none; } }

/* "Up next" is a footnote, not a lead-in. It was using .award-instruction —
   the class that introduces a reveal — so a line about what happens after the
   thing you are currently watching was set at the same size as "And the winner
   is", and on a 1080p beam that is 45px of afterthought. */
.up-next { font-size: var(--fs-sm); color: var(--text-faint);
  letter-spacing: 0.02em; }

/* The quiz's own big screen, covering this one while the quiz is live. Fixed
   and above everything: it replaces the view rather than sitting inside it, so
   there is no doubled header or nested footer. */
.handover { position: fixed; inset: 0; width: 100%; height: 100%;
  border: 0; z-index: 50; background: var(--bg-main); }

/* ---- An open vote, beside whatever is on ----

   Voting for the serious categories is deliberately left open for most of the
   night, and until now opening it took the whole beam: a voting card sat over
   the running order through the meal and the five-a-side. This is the version
   that shares a screen — a card in the corner under the item that is actually
   on, sized to be noticed between mouthfuls rather than read like a team
   sheet.

   The QR keeps its white ground for the same reason .scan-card does: a
   projector lifts black toward grey and eats whatever contrast a tinted code
   had left. Smaller here, because nobody is being asked to scan it this
   second. */
.vote-call { display: flex; align-items: center; gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-4); border-radius: var(--radius);
  background: var(--bg-card); border: 1px solid var(--border-color);
  border-left: 4px solid var(--sport); max-width: 100%;
  animation: rise 500ms ease-out both; }
@media (prefers-reduced-motion: reduce) { .vote-call { animation: none; } }
.vote-call-qr { display: block; width: 16vh; height: 16vh; flex: none;
  padding: 4px; border-radius: 6px; background: var(--scan-ground); }
.vote-call-words { display: flex; flex-direction: column; gap: 0.15rem;
  text-align: left; min-width: 0; }
.vote-call-lead { font-family: 'Oswald', sans-serif; font-weight: 700;
  font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--sport); }
.vote-call-title { font-family: 'Oswald', sans-serif; font-weight: 600;
  font-size: var(--fs-lg); letter-spacing: 0.02em; }
.vote-call-url { font-size: var(--fs-sm); color: var(--text-faint); }

/* ---- The card of the night ----

   Two columns, one grid, so every winner's name starts at the same x down the
   whole list — the same rule as the vote bars, and for the same reason: a
   per-row layout lines each row up with itself and nothing else.

   The three states are told apart by WEIGHT and COLOUR, not by reading the
   label: an announced category is a name in full white, the one being run is
   gold and marked, and the rest recede. From the back of a hall the shape of
   the list should say "we are most of the way through" before a word of it is
   read. */
.roll { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
  gap: var(--sp-2) var(--sp-5); align-items: baseline; width: 100%;
  text-align: left; }
.roll-row { display: contents; }
.roll-title { font-family: 'Oswald', sans-serif; font-weight: 600;
  font-size: var(--fs-md); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-muted); }
.roll-name { font-family: 'Oswald', sans-serif; font-weight: 700;
  font-size: var(--fs-lg); letter-spacing: 0.01em; }
/* Not a name yet — a state. Quieter than any name on the list, so the eye
   lands on what has actually been decided. */
.roll-name.is-pending { font-weight: 500; font-size: var(--fs-sm);
  text-transform: uppercase; letter-spacing: 0.14em; color: var(--text-faint); }
/* The one being run now. Gold, like every other "this is live" in the app. */
.roll-row.is-open .roll-title,
.roll-row.is-announcing .roll-title { color: var(--sport); }
.roll-row.is-open .roll-name,
.roll-row.is-announcing .roll-name { color: var(--sport); }
.roll-row.is-open .roll-name.is-pending,
.roll-row.is-announcing .roll-name.is-pending { color: var(--sport); font-weight: 700; }
/* Still to come: present, so the room can count what is left, but not
   competing with the names of people who have actually won something. */
.roll-row.is-to-come .roll-title { color: var(--text-faint); }

/* The breakdown is as wide as the night is long, so it takes both columns of
   the day grid rather than being squeezed into the narrow one and scrolled
   sideways on a desktop that has the room. */
.day-grid > .card.is-full { grid-column: 1 / -1; }

/* A fixture played before the day. Marked in the header so a team can tell at
   a glance which of their points they walked in with. */
.table th.is-pre { color: var(--sport-ink); }
.table th.is-pre::after { content: ' · before'; font-weight: 500;
  letter-spacing: 0.06em; color: var(--text-faint); }
/* Did not play. Deliberately not a nought: a team that was not in a fixture
   did not score nothing in it. */
.table td.is-empty { color: var(--border-strong); }

/* The roll on a phone. Same component as the beam, one size down: the big
   screen layer rescales it through --scale like everything else. */
.card > .roll { gap: var(--sp-2) var(--sp-4); }
.card .roll-title { font-size: var(--fs-sm); }
.card .roll-name { font-size: var(--fs-base); }
.card .roll-name.is-pending { font-size: var(--fs-micro); }

/* ---- Before the day, and on the night ----

   Two lists under one heading each, rather than one list with a marker on some
   rows. The organiser's question is "what happens tonight", and a fixture
   played three weeks ago answering it — even quietly — is the wrong answer in
   the wrong place. */
.run-group { display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-2) var(--sp-4); margin-top: var(--sp-3);
  font-family: 'Oswald', sans-serif; font-weight: 600; font-size: var(--fs-sm);
  text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-faint); }
.run-group:first-child { margin-top: 0; }
.run-group-hint { font-family: inherit; font-weight: 400; font-size: var(--fs-micro);
  text-transform: none; letter-spacing: 0; color: var(--text-faint); }
/* Already happened. No live border, because nothing about it can be on now. */
.run-row.is-stage-pre { border-left-color: var(--turf); opacity: 1; }
.run-row.is-stage-pre.is-done { opacity: 1; }

/* ---- Putting points on ----

   One box per team and nothing else. The person recording the shootout is
   standing at the edge of a pitch holding a phone, and the whole interaction
   should be "type 3, press the team". */
.points-grid { display: grid; gap: var(--sp-3);
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); }
.points-team { display: grid; grid-template-columns: minmax(0, 1fr) 5.5rem auto;
  align-items: center; gap: var(--sp-3);
  padding: var(--sp-3); border-radius: 10px;
  background: var(--bg-inset); border: 1px solid var(--border-color);
  border-left: 4px solid var(--team-color, var(--team-default)); }
.points-team-name { font-family: 'Oswald', sans-serif; font-weight: 600;
  font-size: var(--fs-base); text-transform: uppercase; letter-spacing: 0.04em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.points-input { width: 100%; text-align: right; font-variant-numeric: tabular-nums; }

.points-entry { display: grid; align-items: center; gap: var(--sp-3);
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.4fr) auto;
  padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border-color); }
.points-entry:last-child { border-bottom: 0; }
.points-entry-team { font-weight: 600; }
.points-entry-value { font-family: 'Oswald', sans-serif; font-weight: 700;
  font-size: var(--fs-base); color: var(--sport-ink); }
.points-entry-note { font-size: var(--fs-sm); color: var(--text-faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Reversed, not removed. The row stays on screen struck through because the
   audit trail is the point — a deleted row cannot be shown to a captain. */
.points-entry.is-reversed { opacity: 0.55; }
.points-entry.is-reversed .points-entry-value { text-decoration: line-through; }

/* ---- Segments of the ceremony ----

   The awards list is grouped by WHEN a category runs — "Fun awards" before the
   food, "Club awards" at the end — with the kinds nested inside. The heading
   carries the count, because "how many are in this segment" is the question
   that follows immediately after "which segment is this". */
#awardRows .mini-h { display: flex; align-items: baseline; gap: var(--sp-3);
  margin-top: var(--sp-4); }
#awardRows .mini-h:first-child { margin-top: 0; }
.mini-h-count { margin-left: auto; font-weight: 600; color: var(--text-faint);
  font-variant-numeric: tabular-nums; letter-spacing: 0; text-transform: none; }
/* The kind, inside a segment. Quieter than the segment heading above it: it is
   a subdivision, and printing it at the same weight made the list read as
   twice as many groups as it has. */
.mini-sub { font-size: var(--fs-micro); font-weight: 500; color: var(--text-faint);
  letter-spacing: 0.04em; margin: var(--sp-2) 0 var(--sp-1); }

/* One big-screen link per segment. A URL carrying a token is read character by
   character off a laptop, so it gets a monospace face and room to wrap. */
.display-link { display: flex; flex-direction: column; gap: 0.15rem;
  padding: var(--sp-2) 0; border-bottom: 1px solid var(--border-color); }
.display-link:last-child { border-bottom: 0; }
.display-link-name { font-family: 'Oswald', sans-serif; font-weight: 600;
  font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: 0.06em; }
.display-link-url { font-family: ui-monospace, 'Cascadia Code', Menlo, monospace;
  font-size: var(--fs-micro); color: var(--text-faint); word-break: break-all; }

/* ---- Picking a shortlist ----

   The directory as chips rather than a text box. Six categories with nine
   eligible members each is fifty-four names typed one at a time, which is
   exactly how four categories reached the week of the event with nobody up. */
.nominee-pick { display: flex; flex-wrap: wrap; gap: var(--sp-2);
  margin-bottom: var(--sp-3); }
.nominee-chip { font: inherit; font-size: var(--fs-sm); cursor: pointer;
  padding: 0.35rem 0.75rem; border-radius: 999px;
  background: var(--bg-inset); color: var(--text-muted);
  border: 1px solid var(--border-color); }
.nominee-chip:hover { border-color: var(--border-strong); }
/* Ticked. Gold, like every other "this is chosen" in the app — and a border
   as well as a fill, so it survives being looked at on a dim laptop screen. */
.nominee-chip.is-on { background: var(--sport-soft); color: var(--sport-on-soft);
  border-color: var(--sport); font-weight: 600; }

/* A nominee who is only a string. The self-vote rule cannot see them and
   nothing joins them to the member holding the phone, so it is worth saying on
   the row rather than leaving it to be found out on the night. */
.nominee-loose { margin-left: var(--sp-2); font-size: var(--fs-micro);
  font-weight: 500; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--warning); }

/* ---- "Tell me when it is my turn" ----

   Sits at the foot of the vote page, because that is where somebody is at the
   moment they learn the thing exists. Quiet at rest: it is an offer, not a
   demand, and a notification prompt that shouts is one that gets denied by
   reflex — and a denied permission cannot be asked for again. */
.notify { display: flex; flex-direction: column; align-items: center;
  gap: var(--sp-2); padding: var(--sp-5) var(--sp-4) var(--sp-6);
  text-align: center; }
.notify-btn { font: inherit; font-size: var(--fs-sm); font-weight: 600;
  cursor: pointer; padding: 0.45rem 1.1rem; border-radius: 999px;
  background: var(--bg-card); color: var(--text-muted);
  border: 1px solid var(--border-strong); }
.notify-btn:hover { border-color: var(--sport); background: var(--sport-soft);
  color: var(--sport-on-soft); }
.notify-btn:disabled { opacity: 0.6; cursor: not-allowed; }
/* Already on. Marked, because "did I turn this on?" is the question the switch
   exists to answer, and an unmarked toggle answers it with a shrug. */
.notify-btn.is-on { background: var(--sport-soft); border-color: var(--sport-ring);
  color: var(--sport-on-soft); }
.notify-btn.is-on::before { content: "\2713\00a0"; }
.notify-hint { font-size: var(--fs-xs); color: var(--text-faint);
  max-width: 34ch; line-height: 1.45; }

/* ---- The final table, at the end of the night ----

   The teamline along the foot carries these same numbers all evening as a
   footnote. At the wrap they are the point, so they are said once at a size
   somebody reads from the back of a hall — and each row carries its team's
   colour, which is how a team is recognised before the name is read.

   Staggered in, using the same --i the vote bars and the tie names use. */
.final-table { display: grid; gap: var(--sp-2); width: 100%;
  max-width: 26ch; margin-inline: auto; }
.final-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: baseline; gap: var(--sp-4);
  padding: var(--sp-2) var(--sp-3); border-radius: var(--radius);
  background: var(--bg-card);
  border-left: 4px solid var(--team-color, var(--team-default));
  animation: rise 500ms ease-out both; animation-delay: calc(var(--i) * 90ms); }
.final-rank { font-family: 'Oswald', sans-serif; font-weight: 700;
  font-size: var(--fs-md); color: var(--text-faint);
  font-variant-numeric: tabular-nums; }
.final-name { font-family: 'Oswald', sans-serif; font-weight: 600;
  font-size: var(--fs-lg); text-transform: uppercase; letter-spacing: 0.04em;
  text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.final-score { font-family: 'Oswald', sans-serif; font-weight: 700;
  font-size: var(--fs-lg); color: var(--sport-ink); }
/* The champion's row, marked the way the leader is marked everywhere else. */
.final-row:first-child { border-color: var(--sport);
  box-shadow: 0 0 24px var(--sport-ring); }
.final-row:first-child .final-rank { color: var(--sport-ink); }
@media (prefers-reduced-motion: reduce) { .final-row { animation: none; } }
