/* ===========================================================================
   FMSS design tokens + shared primitives
   Extracted from fmss-contracts/public/css/styles.css (the FMSS brand block at
   its foot, which supersedes that file's generic indigo/green defaults).

   Dark-first, unlike contracts. Two reasons: FMSS identity is dark, and the
   Main Display is projected into a darkened hall where a light ground would be
   blinding. Light exists as an opt-in for the Master working in a lit room.

   RULE: never write a raw colour below the token block. scripts/check-ui-tokens.js
   fails the build on hardcoded colours, undefined var(--x), and phantom classes.
   =========================================================================== */

:root {
  /* Surfaces — the marketing-site palette. */
  --bg-main: #0A0A0A;
  --bg-card: #121212;
  --bg-inset: #161616;
  --bg-subtle: #1c1c1c;
  --border-color: #222222;
  --border-strong: #333333;

  /* Text */
  --text-main: #FFFFFF;
  --text-muted: #E0E0E0;
  --text-faint: #999999;

  /* Gold — the FMSS accent, in every theme. */
  --sport: #FFD700;
  --sport-deep: #FFC700;
  --sport-ring: rgba(255, 215, 0, 0.22);
  --sport-grad: linear-gradient(135deg, #FFD700 0%, #FFC700 100%);
  --sport-glow: rgba(255, 215, 0, 0.45);
  --accent: #FFD700;
  --accent-hover: #FFC700;
  /* Text sitting ON a dark surface, tinted gold. Raw #FFD700 as body text
     measures ~1.5:1 on light surfaces, so it is mixed toward the ground. */
  --sport-soft: color-mix(in srgb, var(--sport) 12%, var(--bg-card));
  --sport-on-soft: color-mix(in srgb, var(--sport) 72%, white);
  /* Foreground for text ON a solid gold fill — gold is bright in every theme,
     so this stays near-black and is NOT redefined per theme. */
  --on-sport: #0A0A0A;

  /* Gold used AS TEXT, which is a different job from gold used as a fill or a
     border and needs a different value in a light theme. #FFD700 on white is
     about 1.4:1 — it disappears. A bright fill with near-black text on it is
     fine on any ground, which is why --sport itself does not move.
     23 rules said `color: var(--sport)`; they say this instead. */
  --sport-ink: var(--sport);

  /* Turf — the pitch. The mown stripes behind the awards big screen, and the
     second accent that marks a fun award.

     Gold is the trophy and it belongs to the real categories; giving Best
     Beard the same metal as Clubman of the Year is what made the two read as
     the same act. Deliberately NOT --success: green already means "correct"
     and "won" on the quiz screens, and one colour cannot carry two meanings on
     the same night. Dark enough that a projector will not bloom it, saturated
     enough to survive a washed-out beam. */
  --turf: #2F7D52;
  --turf-deep: #1B5E3A;
  --turf-soft: color-mix(in srgb, var(--turf) 16%, var(--bg-card));
  --turf-on-soft: color-mix(in srgb, var(--turf) 45%, white);
  /* The pitch behind the awards screen. These are as strong as a projected
     ground can go: a beam lifts black toward grey and bands any gradient, so
     anything more reads as a washed-out screen rather than as a pitch. */
  --turf-stripe: color-mix(in srgb, var(--turf) 7%, transparent);
  --turf-wash: color-mix(in srgb, var(--turf-deep) 16%, transparent);
  --turf-line: color-mix(in srgb, var(--turf) 22%, transparent);

  /* A QR code has to sit on white — not on a light card. A projector raises
     black toward grey, and a code that starts at 80% contrast arrives at the
     back of the hall with none. This is the one deliberately light surface in
     a dark-first palette, so it is named rather than written inline. */
  --scan-ground: #FFFFFF;

  /* Semantic */
  --danger: #dc2626;
  --on-danger: #fff7f7;
  --warning: #d97706;
  --success: #16a34a;
  --focus: rgba(255, 215, 0, 0.30);

  /* Chrome */
  --topbar-bg: rgba(10, 10, 10, 0.85);
  --toast-bg: #1a1a1a;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.4);
  --shadow-lg: 0 18px 44px rgba(0, 0, 0, 0.55);
  --radius: 14px;

  /* Type scale multiplier. The Main Display overrides this in display.css —
     everything below sizes off it so one number rescales a whole view. */
  --scale: 1;

  /* ---- Type scale ----
     There were THIRTY-ONE distinct font sizes in this app, seventeen of them
     between 0.64rem and 0.95rem. Steps that small are invisible one at a time
     and, together, make every panel look slightly misaligned with every other
     — text that is "not the same size" without ever being a readable
     difference. These eight steps replace all of them.

     Each already carries --scale, so the projector still rescales a whole view
     from one number. Size by ROLE, never by eye: if a new size seems needed,
     the right question is which role it plays. */
  --fs-micro: calc(0.68rem * var(--scale));   /* uppercase eyebrows, tags */
  --fs-xs:    calc(0.76rem * var(--scale));   /* hints, captions, meta */
  --fs-sm:    calc(0.85rem * var(--scale));   /* dense UI: cells, buttons */
  --fs-base:  calc(0.95rem * var(--scale));   /* body */
  --fs-md:    calc(1.1rem  * var(--scale));   /* card titles */
  --fs-lg:    calc(1.35rem * var(--scale));   /* page titles */
  --fs-xl:    calc(1.6rem  * var(--scale));   /* hero */
  --fs-2xl:   calc(2.6rem  * var(--scale));   /* projector */
  /* One step beyond the scale, for the single thing a screen exists to say —
     a winner's name on a projector, and nothing else. The eight steps above
     are a UI scale: their neighbours sit 1.1x apart, which is right for a
     dense admin page and wrong for a composition read across a hall. Measured
     on the reveal before this existed, seven different roles resolved to four
     sizes spanning 23px to 44px, so the whole screen read as one block of
     text at one size. */
  --fs-3xl:   calc(3.1rem  * var(--scale));

  /* ---- Spacing scale ----
     53 distinct padding values and 25 distinct gaps became these seven steps.
     --density is the knob the surfaces turn: see the density block below. */
  --sp-1: calc(0.25rem * var(--density));
  --sp-2: calc(0.4rem  * var(--density));
  --sp-3: calc(0.6rem  * var(--density));
  --sp-4: calc(0.9rem  * var(--density));
  --sp-5: calc(1.25rem * var(--density));
  --sp-6: calc(1.75rem * var(--density));
  --sp-7: calc(2.5rem  * var(--density));

  /* ---- Density ----
     Not every screen wants the same air, and this is the single biggest reason
     the app read as cluttered in some places and wasteful in others.

     1      COMFORTABLE — the default. Phones held by people who are not really
            looking: the programme, the vote, the participant view. Thumb-sized
            targets, generous vertical rhythm, one thing at a time.
     0.72   COMPACT — the control room. A laptop, a mouse, one operator who
            needs the question list, the run controls, every team's answer and
            the hidden answer all visible AT ONCE, mid-sentence, without
            scrolling. Air costs them scrolling, and scrolling costs the show.
     Display sets neither: it turns up --scale instead, because a projector
     needs everything bigger, not tighter. */
  --density: 1;

  /* Real team colours are domain data: they arrive from the DB and are set per
     element as an inline --team-color. This declares the unset default so the
     token linter can see it and so an un-themed team still renders. */
  --team-default: #999999;
  --team-color: var(--team-default);

  /* Position in a list, set per element in JS so a staged animation can delay
     each row by its own index. Declared here for the same reason
     --team-color is: the linter can only see what a stylesheet declares, and
     an element that never gets one still has to animate rather than break. */
  --i: 0;
}

/* Opt-in light theme — token values only, no structural changes. */
:root[data-theme="light"] {
  --bg-main: #f7f8fb;
  --bg-card: #ffffff;
  --bg-inset: #f8fafc;
  --bg-subtle: #f1f3f9;
  --border-color: #e8eaf1;
  --border-strong: #d6d9e4;
  --text-main: #161a26;
  --text-muted: #5b6275;
  --text-faint: #6b7280;
  --sport-soft: color-mix(in srgb, var(--sport) 10%, var(--bg-card));
  --sport-on-soft: color-mix(in srgb, var(--sport) 32%, var(--text-main));
  /* Deep enough to read on white (about 5.4:1) and still recognisably the
     club's gold rather than brown. Fills and borders keep the bright --sport. */
  --sport-ink: #6B5200;
  /* The pitch, on a light ground. The dark-theme values are mixed toward a
     near-black card and turn muddy over white. */
  --turf-on-soft: color-mix(in srgb, var(--turf) 72%, black);
  --turf-stripe: color-mix(in srgb, var(--turf) 5%, transparent);
  --turf-wash: color-mix(in srgb, var(--turf) 8%, transparent);
  --turf-line: color-mix(in srgb, var(--turf) 18%, transparent);
  --topbar-bg: rgba(255, 255, 255, 0.85);
  --toast-bg: #1f2433;
  --shadow-sm: 0 1px 2px rgba(20, 26, 38, 0.04), 0 1px 3px rgba(20, 26, 38, 0.06);
  --shadow-md: 0 4px 12px rgba(20, 26, 38, 0.07), 0 2px 4px rgba(20, 26, 38, 0.04);
  --shadow-lg: 0 18px 44px rgba(20, 26, 38, 0.14);
}

/* ===== Base ===== */

* { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-font-smoothing: antialiased; }

body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: var(--bg-main);
  background-image: linear-gradient(135deg, #0A0A0A 0%, #050505 100%);
  color: var(--text-main);
  line-height: 1.55;
  font-size: var(--fs-base);
  min-height: 100vh;
}
:root[data-theme="light"] body { background-image: none; }

input, select, textarea, button { font-family: inherit; }
[hidden] { display: none !important; }

/* Oswald carries the display voice — wordmark, titles, scores, timers. */
.brand-name, .page-title, .card-title, .display-title, .kpi .v,
.score-value, .timer-value, .mini-h, h1, h2, h3 {
  font-family: 'Oswald', 'Inter', sans-serif;
  letter-spacing: 0.02em;
}

h1 { font-size: var(--fs-lg); font-weight: 700; letter-spacing: -0.01em; }
h2 { font-size: var(--fs-md); font-weight: 700; }
h3 { font-size: var(--fs-md); font-weight: 700; }

/* ===== Brand wordmark ===== */

.brand { display: flex; align-items: center; gap: 0.7rem; }
.brand-logo { width: 42px; height: 42px; object-fit: contain;
  filter: drop-shadow(0 0 8px var(--sport-ring)); }
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-name {
  font-size: var(--fs-md);
  color: var(--sport-ink);
  text-transform: uppercase;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-shadow: 0 0 18px var(--sport-glow);
}
.brand-tag {
  font-size: var(--fs-micro);
  color: var(--sport-ink); opacity: 0.75;
  text-transform: uppercase; letter-spacing: 0.12em; font-weight: 600;
}

/* ===== Connection state =====
   Shared, not per-surface: four pages across both surfaces carry one of these,
   and it lived in quiz.css until the awards big screen — which loads awards.css
   — rendered it unstyled. Anything used by more than one surface belongs here;
   the token linter pools all CSS and structurally cannot catch the difference. */

.conn { display: inline-flex; align-items: center; gap: 0.4rem;
  font-size: var(--fs-micro); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--text-faint); }
.conn::before { content: ""; width: 8px; height: 8px; border-radius: 50%;
  background: var(--text-faint); flex: none; }
.conn.online::before { background: var(--success); }
.conn.connecting::before { background: var(--warning); animation: pulse 1s infinite; }
.conn.offline { color: var(--danger); }
.conn.offline::before { background: var(--danger); }
@keyframes pulse { 50% { opacity: 0.35; } }

/* ===== Layout =====

   THREE widths, because this app has three kinds of page and was giving all of
   them the same 1180px:

   READ (default, 1180) — the programme, the vote. Prose and lists that a person
     scans top to bottom. A longer line is a worse line, so the cap is the point.

   OPERATE (.wrap.is-wide) — admin and quiz setup. A list beside a detail pane,
     driven with a mouse on a laptop or a desk monitor. Capped at 1180 on a 1920
     screen these left 740px of empty background while the detail pane squeezed
     its own contents: /setup ran a 320px list against a 784px editor, /admin a
     340px list against 764px. Same diagnosis as the control room, same fix.

   FORM (.wrap.is-form) — sign-in. The opposite failure: a 1124px-wide card
     holding one select and one PIN field. A form is easier to complete the
     narrower it is, and it belongs in the middle of the screen rather than
     pinned under the top bar.

   A page declares which it is. Nothing infers it from the URL. */
.wrap { width: 100%; max-width: var(--measure, 1180px); margin: 0 auto;
  padding: var(--sp-6);
  display: flex; flex-direction: column; gap: var(--sp-5); }

/* 1720 rather than "no cap": past roughly this, a list-and-detail pair stops
   gaining and the detail pane's own text outruns a comfortable measure. */
.wrap.is-wide { --measure: 1720px; }

.wrap.is-form { --measure: 460px; flex: 1; justify-content: center;
  padding-block: var(--sp-7); }

/* A sign-in that shares a page with something else — /vote signs you in and
   then shows you the awards, both inside one .wrap. The card cannot take the
   page's width, so it takes the form width itself, and the wrap centres it
   while it is the only thing on screen. Measured before this: one select and
   one PIN field spread across 1124px, pinned under the top bar with the rest
   of a 1080px screen empty beneath it. */
.card.is-form { max-width: 460px; width: 100%; margin-inline: auto; }
.wrap:has(> .card.is-form:not([hidden])) { min-height: 72vh; justify-content: center; }

.topbar { display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4); padding: var(--sp-3) var(--sp-6);
  border-bottom: 1px solid var(--border-color);
  position: sticky; top: 0; z-index: 10;
  background: var(--topbar-bg); backdrop-filter: saturate(180%) blur(10px); }
.topbar::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px;
  height: 2px; background: var(--sport-grad); opacity: 0.9; }
.topbar-titles { display: flex; flex-direction: column; line-height: 1.2; min-width: 0; }
.page-title { font-size: var(--fs-md); font-weight: 700; }
.page-context { font-size: var(--fs-xs); color: var(--text-faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.topbar-right { display: flex; align-items: center; gap: var(--sp-3);
  flex-wrap: wrap; justify-content: flex-end; }

/* ===== Card ===== */

.card { position: relative; background: var(--bg-card);
  border: 1px solid var(--border-color); border-radius: var(--radius);
  padding: var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-4);
  box-shadow: var(--shadow-sm); }
.card-header { display: flex; justify-content: space-between; align-items: center;
  gap: var(--sp-2); flex-wrap: wrap;
  padding-bottom: var(--sp-3); border-bottom: 1px solid var(--border-color); }
.card-title { font-size: var(--fs-md); font-weight: 700;
  display: flex; align-items: center; gap: 0.55rem; }
.card-title::before { content: ""; width: 4px; height: 18px; border-radius: 3px;
  background: var(--sport-grad); flex: none; }
.card-sub { font-size: var(--fs-xs); color: var(--text-faint); font-weight: 500; }

/* ===== Buttons ===== */

.btn { background: var(--sport); color: var(--on-sport);
  padding: 0.58rem 1.15rem; border: none; border-radius: 9px;
  font-weight: 700; font-size: var(--fs-sm); cursor: pointer;
  box-shadow: 0 4px 12px var(--sport-ring);
  display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
  transition: transform 0.14s, box-shadow 0.14s, background 0.14s; }
.btn:hover { background: var(--sport-deep); transform: translateY(-1px);
  box-shadow: 0 6px 18px var(--sport-ring); }
.btn:active { transform: translateY(0); }
.btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--focus); }
.btn:disabled, .btn.loading { opacity: 0.65; cursor: not-allowed; pointer-events: none; }

.btn-secondary { background: var(--bg-card); border: 1px solid var(--border-strong);
  color: var(--text-main); box-shadow: var(--shadow-sm); font-weight: 600; }
.btn-secondary:hover { background: var(--sport-soft); border-color: var(--sport-ring);
  color: var(--sport-on-soft); }
.btn-danger { background: var(--bg-card); border: 1px solid var(--border-strong);
  color: var(--text-muted); box-shadow: none; font-weight: 600; }
.btn-danger:hover { background: color-mix(in srgb, var(--danger) 12%, var(--bg-card));
  border-color: var(--danger); color: var(--danger); }
.btn-sm { padding: 0.38rem 0.75rem; font-size: var(--fs-sm); }
.btn-lg { padding: 0.9rem 1.8rem; font-size: var(--fs-md); border-radius: 12px; }
.full-w { width: 100%; }

/* ===== Forms ===== */

label { font-size: var(--fs-xs); color: var(--text-muted); font-weight: 600; }
.form-group { display: flex; flex-direction: column; gap: var(--sp-2); }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-4); }

/* Text-ish controls only. A checkbox or radio caught by this rule becomes a
   full-width block and drops onto its own line under its label — which is what
   happened to every toggle on a page that did not also load awards.css. */
/* One :not() taking a selector LIST, not three chained ones. Chained, this
   selector scores 0-3-1 and silently outranks any single class trying to size
   an input — which is how the score-adjust box ended up full width and
   crushing the judging row. The list form scores 0-1-1, so a class in a later
   stylesheet can still win. */
input:not([type="checkbox"], [type="radio"], [type="color"]),
select, textarea {
  background: var(--bg-inset); border: 1px solid var(--border-strong);
  color: var(--text-main); padding: 0.58rem 0.72rem; border-radius: 9px;
  font-size: var(--fs-base); width: 100%;
  transition: border-color 0.15s, box-shadow 0.15s, background 0.15s; }
input::placeholder, textarea::placeholder { color: var(--text-faint); }
input:focus, select:focus, textarea:focus { border-color: var(--sport); outline: none;
  background: var(--bg-card); box-shadow: 0 0 0 3px var(--sport-ring); }
input.error, select.error, textarea.error { border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 8%, var(--bg-card)); }

.form-error { font-size: var(--fs-xs); color: var(--danger);
  margin-top: 0.3rem; font-weight: 500; min-height: 1rem; }

/* The join-code field: read aloud to a room, typed on a phone. Wide tracking
   and a monospace-ish feel make a mistyped character obvious. */
.code-input { font-family: 'Oswald', sans-serif; font-size: var(--fs-xl);
  font-weight: 600; letter-spacing: 0.3em; text-align: center; text-transform: uppercase;
  padding: 0.7rem 0.5rem; }

/* ===== Tags & pills ===== */

.tag { display: inline-flex; padding: 0.12rem 0.6rem; border-radius: 9999px;
  font-size: var(--fs-xs); font-weight: 600;
  border: 1px solid var(--border-strong); color: var(--text-muted); background: var(--bg-inset); }
.tag-live { color: color-mix(in srgb, var(--success) 72%, var(--text-main));
  border-color: color-mix(in srgb, var(--success) 35%, var(--bg-card));
  background: color-mix(in srgb, var(--success) 13%, var(--bg-card)); }
.tag-wait { color: color-mix(in srgb, var(--warning) 72%, var(--text-main));
  border-color: color-mix(in srgb, var(--warning) 35%, var(--bg-card));
  background: color-mix(in srgb, var(--warning) 13%, var(--bg-card)); }
.tag-locked { color: color-mix(in srgb, var(--danger) 72%, var(--text-main));
  border-color: color-mix(in srgb, var(--danger) 35%, var(--bg-card));
  background: color-mix(in srgb, var(--danger) 13%, var(--bg-card)); }
.tag-gold { color: var(--sport-on-soft); border-color: var(--sport-ring);
  background: var(--sport-soft); }

/* Team identity dot — colour comes from the DB via an inline --team-color. */
.team-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%;
  margin-right: 0.4rem; vertical-align: middle;
  background: var(--team-color, var(--team-default)); }

/* ===== Key/value rows ===== */

.mini-h { font-size: var(--fs-micro); font-weight: 700;
  color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em;
  margin-bottom: var(--sp-3); }

.kv { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem;
  padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);
  font-size: var(--fs-base); }
.kv:last-child { border-bottom: none; }
.kv .k { display: flex; align-items: center; color: var(--text-muted); min-width: 0; }
.kv .v { font-weight: 700; font-variant-numeric: tabular-nums; }

/* A short code meant to be read aloud and typed in — join codes, PINs. Wide
   tracking so similar-looking characters are unambiguous across a room. */
.pin-chip { display: inline-block; font-family: 'Oswald', sans-serif; font-weight: 600;
  letter-spacing: 0.14em; font-size: var(--fs-base);
  color: var(--sport-ink); background: var(--sport-soft);
  border: 1px solid var(--sport-ring); padding: 0.1rem 0.6rem; border-radius: 7px; }

/* ===== Numbers ===== */

.num, .score-value { font-variant-numeric: tabular-nums; white-space: nowrap; }
.score-value { font-weight: 800; letter-spacing: -0.02em; color: var(--sport-on-soft); }
.score-value.up { color: var(--success); }
.score-value.down { color: var(--danger); }

/* ===== Segmented control ===== */

.seg { display: inline-flex; gap: 0.3rem; background: var(--bg-subtle);
  padding: 0.25rem; border-radius: 11px; border: 1px solid var(--border-color); }
.seg button { background: none; border: none; cursor: pointer;
  padding: 0.4rem 0.9rem; border-radius: 8px;
  font-size: var(--fs-sm); font-weight: 600; color: var(--text-muted); }
.seg button.active { background: var(--bg-card); color: var(--sport-on-soft);
  box-shadow: var(--shadow-sm); }

/* ===== Table ===== */

.table { width: 100%; border-collapse: collapse; text-align: left;
  font-size: var(--fs-sm); }
.table th { color: var(--text-faint); font-weight: 700;
  font-size: var(--fs-micro); text-transform: uppercase;
  letter-spacing: 0.06em; padding: 0.6rem 0.8rem;
  border-bottom: 1px solid var(--border-color); }
.table td { padding: 0.85rem 0.8rem; border-bottom: 1px solid var(--border-color);
  vertical-align: middle; }
.table tr:last-child td { border-bottom: none; }
.table tbody tr:hover { background: var(--sport-soft); }

/* Wide content must scroll inside itself, never the page body. */
.scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ===== Feedback ===== */

.spinner { display: inline-block; width: 14px; height: 14px;
  border: 2px solid var(--border-strong); border-top-color: var(--sport);
  border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

.toast { position: fixed; bottom: 1.5rem; right: 1.5rem; background: var(--toast-bg);
  color: var(--text-main); padding: 0.75rem 1.15rem; border-radius: 10px;
  font-size: var(--fs-sm); box-shadow: var(--shadow-lg);
  opacity: 0; transform: translateY(10px); z-index: 50;
  display: flex; align-items: center; gap: 0.6rem; max-width: 400px;
  transition: opacity 0.2s, transform 0.2s; }
.toast.show { opacity: 1; transform: translateY(0); }
.toast.err { background: var(--danger); color: var(--on-danger); }

.empty-state { text-align: center; padding: 2.5rem 1.5rem; color: var(--text-faint); }
.empty-state-title { font-size: var(--fs-base); font-weight: 600;
  color: var(--text-muted); margin-bottom: 0.3rem; }
.empty-state-desc { font-size: var(--fs-sm); line-height: 1.5; }

.muted { color: var(--text-muted); font-size: var(--fs-sm); }
.hint { font-size: var(--fs-xs); color: var(--text-faint); line-height: 1.5; }

/* ===== Centred single-panel views (join screen, display standby) ===== */

.centre { min-height: 100vh; display: flex; align-items: center; justify-content: center;
  padding: 2rem; position: relative; overflow: hidden; }
.centre::before { content: ""; position: absolute; inset: -20% -20% auto auto;
  width: 460px; height: 460px; background: var(--sport-grad);
  filter: blur(80px); opacity: 0.12; border-radius: 50%; pointer-events: none; }
.panel { position: relative; width: 100%; max-width: 400px; background: var(--bg-card);
  border: 1px solid var(--border-color); border-radius: 16px; padding: 2.5rem;
  box-shadow: var(--shadow-lg); }
.panel-head { text-align: center; margin-bottom: 1.85rem; }
.panel-logo { width: 76px; height: 76px; object-fit: contain; display: block;
  margin: 0 auto 0.9rem; filter: drop-shadow(0 0 12px var(--sport-glow)); }
.panel-title { font-family: 'Oswald', sans-serif; font-size: var(--fs-xl);
  color: var(--sport-ink); text-transform: uppercase; letter-spacing: 0.16em;
  text-shadow: 0 0 18px var(--sport-glow); }
.panel-sub { color: var(--text-muted); font-size: var(--fs-sm); margin-top: 0.35rem; }
.stack { display: flex; flex-direction: column; gap: var(--sp-4); }

/* ===== Responsive ===== */

/* A phone is already narrow; it does not also need to be tight. Rather than
   respecifying every padding, narrow screens simply turn the density knob
   down a little — the proportions the scale sets are preserved. */
@media (max-width: 820px) {
  :root { --density: 0.8; }
  .form-row { grid-template-columns: 1fr; }
  .wrap { padding: var(--sp-4); }
  .panel { padding: var(--sp-5); }
}

/* ===== Explanation: callouts and tooltips =====

   Two mechanisms, deliberately split by device.

   `.callout` is VISIBLE text. It is what the phone views use, because a hover
   tooltip cannot be reached on a touchscreen — and the phone views are the ones
   held by fifty people who have never seen this app and will not be trained.

   `[data-tip]` is a hover tooltip, used only on the control-room screens, which
   are driven on a laptop by one person with a mouse. It carries the detail that
   would clutter a dense panel if it were always on screen. Nothing essential
   may live in a tooltip alone. */

.callout { display: flex; gap: 0.6rem; align-items: flex-start;
  padding: 0.65rem 0.8rem; border-radius: 9px;
  border: 1px solid var(--border-color); background: var(--bg-subtle);
  font-size: var(--fs-sm); line-height: 1.5; color: var(--text-muted); }
.callout-mark { flex: none; font-weight: 800; color: var(--sport-ink);
  font-family: 'Oswald', sans-serif; }
.callout strong { color: var(--text-main); font-weight: 600; }
.callout.is-gold { border-color: var(--sport-ring); background: var(--sport-soft); }
.callout.is-warn { border-color: color-mix(in srgb, var(--warning) 35%, var(--bg-card));
  background: color-mix(in srgb, var(--warning) 10%, var(--bg-card)); }

/* A one-line lead-in above a group of controls, saying what the group is for. */
.group-label { display: flex; align-items: baseline; gap: 0.5rem;
  font-size: var(--fs-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-faint);
  margin-bottom: 0.45rem; }
.group-label .group-why { text-transform: none; letter-spacing: 0;
  font-weight: 400; color: var(--text-faint); }

/* Tooltip. Pure CSS so it cannot break the page, and pointer-events: none so it
   never intercepts the click on the control it describes. */
[data-tip] { position: relative; }
[data-tip]::after {
  content: attr(data-tip);
  position: absolute; bottom: calc(100% + 7px); left: 50%;
  transform: translateX(-50%) translateY(3px);
  min-width: 8rem; max-width: 17rem; width: max-content;
  padding: 0.45rem 0.65rem; border-radius: 8px;
  background: var(--toast-bg); color: var(--text-main);
  border: 1px solid var(--border-strong); box-shadow: var(--shadow-lg);
  font-size: var(--fs-xs); font-weight: 400; line-height: 1.45;
  text-align: left; text-transform: none; letter-spacing: 0;
  white-space: normal; pointer-events: none;
  opacity: 0; transition: opacity 0.14s, transform 0.14s; z-index: 60;
}
[data-tip]:hover::after, [data-tip]:focus-visible::after {
  opacity: 1; transform: translateX(-50%) translateY(0);
}
/* Near the left or right edge of a panel the centred balloon would overflow. */
[data-tip].tip-left::after { left: 0; transform: translateX(0) translateY(3px); }
[data-tip].tip-left:hover::after,
[data-tip].tip-left:focus-visible::after { transform: translateX(0) translateY(0); }

/* A tooltip is invisible until you know it is there, so anything carrying one
   that is not already an obvious control gets a quiet marker. */
.tip-mark { border-bottom: 1px dotted var(--text-faint); cursor: help; }

@media (hover: none) {
  /* No hover on a touchscreen: suppress the balloon rather than leave it
     half-triggered by a tap. The visible .callout text carries the meaning. */
  [data-tip]::after { display: none; }
}

/* ===== Cross-view navigation =====
   The six views live on two hostnames and had no links between them at all.
   Rendered from /api/me's `panels`, so a link is never offered to someone who
   would be refused when they click it. */
.view-nav { position: relative; }

/* Who you are and what you hold, as the thing you press. A row of bare words
   said where you could go and nothing about why you could go there — someone
   with one role and someone with three saw identical pills. */
.view-nav-toggle { display: inline-flex; align-items: baseline; gap: var(--sp-2);
  cursor: pointer; border: 1px solid var(--border-color); background: var(--bg-subtle);
  border-radius: 999px; padding: 0.28rem 0.75rem; color: var(--text-main);
  font-size: var(--fs-xs); font-weight: 600; }
.view-nav-toggle:hover { border-color: var(--sport-ring); background: var(--sport-soft); }
.view-nav-role { color: var(--sport-on-soft); font-weight: 500; font-size: var(--fs-micro); }
.view-nav-caret { color: var(--text-faint); font-size: var(--fs-micro); }

.view-nav-menu { position: absolute; right: 0; top: calc(100% + 8px); z-index: 40;
  min-width: 17rem; max-width: min(22rem, calc(100vw - 2rem));
  display: flex; flex-direction: column;
  background: var(--bg-card); border: 1px solid var(--border-strong);
  border-radius: 12px; box-shadow: var(--shadow-lg); overflow: hidden; }

/* What each role actually lets you do, said once, at the top. */
.view-nav-roles { padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--border-color); background: var(--bg-subtle); }
.view-nav-why { font-size: var(--fs-micro); color: var(--text-faint); line-height: 1.5; }
.view-nav-why b { color: var(--sport-on-soft); font-weight: 700; }

.view-nav-item { display: flex; flex-direction: column; gap: 0.1rem;
  padding: var(--sp-3) var(--sp-4); text-decoration: none; color: var(--text-main);
  border-bottom: 1px solid var(--border-color); }
.view-nav-item:last-child { border-bottom: none; }
.view-nav-item:hover { background: var(--sport-soft); }
.view-nav-item.is-here { background: var(--bg-inset); cursor: default; }
.view-nav-item.is-here:hover { background: var(--bg-inset); }
.view-nav-item-name { display: flex; align-items: baseline; gap: var(--sp-2);
  font-size: var(--fs-sm); font-weight: 600; }
.view-nav-here { font-size: var(--fs-micro); font-weight: 600; color: var(--sport-ink);
  text-transform: uppercase; letter-spacing: 0.06em; }
.view-nav-item-blurb { font-size: var(--fs-micro); color: var(--text-faint); line-height: 1.45; }

/* ===== Surface tabs =====
   The two public jobs of the annual day surface: the function, and the vote.
   They are separate pages because they are separate tasks — one is a board you
   leave open, the other is something you are sent to do. */
.tabbar { display: flex; gap: 0.15rem; padding: 0 1.5rem;
  border-bottom: 1px solid var(--border-color); background: var(--bg-card);
  overflow-x: auto; -webkit-overflow-scrolling: touch; }
/* Works as an <a> on the programme surface and a <button> on the admin one, so
   it has to strip the button chrome the browser supplies — without this the
   admin tabs rendered as grey boxes rather than a tab strip. */
.tab { flex: none; text-decoration: none; padding: 0.7rem 0.95rem;
  background: none; border: none; cursor: pointer; font-family: inherit;
  font-size: var(--fs-sm); font-weight: 600;
  color: var(--text-muted); border-bottom: 2px solid transparent;
  white-space: nowrap; transition: color 0.15s, border-color 0.15s; }
.tab:focus-visible { outline: 2px solid var(--sport); outline-offset: -2px; }
.tab:hover { color: var(--text-main); }
.tab.active { color: var(--sport-ink); border-bottom-color: var(--sport); }

/* ===== Call to action =====
   A whole-row link, sized to be hit with a thumb without looking. */
.cta { display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; text-decoration: none; padding: 0.9rem 1.15rem;
  border-radius: var(--radius); border: 1px solid var(--sport-ring);
  background: var(--sport-soft); transition: border-color 0.15s, background 0.15s; }
.cta:hover { border-color: var(--sport); }
.cta-text { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.cta-title { font-family: 'Oswald', sans-serif; font-weight: 600;
  font-size: var(--fs-md); color: var(--sport-ink);
  text-transform: uppercase; letter-spacing: 0.05em; }
.cta-sub { font-size: var(--fs-sm); color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cta-go { flex: none; font-weight: 700; font-size: var(--fs-sm);
  color: var(--on-sport); background: var(--sport);
  border-radius: 999px; padding: 0.4rem 1.1rem; }

@media (max-width: 820px) {
  .tabbar { padding: 0 1.1rem; }
}


/* ===== Density modes =====
   See --density in the token block. This is the switch the control-room pages
   throw: the same components, the same type scale, less air — because that
   operator needs the question list, the run controls, every team's answer and
   the hidden answer on screen AT ONCE while talking to a room. */
body.dense { --density: 0.72; }
/* ...except the centred single-task screens. Signing in is one field at a
   time with nothing competing for the space; compacting it buys nothing and
   makes the one thing you came to do look cramped. */
body.dense .centre { --density: 1; }

/* Compact surfaces also drop one type step on the densest text, so a tighter
   box does not simply crowd the same words. Body text is untouched: this is
   about labels and chrome, never about the words that carry meaning. */
body.dense .card-sub,
body.dense .page-context { font-size: var(--fs-micro); }
body.dense .card { border-radius: 11px; }


/* ===== Checkbox toggle =====
   Shared, because both surfaces use it. It used to live in awards.css alone,
   which meant every toggle on the quiz surface rendered unstyled — the token
   linter cannot catch that, since it reads all stylesheets as one pool and
   never sees which page loads which. */
.toggle-row { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); }
.toggle { display: inline-flex; align-items: center; gap: var(--sp-2);
  cursor: pointer; font-size: var(--fs-sm); color: var(--text-muted); }
.toggle input { width: 16px; height: 16px; accent-color: var(--sport); flex: none; }

/* ===== Field grid =====
   Two columns of short fields beat one long column: a whole settings block
   should be readable without scrolling. Shared, because both the quiz setup
   screen and the organiser screen lay out forms this way — it lived in
   quiz.css, which the organiser screen does not load, so it rendered as a
   plain stack there. The token linter cannot catch that: it reads every
   stylesheet as one pool and never sees which page loads which. */
.field-grid { display: grid; gap: var(--sp-3) var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.field-grid .hint { margin-top: 0.1rem; }

/* ===== Theme switch =====
   Quiet: it is used once, if ever, and then wants to be invisible. Sized to a
   real touch target anyway, because it sits in a top bar people tap on a
   phone. */
.theme-toggle { flex: none; display: inline-grid; place-items: center;
  width: 2rem; height: 2rem; border-radius: 8px; cursor: pointer;
  background: none; border: 1px solid var(--border-color);
  color: var(--text-faint); font-size: var(--fs-base); line-height: 1;
  transition: color 0.14s, border-color 0.14s, background 0.14s; }
.theme-toggle:hover { color: var(--sport-ink); border-color: var(--sport-ring);
  background: var(--sport-soft); }
.theme-toggle:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
