/* Brackets · Material 3
   ===========================================================================
   A hand-rolled Material 3 implementation rather than @material/web.

   That is a deliberate trade and it is the same trade the whole product makes.
   The component library is ~300kB of JavaScript before a single bracket is
   drawn, it ships its own custom-element runtime, and every control becomes a
   shadow-DOM boundary you cannot style around at 2am when a station card is
   one pixel too tall on someone's phone. The complaint that started this
   project was PERFORMANCE, and shipping a component framework to solve a
   design-language problem would have been the first thing to break the pitch.

   So: M3's tokens, type scale, state layers, elevation and shape are
   implemented here in plain CSS -- 44kB as written, 11kB gzipped, and zero
   JavaScript. What is NOT implemented is anything M3 specifies that this app
   does not use. There is no date picker here because nothing needed one.

   No webfont either, for the same reason. `Roboto` is named first so Android
   and anyone with it installed gets the real thing; everyone else gets their
   system UI face. M3's type scale is a set of sizes, weights and tracking
   values, and those are what carry the language -- a 170kB font download on
   venue wifi is not worth the last 5% of fidelity.

   ---------------------------------------------------------------------------
   ORGANISATION
     1. Tonal palettes        — the raw tones, one per key colour
     2. Colour roles          — light and dark schemes built from the tones
     3. Type scale, shape, elevation, motion
     4. Base and layout
     5. Components            — buttons, fields, cards, chips, nav, dialogs…
     6. App-specific          — bracket grid, station cards, entrant table
   =========================================================================== */

/* ===========================================================================
   1. TONAL PALETTES
   ---------------------------------------------------------------------------
   M3 generates 13 tones per key colour from a seed. These are that generation
   for a cyan-teal seed, chosen because BattyDev's existing pages are built
   around #66edf1 and a sub-site should look like it belongs to the site.

   Tertiary is a warm gold, and it is not decorative: in this app tertiary
   means "the organiser needs to look at this". Keeping attention on its own
   role -- rather than reaching for error red, which should mean something has
   actually gone wrong -- is what lets the run view have three levels of
   urgency without shouting.
   =========================================================================== */
:root {
  /* primary — cyan-teal */
  --p-0:#000000;   --p-10:#001f23; --p-20:#00363c; --p-25:#004147; --p-30:#004f57;
  --p-40:#006874;  --p-50:#008394; --p-60:#00a0b4; --p-70:#22bcd3; --p-80:#4fd8f0;
  --p-90:#a6eeff;  --p-95:#d5f6ff; --p-98:#f0fbff; --p-99:#f8fdff; --p-100:#ffffff;

  /* secondary — the same hue, drained, for quieter surfaces */
  --s-10:#051f23; --s-20:#1b3438; --s-25:#263f43; --s-30:#324a4f; --s-40:#496266;
  --s-50:#617a7f; --s-60:#7a9499; --s-70:#94afb4; --s-80:#afcbd0; --s-90:#cbe8ed;
  --s-95:#d9f6fb; --s-98:#f0fbff;

  /* tertiary — warm gold. "Needs your attention", not "broken". */
  --t-10:#251a00; --t-20:#3f2e00; --t-25:#4c3800; --t-30:#5a4300; --t-40:#785a00;
  --t-50:#977200; --t-60:#b78c00; --t-70:#d8a600; --t-80:#efc048; --t-90:#ffdf9b;
  --t-95:#ffefd5; --t-98:#fff8f2;

  /* error — M3's reference error palette */
  --e-10:#410002; --e-20:#690005; --e-25:#7e0007; --e-30:#93000a; --e-40:#ba1a1a;
  --e-50:#de3730; --e-60:#ff5449; --e-70:#ff897d; --e-80:#ffb4ab; --e-90:#ffdad6;
  --e-95:#ffedea; --e-98:#fff8f7;

  /* neutral */
  --n-0:#000000;  --n-4:#0b0f10;  --n-6:#101416;  --n-10:#191c1d; --n-12:#1d2021;
  --n-17:#272a2b; --n-20:#2d3132; --n-22:#313436; --n-24:#353839; --n-30:#444748;
  --n-40:#5c5f60; --n-50:#757779; --n-60:#8f9193; --n-70:#a9abad; --n-80:#c4c7c8;
  --n-87:#d8dbdc; --n-90:#e1e3e4; --n-92:#e7e9ea; --n-94:#edefef; --n-95:#eff1f2;
  --n-96:#f2f4f5; --n-98:#f8fafa; --n-99:#fbfcfd; --n-100:#ffffff;

  /* neutral variant — outlines and the surface-variant family */
  --nv-10:#131d20; --nv-20:#283134; --nv-30:#3e484a; --nv-40:#556061;
  --nv-50:#6d797a; --nv-60:#879294; --nv-70:#a1adae; --nv-80:#bdc8ca;
  --nv-90:#d9e4e6; --nv-95:#e7f2f4;
}

/* ===========================================================================
   2. COLOUR ROLES
   ---------------------------------------------------------------------------
   Light is the bare :root so it is the fallback everywhere. Dark is applied
   both by the OS preference and by an explicit [data-theme] on <html>, because
   a venue is dark and a phone in a venue is usually on auto -- but a TO who
   wants the bracket bright on a projector should be able to force it.
   =========================================================================== */
:root {
  color-scheme: light;

  --md-primary: var(--p-40);
  --md-on-primary: var(--p-100);
  --md-primary-container: var(--p-90);
  --md-on-primary-container: var(--p-10);

  --md-secondary: var(--s-40);
  --md-on-secondary: var(--n-100);
  --md-secondary-container: var(--s-90);
  --md-on-secondary-container: var(--s-10);

  --md-tertiary: var(--t-40);
  --md-on-tertiary: var(--n-100);
  --md-tertiary-container: var(--t-90);
  --md-on-tertiary-container: var(--t-10);

  --md-error: var(--e-40);
  --md-on-error: var(--n-100);
  --md-error-container: var(--e-90);
  --md-on-error-container: var(--e-10);

  --md-surface: var(--n-98);
  --md-on-surface: var(--n-10);
  --md-surface-variant: var(--nv-90);
  --md-on-surface-variant: var(--nv-30);

  --md-surface-container-lowest: var(--n-100);
  --md-surface-container-low: var(--n-96);
  --md-surface-container: var(--n-94);
  --md-surface-container-high: var(--n-92);
  --md-surface-container-highest: var(--n-90);

  --md-outline: var(--nv-50);
  --md-outline-variant: var(--nv-80);

  --md-inverse-surface: var(--n-20);
  --md-inverse-on-surface: var(--n-95);
  --md-inverse-primary: var(--p-80);

  --md-scrim: #000000;
  --md-shadow: #000000;

  /* Semantic aliases. Components below reference these rather than the raw
     roles, so a state's colour is changed in one place -- and so that reading
     the component CSS tells you what a colour MEANS. */
  --ok: #2e6b41;
  --ok-container: #b6f0c4;
  --on-ok-container: #00210e;
  --warn: var(--md-tertiary);
  --warn-container: var(--md-tertiary-container);
  --on-warn-container: var(--md-on-tertiary-container);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) { color-scheme: dark; }
}

/* The dark scheme, written once and applied from both triggers. Duplicating
   the block rather than using a shared class keeps it a pure token swap --
   nothing downstream ever needs to know which theme is on. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --md-primary: var(--p-80);
    --md-on-primary: var(--p-20);
    --md-primary-container: var(--p-30);
    --md-on-primary-container: var(--p-90);

    --md-secondary: var(--s-80);
    --md-on-secondary: var(--s-20);
    --md-secondary-container: var(--s-30);
    --md-on-secondary-container: var(--s-90);

    --md-tertiary: var(--t-80);
    --md-on-tertiary: var(--t-20);
    --md-tertiary-container: var(--t-30);
    --md-on-tertiary-container: var(--t-90);

    --md-error: var(--e-80);
    --md-on-error: var(--e-20);
    --md-error-container: var(--e-30);
    --md-on-error-container: var(--e-90);

    --md-surface: var(--n-6);
    --md-on-surface: var(--n-90);
    --md-surface-variant: var(--nv-30);
    --md-on-surface-variant: var(--nv-80);

    --md-surface-container-lowest: var(--n-4);
    --md-surface-container-low: var(--n-10);
    --md-surface-container: var(--n-12);
    --md-surface-container-high: var(--n-17);
    --md-surface-container-highest: var(--n-22);

    --md-outline: var(--nv-60);
    --md-outline-variant: var(--nv-30);

    --md-inverse-surface: var(--n-90);
    --md-inverse-on-surface: var(--n-20);
    --md-inverse-primary: var(--p-40);

    --ok: #7fd79b;
    --ok-container: #00522a;
    --on-ok-container: #b6f0c4;
  }
}

:root[data-theme='dark'] {
  color-scheme: dark;
  --md-primary: var(--p-80);
  --md-on-primary: var(--p-20);
  --md-primary-container: var(--p-30);
  --md-on-primary-container: var(--p-90);
  --md-secondary: var(--s-80);
  --md-on-secondary: var(--s-20);
  --md-secondary-container: var(--s-30);
  --md-on-secondary-container: var(--s-90);
  --md-tertiary: var(--t-80);
  --md-on-tertiary: var(--t-20);
  --md-tertiary-container: var(--t-30);
  --md-on-tertiary-container: var(--t-90);
  --md-error: var(--e-80);
  --md-on-error: var(--e-20);
  --md-error-container: var(--e-30);
  --md-on-error-container: var(--e-90);
  --md-surface: var(--n-6);
  --md-on-surface: var(--n-90);
  --md-surface-variant: var(--nv-30);
  --md-on-surface-variant: var(--nv-80);
  --md-surface-container-lowest: var(--n-4);
  --md-surface-container-low: var(--n-10);
  --md-surface-container: var(--n-12);
  --md-surface-container-high: var(--n-17);
  --md-surface-container-highest: var(--n-22);
  --md-outline: var(--nv-60);
  --md-outline-variant: var(--nv-30);
  --md-inverse-surface: var(--n-90);
  --md-inverse-on-surface: var(--n-20);
  --md-inverse-primary: var(--p-40);
  --ok: #7fd79b;
  --ok-container: #00522a;
  --on-ok-container: #b6f0c4;
}

/* ===========================================================================
   3. TYPE, SHAPE, ELEVATION, MOTION
   =========================================================================== */
:root {
  /* M3 type scale. Tracking (letter-spacing) is part of the spec, not a
     flourish -- label-small at 0.5px is what stops a 11px chip label turning
     into a smear on a low-DPI projector. */
  --font: Roboto, 'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif;
  --font-mono: 'Roboto Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;

  --display-large: 700 clamp(2.5rem, 6vw, 3.562rem)/1.12 var(--font);
  --display-medium: 700 clamp(2.1rem, 5vw, 2.812rem)/1.16 var(--font);
  --headline-large: 400 2rem/1.25 var(--font);
  --headline-medium: 400 1.75rem/1.29 var(--font);
  --headline-small: 400 1.5rem/1.33 var(--font);
  --title-large: 400 1.375rem/1.27 var(--font);
  --title-medium: 500 1rem/1.5 var(--font);
  --title-small: 500 0.875rem/1.43 var(--font);
  --body-large: 400 1rem/1.5 var(--font);
  --body-medium: 400 0.875rem/1.43 var(--font);
  --body-small: 400 0.75rem/1.33 var(--font);
  --label-large: 500 0.875rem/1.43 var(--font);
  --label-medium: 500 0.75rem/1.33 var(--font);
  --label-small: 500 0.6875rem/1.45 var(--font);

  /* shape scale */
  --shape-xs: 4px;
  --shape-sm: 8px;
  --shape-md: 12px;
  --shape-lg: 16px;
  --shape-xl: 28px;
  --shape-full: 999px;

  /* elevation — M3's two-shadow recipe (key light + ambient) */
  --elev-0: none;
  --elev-1: 0 1px 2px 0 rgb(0 0 0 / .3), 0 1px 3px 1px rgb(0 0 0 / .15);
  --elev-2: 0 1px 2px 0 rgb(0 0 0 / .3), 0 2px 6px 2px rgb(0 0 0 / .15);
  --elev-3: 0 4px 8px 3px rgb(0 0 0 / .15), 0 1px 3px 0 rgb(0 0 0 / .3);
  --elev-4: 0 6px 10px 4px rgb(0 0 0 / .15), 0 2px 3px 0 rgb(0 0 0 / .3);

  /* motion — M3 expressive easing */
  --ease-standard: cubic-bezier(.2, 0, 0, 1);
  --ease-emphasised: cubic-bezier(.05, .7, .1, 1);
  --dur-short: 150ms;
  --dur-medium: 250ms;
  --dur-long: 400ms;

  /* state layer opacities */
  --state-hover: .08;
  --state-focus: .10;
  --state-press: .10;
}

/* ===========================================================================
   4. BASE
   =========================================================================== */
* { box-sizing: border-box; }
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--md-surface);
  color: var(--md-on-surface);
  font: var(--body-large);
  -webkit-font-smoothing: antialiased;
  /* iOS notch. The nav bar sits on the bottom edge on phones, so without this
     the last nav item lands under the home indicator. */
  padding-bottom: env(safe-area-inset-bottom);
}

h1, h2, h3, h4 { margin: 0; font-weight: inherit; }
p { margin: 0 0 1em; }
a { color: var(--md-primary); }

:focus-visible {
  outline: 3px solid var(--md-primary);
  outline-offset: 2px;
  border-radius: var(--shape-xs);
}

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Skip link. The nav rail is up to nine stops before the content on every
   page, and a keyboard user should not have to tab through it each time. */
.skip-link {
  position: absolute; z-index: 100; top: 8px; left: 8px;
  padding: 12px 20px; border-radius: var(--shape-full);
  background: var(--md-primary); color: var(--md-on-primary);
  font: var(--label-large); text-decoration: none;
  transform: translateY(-200%);
  transition: transform var(--dur-short) var(--ease-standard);
}
.skip-link:focus { transform: translateY(0); }

/* The scroll panes take focus so they can be scrolled from the keyboard; they
   must not then draw a focus ring as though they were a control, so the ring
   is drawn only for real keyboard focus. */
[tabindex='0']:focus:not(:focus-visible) { outline: none; }

/* <main> is focused programmatically after every navigation so a screen
   reader lands on the new content. It is not interactive and must not look it
   -- without this the whole page gets a primary-coloured ring down its left
   edge on every route change. */
[tabindex='-1']:focus { outline: none; }

/* Icons are an inline SVG sprite in index.html — no icon font, no network
   request, no flash of missing glyphs. currentColor means an icon inside a
   button is automatically the button's text colour. */
.icon {
  width: 1.25rem; height: 1.25rem; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.icon-sm { width: 1rem; height: 1rem; }
.icon-lg { width: 1.5rem; height: 1.5rem; }

/* ---- type helpers ---- */
.display-large { font: var(--display-large); letter-spacing: -.02em; }
.display-medium { font: var(--display-medium); letter-spacing: -.015em; }
.headline-large { font: var(--headline-large); }
.headline-medium { font: var(--headline-medium); }
.headline-small { font: var(--headline-small); }
.title-large { font: var(--title-large); }
.title-medium { font: var(--title-medium); letter-spacing: .009em; }
.title-small { font: var(--title-small); letter-spacing: .006em; }
.body-large { font: var(--body-large); letter-spacing: .009em; }
.body-medium { font: var(--body-medium); letter-spacing: .016em; }
.body-small { font: var(--body-small); letter-spacing: .025em; }
.label-large { font: var(--label-large); letter-spacing: .006em; }
.label-medium { font: var(--label-medium); letter-spacing: .036em; }
.label-small { font: var(--label-small); letter-spacing: .045em; }
.dim { color: var(--md-on-surface-variant); }

/* ===========================================================================
   5. LAYOUT
   ---------------------------------------------------------------------------
   The window-size classes are M3's: compact (<600), medium (600–839),
   expanded (840+). Navigation moves from a bottom bar to a rail at `medium`,
   which is the one breakpoint decision that matters -- a TO on a tablet at the
   desk and a player on a phone are the two real form factors, and both have to
   be first-class. This is where the competition falls down: the organiser
   tools exist on desktop and the phone gets a viewer.
   =========================================================================== */
.app {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: auto 1fr auto;
  grid-template-areas: 'bar' 'main' 'nav';
  min-height: 100dvh;
}

@media (min-width: 600px) {
  .app {
    grid-template-columns: 88px 1fr;
    grid-template-rows: auto 1fr;
    grid-template-areas: 'nav bar' 'nav main';
  }
}

.scaffold { grid-area: main; min-width: 0; }

.pane {
  max-width: 1240px;
  margin: 0 auto;
  padding: 16px 16px 96px;
}
@media (min-width: 600px) { .pane { padding: 24px 24px 48px; } }

.stack { display: flex; flex-direction: column; gap: 16px; }
.stack-sm { display: flex; flex-direction: column; gap: 8px; }
.row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.row-tight { display: flex; align-items: center; gap: 8px; }
.spacer { flex: 1 1 auto; }
.grid-cards {
  display: grid; gap: 16px;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}

/* ---- top app bar ---- */
.top-bar {
  grid-area: bar;
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; gap: 8px;
  min-height: 64px; padding: 8px 8px 8px 4px;
  background: var(--md-surface);
  color: var(--md-on-surface);
}
.top-bar.scrolled {
  background: var(--md-surface-container);
  box-shadow: var(--elev-1);
}
.top-bar h1 { font: var(--title-large); flex: 1 1 auto; min-width: 0; }
.top-bar .sub { display: block; font: var(--body-small); color: var(--md-on-surface-variant); }

/* ---- navigation ---- */
/* Compact: bottom bar. Medium+: rail. Same markup, so a resize does not
   re-render anything and a TO rotating a tablet does not lose their place. */
.nav {
  grid-area: nav;
  display: flex;
  background: var(--md-surface-container);
}
.nav-item {
  flex: 1; display: flex; flex-direction: column; align-items: center;
  gap: 4px; padding: 12px 0 16px;
  border: 0; background: none; cursor: pointer;
  color: var(--md-on-surface-variant);
  font: var(--label-medium); text-decoration: none;
}
.nav-item .pill {
  display: grid; place-items: center;
  width: 64px; height: 32px; border-radius: var(--shape-full);
  transition: background var(--dur-short) var(--ease-standard);
}
.nav-item[aria-current='page'] { color: var(--md-on-surface); }
.nav-item[aria-current='page'] .pill {
  background: var(--md-secondary-container);
  color: var(--md-on-secondary-container);
}
.nav-item:hover .pill { background: color-mix(in srgb, var(--md-on-surface) 8%, transparent); }
.nav-item[aria-current='page']:hover .pill {
  background: color-mix(in srgb, var(--md-on-secondary-container) 8%, var(--md-secondary-container));
}

@media (max-width: 599px) {
  .nav {
    position: sticky; bottom: 0; z-index: 20;
    border-top: 1px solid var(--md-outline-variant);
    padding-bottom: env(safe-area-inset-bottom);
  }
}
@media (min-width: 600px) {
  .nav {
    flex-direction: column; align-items: center;
    gap: 4px; padding: 12px 0; width: 88px;
    background: var(--md-surface);
  }
  .nav-item { flex: none; padding: 6px 0; width: 100%; }
}

/* ===========================================================================
   6. COMPONENTS
   =========================================================================== */

/* ---- buttons -------------------------------------------------------------
   One base, five variants. The state layer is a ::before at a token opacity
   rather than a colour change, which is how M3 keeps hover/focus/press
   consistent across every variant and every colour. */
.btn {
  position: relative; isolation: isolate;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 40px; padding: 0 24px;
  border: 0; border-radius: var(--shape-full);
  font: var(--label-large); letter-spacing: .006em;
  text-decoration: none; cursor: pointer; white-space: nowrap;
  transition: box-shadow var(--dur-short) var(--ease-standard);
}
.btn::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit; background: currentColor; opacity: 0;
  transition: opacity var(--dur-short) var(--ease-standard);
}
.btn:hover::before { opacity: var(--state-hover); }
.btn:focus-visible::before { opacity: var(--state-focus); }
.btn:active::before { opacity: var(--state-press); }
.btn:disabled, .btn[aria-disabled='true'] {
  cursor: not-allowed; box-shadow: none;
  background: color-mix(in srgb, var(--md-on-surface) 12%, transparent);
  color: color-mix(in srgb, var(--md-on-surface) 38%, transparent);
}
.btn:disabled::before { opacity: 0; }

.btn-filled { background: var(--md-primary); color: var(--md-on-primary); }
.btn-filled:hover { box-shadow: var(--elev-1); }
.btn-tonal { background: var(--md-secondary-container); color: var(--md-on-secondary-container); }
.btn-outlined {
  background: transparent; color: var(--md-primary);
  box-shadow: inset 0 0 0 1px var(--md-outline);
}
.btn-text { background: transparent; color: var(--md-primary); padding: 0 12px; }
.btn-danger { background: var(--md-error); color: var(--md-on-error); }
.btn-danger-text { background: transparent; color: var(--md-error); padding: 0 12px; }

.btn-icon {
  padding: 0; width: 40px; min-height: 40px;
  background: transparent; color: var(--md-on-surface-variant);
}
.btn-sm { min-height: 32px; padding: 0 16px; font: var(--label-medium); }
.btn-lg { min-height: 56px; padding: 0 32px; font: var(--title-medium); }
.btn-block { width: 100%; }

/* FAB — the primary action on compact screens, where it sits above the
   bottom nav rather than competing with it. */
.fab {
  position: fixed; right: 16px; z-index: 21;
  bottom: calc(88px + env(safe-area-inset-bottom));
  display: inline-flex; align-items: center; gap: 12px;
  min-height: 56px; padding: 0 20px;
  border: 0; border-radius: var(--shape-lg);
  background: var(--md-primary-container); color: var(--md-on-primary-container);
  font: var(--label-large); cursor: pointer; box-shadow: var(--elev-3);
}
.fab:hover { box-shadow: var(--elev-4); }
@media (min-width: 600px) { .fab { bottom: 24px; right: 24px; } }

/* ---- cards --------------------------------------------------------------- */
.card {
  border-radius: var(--shape-md);
  background: var(--md-surface-container-low);
  color: var(--md-on-surface);
  padding: 16px;
}
.card-elevated { background: var(--md-surface-container-low); box-shadow: var(--elev-1); }
.card-outlined { background: var(--md-surface); box-shadow: inset 0 0 0 1px var(--md-outline-variant); }
.card-filled { background: var(--md-surface-container-highest); }
.card-lg { border-radius: var(--shape-lg); padding: 24px; }

/* An interactive card gets the same state layer as a button. */
a.card, button.card {
  display: block; width: 100%; text-align: left; border: 0; cursor: pointer;
  position: relative; isolation: isolate; color: inherit; text-decoration: none;
  font: inherit;
  transition: box-shadow var(--dur-short) var(--ease-standard);
}
/* `a.card` is (0,1,1) and `.row` is (0,1,0), so on an element carrying both
   the display:block above silently wins and every layout helper on that card
   stops working -- the spacer does not push, and trailing content wraps to its
   own line. Restating them at (0,2,0) is the fix; it is not redundant. */
a.card.row, button.card.row,
a.card.row-tight, button.card.row-tight { display: flex; }
a.card.stack, button.card.stack { display: flex; flex-direction: column; }
a.card::before, button.card::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit; background: var(--md-on-surface); opacity: 0;
  transition: opacity var(--dur-short) var(--ease-standard);
}
a.card:hover::before, button.card:hover::before { opacity: var(--state-hover); }
a.card:hover, button.card:hover { box-shadow: var(--elev-2); }

/* ---- text fields --------------------------------------------------------- */
/* Outlined variant. The label is a real <label> sitting in the notch rather
   than a floating placeholder, so it is still announced when the field has a
   value -- the floating-placeholder pattern is the single most common a11y
   failure in Material implementations. */
.field { position: relative; display: block; }
.field > .field-label {
  position: absolute; top: -8px; left: 12px; z-index: 1;
  padding: 0 4px; background: var(--md-surface);
  font: var(--body-small); color: var(--md-on-surface-variant);
  pointer-events: none;
}
.field:focus-within > .field-label { color: var(--md-primary); }
.field input, .field select, .field textarea {
  width: 100%; min-height: 56px; padding: 16px;
  border: 1px solid var(--md-outline); border-radius: var(--shape-xs);
  background: transparent; color: var(--md-on-surface);
  font: var(--body-large); font-family: var(--font);
}
.field textarea { min-height: 96px; resize: vertical; }
.field select { appearance: none; cursor: pointer; padding-right: 44px; }
.field .select-arrow {
  position: absolute; right: 14px; top: 18px; pointer-events: none;
  color: var(--md-on-surface-variant);
}
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--md-primary); border-width: 2px;
  padding: 15px;
}
.field input:disabled, .field select:disabled {
  color: color-mix(in srgb, var(--md-on-surface) 38%, transparent);
  border-color: color-mix(in srgb, var(--md-on-surface) 12%, transparent);
}
.field-help {
  display: block; padding: 4px 16px 0;
  font: var(--body-small); color: var(--md-on-surface-variant);
}
.field-error .field-help { color: var(--md-error); }
.field-error input, .field-error select { border-color: var(--md-error); }

/* Fields on a container background need the label notch to match it. */
.card .field > .field-label { background: var(--md-surface-container-low); }
.card-filled .field > .field-label { background: var(--md-surface-container-highest); }

/* ---- switch -------------------------------------------------------------- */
.switch { display: flex; align-items: center; gap: 16px; cursor: pointer; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch .track {
  position: relative; flex: none;
  width: 52px; height: 32px; border-radius: var(--shape-full);
  background: var(--md-surface-container-highest);
  border: 2px solid var(--md-outline);
  transition: background var(--dur-short) var(--ease-standard),
              border-color var(--dur-short) var(--ease-standard);
}
.switch .thumb {
  position: absolute; top: 50%; left: 6px;
  width: 16px; height: 16px; border-radius: var(--shape-full);
  background: var(--md-outline);
  transform: translate(0, -50%);
  transition: transform var(--dur-medium) var(--ease-emphasised),
              width var(--dur-short) var(--ease-standard),
              height var(--dur-short) var(--ease-standard),
              background var(--dur-short) var(--ease-standard);
}
.switch input:checked + .track { background: var(--md-primary); border-color: var(--md-primary); }
.switch input:checked + .track .thumb {
  width: 24px; height: 24px; left: 2px;
  transform: translate(22px, -50%);
  background: var(--md-on-primary);
}
.switch input:focus-visible + .track { outline: 3px solid var(--md-primary); outline-offset: 2px; }

/* ---- chips --------------------------------------------------------------- */
.chip {
  position: relative; isolation: isolate;
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 32px; padding: 0 16px;
  border: 1px solid var(--md-outline); border-radius: var(--shape-sm);
  background: transparent; color: var(--md-on-surface-variant);
  font: var(--label-large); cursor: pointer; white-space: nowrap;
}
.chip::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit; background: currentColor; opacity: 0;
  transition: opacity var(--dur-short) var(--ease-standard);
}
.chip:hover::before { opacity: var(--state-hover); }
.chip[aria-pressed='true'], .chip.selected {
  background: var(--md-secondary-container);
  color: var(--md-on-secondary-container);
  border-color: transparent;
}
.chip-static { cursor: default; }
.chip-assist { border-color: var(--md-outline-variant); }

/* Status chips carry meaning, so they are the one place a colour is fixed
   rather than themed: green is done, gold is waiting on the organiser, red is
   wrong. Paired with text in every case -- colour is never the only signal. */
.chip-ok { background: var(--ok-container); color: var(--on-ok-container); border-color: transparent; }
.chip-warn { background: var(--warn-container); color: var(--on-warn-container); border-color: transparent; }
.chip-error { background: var(--md-error-container); color: var(--md-on-error-container); border-color: transparent; }
.chip-info { background: var(--md-primary-container); color: var(--md-on-primary-container); border-color: transparent; }

/* ---- segmented button ---------------------------------------------------- */
.segmented {
  display: inline-flex; border-radius: var(--shape-full); overflow: hidden;
  box-shadow: inset 0 0 0 1px var(--md-outline);
}
.segmented button {
  position: relative; isolation: isolate;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  min-height: 40px; padding: 0 16px; flex: 1;
  border: 0; border-right: 1px solid var(--md-outline);
  background: transparent; color: var(--md-on-surface);
  font: var(--label-large); cursor: pointer; white-space: nowrap;
}
.segmented button:last-child { border-right: 0; }
.segmented button::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: currentColor; opacity: 0;
  transition: opacity var(--dur-short) var(--ease-standard);
}
.segmented button:hover::before { opacity: var(--state-hover); }
.segmented button[aria-pressed='true'] {
  background: var(--md-secondary-container); color: var(--md-on-secondary-container);
}
.segmented-block { display: flex; width: 100%; }

/* ---- list ---------------------------------------------------------------- */
.list { display: flex; flex-direction: column; }
.list-item {
  position: relative; isolation: isolate;
  display: flex; align-items: center; gap: 16px;
  min-height: 56px; padding: 8px 16px;
  border: 0; background: transparent; color: inherit;
  font: inherit; text-align: left; text-decoration: none; width: 100%;
}
button.list-item, a.list-item { cursor: pointer; }
button.list-item::before, a.list-item::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: var(--md-on-surface); opacity: 0;
  transition: opacity var(--dur-short) var(--ease-standard);
}
button.list-item:hover::before, a.list-item:hover::before { opacity: var(--state-hover); }
.list-item .headline { font: var(--body-large); }
.list-item .supporting { font: var(--body-medium); color: var(--md-on-surface-variant); }
.list-divider { height: 1px; background: var(--md-outline-variant); margin: 0 16px; }

/* ---- avatar -------------------------------------------------------------- */
.avatar {
  display: grid; place-items: center; flex: none;
  width: 40px; height: 40px; border-radius: var(--shape-full);
  background: var(--md-primary-container); color: var(--md-on-primary-container);
  font: var(--title-medium); overflow: hidden;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar-sm { width: 32px; height: 32px; font: var(--label-medium); }
.avatar-lg { width: 72px; height: 72px; font: var(--headline-small); }

/* ---- dialog -------------------------------------------------------------- */
dialog.m3 {
  width: min(560px, calc(100vw - 32px));
  max-height: min(80dvh, 720px);
  padding: 0; border: 0; border-radius: var(--shape-xl);
  background: var(--md-surface-container-high);
  color: var(--md-on-surface);
  box-shadow: var(--elev-3);
}
dialog.m3::backdrop { background: color-mix(in srgb, var(--md-scrim) 40%, transparent); }
dialog.m3 .dialog-head { padding: 24px 24px 16px; }
dialog.m3 .dialog-head h2 { font: var(--headline-small); }
dialog.m3 .dialog-body { padding: 0 24px; overflow-y: auto; max-height: 62dvh; }
dialog.m3 .dialog-actions {
  display: flex; justify-content: flex-end; gap: 8px; padding: 24px;
  flex-wrap: wrap;
}
dialog.m3 .field > .field-label { background: var(--md-surface-container-high); }

/* Full-screen dialog on compact — M3's own guidance, and the right call for
   the import flow, which is a table the user needs room to read. */
@media (max-width: 599px) {
  dialog.m3.full {
    width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh;
    border-radius: 0; margin: 0;
  }
  dialog.m3.full .dialog-body { max-height: none; flex: 1; }
}

/* ---- snackbar ------------------------------------------------------------ */
.snackbar {
  position: fixed; z-index: 40; left: 16px; right: 16px;
  bottom: calc(88px + env(safe-area-inset-bottom));
  display: flex; align-items: center; gap: 8px;
  max-width: 560px; margin: 0 auto; padding: 14px 16px;
  border-radius: var(--shape-xs);
  background: var(--md-inverse-surface); color: var(--md-inverse-on-surface);
  font: var(--body-medium); box-shadow: var(--elev-3);
  animation: snack-in var(--dur-medium) var(--ease-emphasised);
}
.snackbar button {
  border: 0; background: none; cursor: pointer; padding: 4px 8px;
  color: var(--md-inverse-primary); font: var(--label-large);
}
@media (min-width: 600px) { .snackbar { bottom: 24px; left: 112px; right: auto; } }
@keyframes snack-in { from { opacity: 0; transform: translateY(16px); } }

/* ---- banner -------------------------------------------------------------- */
.banner {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 16px; border-radius: var(--shape-md);
  background: var(--md-surface-container-high);
  font: var(--body-medium);
}
.banner-warn { background: var(--warn-container); color: var(--on-warn-container); }
.banner-error { background: var(--md-error-container); color: var(--md-on-error-container); }
.banner-info { background: var(--md-primary-container); color: var(--md-on-primary-container); }
.banner .icon { margin-top: 2px; }

/* ---- tabs ---------------------------------------------------------------- */
.tabs {
  display: flex; gap: 0; overflow-x: auto; scrollbar-width: none;
  border-bottom: 1px solid var(--md-outline-variant);
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
  position: relative; isolation: isolate;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 48px; padding: 0 16px; flex: 0 0 auto;
  border: 0; background: none; cursor: pointer;
  color: var(--md-on-surface-variant); font: var(--title-small);
  white-space: nowrap;
}
.tab::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: currentColor; opacity: 0;
  transition: opacity var(--dur-short) var(--ease-standard);
}
.tab:hover::before { opacity: var(--state-hover); }
.tab { text-decoration: none; }
.tab[aria-current='page'] { color: var(--md-primary); }
.tab[aria-current='page']::after {
  content: ''; position: absolute; left: 8px; right: 8px; bottom: 0;
  height: 3px; border-radius: 3px 3px 0 0; background: var(--md-primary);
}

/* ---- progress ------------------------------------------------------------ */
.progress {
  height: 4px; border-radius: var(--shape-full); overflow: hidden;
  background: var(--md-secondary-container);
}
.progress > i { display: block; height: 100%; background: var(--md-primary); border-radius: inherit; }

/* ---- empty state --------------------------------------------------------- */
.empty {
  display: grid; place-items: center; gap: 12px;
  padding: 48px 24px; text-align: center;
  color: var(--md-on-surface-variant);
}
.empty .icon { width: 48px; height: 48px; color: var(--md-outline); stroke-width: 1.5; }

/* ===========================================================================
   7. APP-SPECIFIC
   =========================================================================== */

/* ---- the bracket grid ----------------------------------------------------
   CSS grid, one column per round, matches positioned by grid-row. No canvas,
   no SVG connectors library, no virtualisation.

   The connectors are ::after borders on the match itself. That is the trick
   that keeps this fast: a 256-entrant bracket is ~500 DOM nodes and zero
   layout passes beyond the initial one, where an SVG-connector approach needs
   a measure pass per node and re-measures on every resize. On a mid-range
   Android phone this is the difference between "instant" and "the tab
   reloaded". */
.bracket-scroll {
  overflow: auto;
  /* Its own scroll pane in both axes. Without a bounded height the round
     headers cannot stick to anything and a 128-entrant bracket makes the page
     itself thousands of pixels tall, which on a phone means you lose the tabs
     and the app bar the moment you look at it. */
  max-height: min(72dvh, 900px);
  padding: 8px 4px 24px;
  /* Momentum scrolling with snap to round boundaries -- on a phone you read a
     bracket one round at a time, and free-scrolling loses your place. */
  scroll-snap-type: x proximity;
  overscroll-behavior-x: contain;
}
.bracket {
  display: flex;
  gap: 32px;
  /* stretch, not start: every round column becomes as tall as the tallest, and
     that free space is what the distribution below has to work with. With
     `start` the columns are only as tall as their own content, so a round-two
     match sits at the top instead of centred between the two round-one
     matches that feed it -- which is what makes a bracket readable at a
     glance. */
  align-items: stretch;
  min-width: min-content;
}
.bracket-round {
  display: flex;
  flex-direction: column;
  scroll-snap-align: start;
  min-width: 232px;
}
.bracket-round > h3 {
  flex: none;
  font: var(--label-large); color: var(--md-on-surface-variant);
  padding: 0 4px 6px; position: sticky; top: 0;
  background: var(--md-surface); z-index: 1;
}
/* The matches themselves, spread down the column so each one lands opposite
   the midpoint of its feeders. */
.round-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  gap: 8px;
}

.match {
  position: relative;
  border-radius: var(--shape-sm);
  background: var(--md-surface-container-low);
  box-shadow: inset 0 0 0 1px var(--md-outline-variant);
  font: var(--body-medium);
  overflow: hidden;
}
.match.ready { box-shadow: inset 0 0 0 2px var(--md-primary); }
.match.live { box-shadow: inset 0 0 0 2px var(--warn); }
/* Completed and bye matches are de-emphasised with explicit colours, NOT with
   opacity on the container.

   Opacity was the obvious way to do it and it is the wrong one: it fades the
   text against the page as well as the card, so a name inside a 0.5-opacity
   bye card measured 2.4:1 against the surface behind it. Contrast is computed
   on what is actually painted, so anything that dims a whole subtree dims it
   below the threshold too. Muting the colour deliberately keeps the visual
   hierarchy and stays measurable. */
.match.done { background: var(--md-surface-container); }
.match.bye {
  background: var(--md-surface);
  box-shadow: inset 0 0 0 1px var(--md-outline-variant);
}
.match.bye .match-side .who { color: var(--md-on-surface-variant); }
button.match { display: block; width: 100%; text-align: left; border: 0; cursor: pointer; padding: 0; }

/* Spans, not divs: they sit inside an aria-hidden wrapper span within a
   <button>, and a <div> inside a <button> is invalid HTML that browsers
   silently reparent -- which breaks the layout in the way that is hardest to
   debug. display:flex makes them behave exactly as before. */
.match > span { display: block; }
.match-side {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px; min-height: 40px;
}
.match-side + .match-side { border-top: 1px solid var(--md-outline-variant); }
.match-side .seed {
  flex: none; width: 22px; text-align: right;
  font: var(--label-small); color: var(--md-on-surface-variant);
  font-variant-numeric: tabular-nums;
}
.match-side .who {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.match-side .score {
  flex: none; font: var(--title-small); font-variant-numeric: tabular-nums;
  color: var(--md-on-surface-variant);
}
.match-side.won { background: color-mix(in srgb, var(--md-primary) 12%, transparent); }
.match-side.won .who { font-weight: 600; }
.match-side.won .score { color: var(--md-primary); }
.match-side.lost .who, .match-side.lost .seed { color: var(--md-on-surface-variant); }
/* `waiting` / `bye` placeholders. on-surface-variant is the M3 role for
   secondary text and clears 4.5:1 against every surface token here. */
.match-side.tbd .who { color: var(--md-on-surface-variant); font-style: italic; }
.match-meta {
  display: flex; align-items: center; gap: 6px;
  padding: 4px 10px; border-top: 1px solid var(--md-outline-variant);
  font: var(--label-medium); color: var(--md-on-surface-variant);
  background: var(--md-surface-container);
}

/* ---- station cards ------------------------------------------------------- */
.stations { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
.station {
  border-radius: var(--shape-md); padding: 12px;
  background: var(--md-surface-container-low);
  box-shadow: inset 0 0 0 1px var(--md-outline-variant);
}
.station.busy { box-shadow: inset 0 0 0 2px var(--warn); }
.station.open { box-shadow: inset 0 0 0 2px var(--ok); }
.station-head {
  display: flex; align-items: center; gap: 8px;
  font: var(--title-small); margin-bottom: 8px;
}
.station .timer { font: var(--label-medium); font-variant-numeric: tabular-nums; }
.station .timer.over { color: var(--md-error); font-weight: 700; }

/* ---- the entrant table ---------------------------------------------------
   A real table, not a div grid, because a TO will select a range with shift,
   copy it, and paste it into a spreadsheet -- and only a table survives that
   round trip. Sticky header and first column so a 200-row roster stays
   readable while scrolling. */
.table-wrap {
  overflow: auto; max-height: 70dvh;
  border-radius: var(--shape-md);
  box-shadow: inset 0 0 0 1px var(--md-outline-variant);
  background: var(--md-surface-container-lowest);
}
table.data { border-collapse: separate; border-spacing: 0; width: 100%; font: var(--body-medium); }
table.data th, table.data td {
  padding: 10px 12px; text-align: left;
  border-bottom: 1px solid var(--md-outline-variant);
  white-space: nowrap;
}
table.data thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--md-surface-container);
  font: var(--title-small); color: var(--md-on-surface-variant);
}
table.data tbody tr:hover td { background: color-mix(in srgb, var(--md-on-surface) 4%, transparent); }
table.data tbody tr.selected td { background: var(--md-secondary-container); color: var(--md-on-secondary-container); }
table.data td.num { text-align: right; font-variant-numeric: tabular-nums; }
/* 32px, not the browser's default ~13px.
   The roster table is dense on purpose -- a 200-person event should not need
   scrolling for a week -- but WCAG 2.2's 24x24 minimum target size applies to
   every one of these, and a 13px inline editor is genuinely hard to hit even
   with a mouse. 32 keeps the table compact and clears the requirement. */
table.data td input {
  width: 100%; min-width: 60px; min-height: 32px; padding: 4px 8px;
  border: 1px solid transparent; border-radius: var(--shape-xs);
  background: transparent; color: inherit; font: inherit;
}
/* Checkboxes have no intrinsic size to grow, so they are sized explicitly.
   accent-color keeps them themed without replacing the native control, which
   would cost the keyboard and screen-reader behaviour that comes free. */
table.data input[type='checkbox'],
.check input[type='checkbox'] {
  width: 24px; height: 24px; margin: 0;
  accent-color: var(--md-primary);
  cursor: pointer;
}
table.data .check { width: 48px; text-align: center; }
table.data td input:hover { border-color: var(--md-outline-variant); }
table.data td input:focus { outline: none; border-color: var(--md-primary); background: var(--md-surface); }
table.data .check { width: 44px; }

/* Bulk action bar — appears when rows are selected. Not a floating toolbar
   that covers the rows you just picked; it takes the top of the table. */
.bulk-bar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 8px 12px; border-radius: var(--shape-md);
  background: var(--md-secondary-container); color: var(--md-on-secondary-container);
}

/* ---- guidance ("next steps") --------------------------------------------- */
.guide-list { display: flex; flex-direction: column; gap: 8px; }
.guide {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 12px 14px; border-radius: var(--shape-md);
  background: var(--md-surface-container-low);
  box-shadow: inset 0 0 0 1px var(--md-outline-variant);
}
.guide.urgent { background: var(--md-error-container); color: var(--md-on-error-container); box-shadow: none; }
.guide.attention { background: var(--warn-container); color: var(--on-warn-container); box-shadow: none; }
.guide .guide-body { flex: 1 1 auto; min-width: 0; }
.guide .guide-title { font: var(--title-small); }
/* Full opacity. This is the sentence that explains WHY the suggestion fired,
   which is the part a TO has to be able to read -- fading it was decorative
   and it cost contrast on the coloured urgent/attention backgrounds. */
.guide .guide-why { font: var(--body-medium); margin-top: 2px; }
.guide .guide-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }

/* ---- seeding lab --------------------------------------------------------- */
.seed-row {
  display: flex; align-items: center; gap: 12px;
  padding: 8px 12px; border-radius: var(--shape-sm);
  background: var(--md-surface-container-low);
}
.seed-row.moved { background: var(--md-tertiary-container); color: var(--md-on-tertiary-container); }
.seed-row .rank {
  flex: none; width: 32px; height: 32px; display: grid; place-items: center;
  border-radius: var(--shape-full); background: var(--md-surface-container-highest);
  font: var(--label-large); font-variant-numeric: tabular-nums;
}
/* The up/down pair. 40x40 each, comfortably over the 24x24 minimum target
   size, and spaced so a thumb does not hit both. */
.seed-move { display: flex; gap: 2px; flex: none; }
.icon.flip { transform: rotate(180deg); }

/* ---- rules sheet --------------------------------------------------------- */
.rules-group { margin-bottom: 24px; }
.rules-group > h3 {
  font: var(--title-medium); margin-bottom: 8px;
  padding-bottom: 4px; border-bottom: 1px solid var(--md-outline-variant);
}
.rules-row {
  display: flex; gap: 16px; align-items: baseline;
  padding: 6px 0; font: var(--body-medium);
}
.rules-row dt { flex: 0 0 200px; color: var(--md-on-surface-variant); }
.rules-row dd { margin: 0; flex: 1 1 auto; }
.rules-row.overridden dd::after {
  content: 'changed'; margin-left: 8px; padding: 1px 6px;
  border-radius: var(--shape-full); font: var(--label-small);
  background: var(--md-tertiary-container); color: var(--md-on-tertiary-container);
}
@media (max-width: 599px) { .rules-row { flex-direction: column; gap: 0; } .rules-row dt { flex: none; } }

/* ---- offline / sync indicator -------------------------------------------- */
/* The whole product promise in one control. It is in the app bar, always, and
   it says what is actually true: how many writes are waiting and whether the
   last sync worked. Hiding sync state is what makes people distrust an app
   on bad wifi -- they tap report, nothing visibly happens, so they tap again. */
.sync {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px; border-radius: var(--shape-full);
  font: var(--label-medium); white-space: nowrap;
  background: var(--md-surface-container-high); color: var(--md-on-surface-variant);
}
.sync.pending { background: var(--warn-container); color: var(--on-warn-container); }
.sync.offline { background: var(--md-error-container); color: var(--md-on-error-container); }
.sync .dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
.sync.pending .dot { animation: pulse 1.4s var(--ease-standard) infinite; }
@keyframes pulse { 50% { opacity: .3; } }

/* ---- misc ---------------------------------------------------------------- */
.code {
  font: var(--body-medium); font-family: var(--font-mono);
  letter-spacing: .1em; padding: 2px 8px;
  border-radius: var(--shape-xs); background: var(--md-surface-container-highest);
}
.invite-code {
  font-family: var(--font-mono); font-size: clamp(1.5rem, 5vw, 2rem);
  font-weight: 700; letter-spacing: .2em;
}
.divider { height: 1px; background: var(--md-outline-variant); border: 0; margin: 8px 0; }

.h-scroll {
  display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px;
  scrollbar-width: thin;
}
.h-scroll::-webkit-scrollbar { height: 4px; }
.h-scroll::-webkit-scrollbar-thumb { background: var(--md-outline-variant); border-radius: 2px; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
  }
}

/* ===========================================================================
   8. GAME THEMING
   ---------------------------------------------------------------------------
   The colour side lives in data/themes.js, which injects one <style> of
   [data-game="…"] rules that override M3's own colour roles. That is the whole
   trick: because the override is on the ROLES rather than on bespoke classes,
   every button, chip, tab, switch and focus ring inside a themed container
   picks up the game with no component-level work at all.

   What is below is only the structure the artwork needs.
   =========================================================================== */

/* ---- the mark ------------------------------------------------------------
   Two-letter game badge. Its colours come from the theme rather than from a
   single accent hex, because one accent cannot be legible on both a light and
   a dark surface -- white-on-accent was the pairing that actually failed an
   audit here. */
.game-mark {
  background: var(--game-mark-bg, var(--md-primary-container));
  color: var(--game-mark-fg, var(--md-on-primary-container));
  font: var(--label-large);
  letter-spacing: .04em;
}
[data-game] .game-mark {
  background: var(--md-primary-container);
  color: var(--md-on-primary-container);
}

/* ---- the hero banner ---------------------------------------------------- */
.game-hero {
  position: relative; isolation: isolate; overflow: hidden;
  border-radius: var(--shape-lg);
  background: var(--game-deep);
  color: var(--game-ink);
  min-height: 168px;
  display: flex; align-items: flex-end;
}
.game-hero.compact { min-height: 96px; border-radius: var(--shape-md); }

.game-hero-art {
  position: absolute; inset: 0; z-index: -2;
  display: flex; align-items: center; justify-content: flex-end;
  color: var(--game-accent);
  /* The drawn motif is a line drawing on a near-flat ground, so it needs more
     presence than a photograph would; a real hero image dropped into the slot
     is already busy and gets full strength from the scrim alone. */
  opacity: .55;
}
.game-hero-art:has(img) { opacity: 1; }
.game-hero-art img { width: 100%; height: 100%; object-fit: cover; }
.game-hero-art .game-art { width: 100%; height: 100% !important; }

/* Scrim. The art runs edge to edge behind the text, so the text needs its own
   guaranteed contrast rather than relying on where the art happens to be
   light -- doubly so once somebody drops a real screenshot in, which we cannot
   see and cannot design around. */
.game-hero::after {
  content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: linear-gradient(90deg,
    var(--game-deep) 0%,
    color-mix(in srgb, var(--game-deep) 88%, transparent) 46%,
    color-mix(in srgb, var(--game-deep) 30%, transparent) 100%);
}

.game-hero-body { padding: 20px; max-width: 34rem; }
.game-hero.compact .game-hero-body { padding: 14px 16px; }
.game-hero-eyebrow {
  margin: 0 0 2px; font: var(--label-medium);
  color: var(--md-primary); text-transform: uppercase; letter-spacing: .09em;
}
.game-hero-title { font: var(--headline-small); margin: 0; }
.game-hero.compact .game-hero-title { font: var(--title-medium); }
.game-hero-sub { margin: 4px 0 0; font: var(--body-medium); color: var(--game-ink); }
.game-hero-credit {
  position: absolute; right: 10px; bottom: 8px; margin: 0;
  font: var(--label-small); color: var(--game-ink); background: var(--game-deep);
}

@media (max-width: 599px) {
  .game-hero { min-height: 140px; }
  .game-hero-body { padding: 16px; }
}

/* Batty Brackets: an independent sports publication around operational tools.
   Serif display type is reserved for identity and editorial headings; forms,
   scores and bracket labels keep the system sans. These explicit role pairs
   follow the existing colour scheme, including the user's manual override.
   Game palettes remain untouched: a game is a subject, not the publisher. */
:root { --font-editorial: Georgia, 'Times New Roman', serif; }
.brand-signature {
  display: block; font: var(--label-small); letter-spacing: .06em;
  color: var(--md-on-surface-variant); margin-bottom: 2px;
}
.top-bar { border-bottom: 1px solid var(--md-outline-variant); }
.top-bar h1 { overflow-wrap: anywhere; }
.btn:not(.btn-icon), .fab { border-radius: 4px; }
.card, .banner, .station, .game-hero, .demo-tour { border-radius: 6px; }
.app-publication, .publication {
  --md-surface: light-dark(#f3eedf, #211f19);
  --md-on-surface: light-dark(#20221c, #f3eedf);
  --md-on-surface-variant: light-dark(#55574d, #c6c4b9);
  --md-outline: light-dark(#73756a, #a09e92);
  --md-outline-variant: light-dark(#bebeb0, #55564b);
  --md-primary: light-dark(#982e22, #ffb4a6);
  --md-on-primary: light-dark(#ffffff, #4b0c05);
  --md-primary-container: light-dark(#ffdad2, #713023);
  --md-on-primary-container: light-dark(#390900, #ffdad2);
  background: var(--md-surface); color: var(--md-on-surface);
}
.publication {
  margin-top: 18px; margin-bottom: 24px; padding-top: 22px;
}
.publication-edition {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px;
  font: var(--label-small); text-transform: uppercase; letter-spacing: .09em;
  border-bottom: 1px solid var(--md-outline); padding-bottom: 10px;
}
.publication-wordmark {
  font: bold clamp(2.5rem, 6.4vw, 5.5rem)/1.25 var(--font-editorial);
  letter-spacing: -.06em; text-align: center; margin: 16px 0 10px;
}
.publication-wordmark span { color: var(--md-primary); }
.publication-nav {
  display: flex; justify-content: center; flex-wrap: wrap; gap: 8px 32px;
  border-bottom: 4px double var(--md-on-surface); padding-bottom: 8px;
}
.publication-nav a, .publication-nav button {
  display: inline-flex; align-items: center; gap: 8px; padding: 10px 4px;
  min-height: 44px; background: transparent; color: inherit; border: 0;
  font: var(--label-large); cursor: pointer; text-decoration: none;
}
.publication-nav a:hover, .publication-nav button:hover { text-decoration: underline; }
.publication-lead {
  display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: clamp(24px, 4vw, 56px); padding: clamp(28px, 4vw, 48px) 0;
}
.publication-kicker {
  font: var(--label-small); letter-spacing: .1em; text-transform: uppercase;
  color: var(--md-on-surface-variant); margin: 0;
}
.publication-story h3 {
  font: 400 clamp(2.5rem, 5.8vw, 5rem)/1.04 var(--font-editorial);
  letter-spacing: -.055em; margin: 22px 0;
}
.publication-deck {
  font: 400 1.15rem/1.65 var(--font-editorial); max-width: 42ch; margin: 0 0 26px;
}
.local-actions { display: flex; gap: 12px; flex-wrap: wrap; }
.publication-note {
  font: var(--body-small); color: var(--md-on-surface-variant);
  margin: 18px 0 0; max-width: 52ch;
}
.publication-feature { border-left: 1px solid var(--md-outline); padding-left: clamp(20px, 3vw, 36px); }
.publication-four {
  display: flex; align-items: center; flex-wrap: wrap; gap: 18px; margin: 10px 0;
  font: 400 clamp(5rem, 9vw, 8rem)/1.2 var(--font-editorial); color: var(--md-primary);
}
.publication-four span { font: var(--label-large); text-transform: uppercase; letter-spacing: .06em; color: var(--md-on-surface); }
.publication-feature h3 { font: 400 clamp(1.6rem, 2.8vw, 2.5rem)/1.15 var(--font-editorial); letter-spacing: -.035em; }
.publication-facts { margin: 24px 0 8px; }
.publication-facts > div { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 6px 14px; border-top: 1px solid var(--md-outline-variant); padding: 12px 0; }
.publication-facts dt { font: var(--label-small); text-transform: uppercase; }
.publication-facts dd { font: var(--body-small); margin: 0; }
.publication-feature .btn { padding-left: 0; }
.publication-section-heading {
  display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap;
  gap: 10px; border-top: 4px double var(--md-on-surface); padding: 18px 0;
}
.publication-section-heading h3 { font: 400 1.75rem/1.2 var(--font-editorial); }
.publication-section-heading > span { font: var(--label-small); color: var(--md-on-surface-variant); }
.publication-events .card { border-radius: 0; box-shadow: none; border-bottom: 1px solid var(--md-outline-variant); padding: 16px 0; }
.publication-tools { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 28px; padding: 36px 0; }
.publication-tools article { border-top: 1px solid var(--md-outline); padding-top: 16px; }
.publication-tools h3 { font: 400 1.5rem/1.2 var(--font-editorial); margin: 14px 0 10px; }
.publication-tools p { font: var(--body-medium); color: var(--md-on-surface-variant); margin: 0; }
.publication-footer { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px 24px; border-top: 4px double var(--md-on-surface); padding-top: 16px; font: var(--body-small); }
.publication-footer a { display: inline-flex; align-items: center; min-height: 44px; color: inherit; font-weight: 600; }
.publication-start { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; border-top: 1px solid var(--md-outline); padding: 22px 0; }
.publication-start p { font: 400 1.5rem/1.3 var(--font-editorial); margin: 0; }
.app-publication .nav { background: var(--md-surface); }
.app-publication .nav-item[aria-current] .pill { background: var(--md-primary-container); color: var(--md-on-primary-container); }
.app-publication .demo-bar { background: var(--md-surface); color: var(--md-on-surface); border-top: 1px solid var(--md-outline); }
.app-publication .demo-tour { background: var(--md-surface); }
@media (max-width: 799px) {
  .publication-lead, .publication-tools { grid-template-columns: 1fr; }
  .publication-feature { border-left: 0; border-top: 1px solid var(--md-outline); padding: 24px 0 0; }
  .publication-wordmark { font-size: clamp(2.3rem, 8vw, 4rem); }
}
.local-console-links { margin: 16px 0; }
.local-device-note { font: var(--body-medium); color: var(--md-on-surface-variant); border-left: 3px solid var(--md-outline); padding: 8px 16px; margin: 0 0 20px; }
.game-hero[data-game="tokon"] { border-left: 6px solid var(--md-primary); min-height: 200px; }
.game-hero[data-game="tokon"] .game-hero-body { padding: 28px; max-width: 65%; background: var(--game-deep); }
.game-hero[data-game="tokon"] .game-hero-title { font-size: clamp(1.5rem, 3vw, 2.5rem); line-height: 1.12; font-weight: 800; letter-spacing: -.025em; overflow-wrap: anywhere; }
.game-hero[data-game="tokon"] .game-hero-art { left: 55%; opacity: 1; }
.game-hero[data-game="tokon"] .game-hero-eyebrow { margin-bottom: 12px; }
.game-hero[data-game="tokon"].compact { min-height: 96px; }
.game-hero[data-game="tokon"].compact .game-hero-title { font: var(--title-medium); }
.tv[data-game="tokon"] .tv-head { border-left: 6px solid var(--md-primary); padding-left: 20px; }
.tv[data-game="tokon"] .tv-title { font-weight: 800; letter-spacing: -.025em; overflow-wrap: anywhere; }
@media (max-width: 799px) {
  .game-hero[data-game="tokon"] .game-hero-body { max-width: 100%; padding: 20px; }
  .game-hero[data-game="tokon"] .game-hero-art { display: none; }
}

/* ---- the game-select card ----------------------------------------------- */
/* The motif sits behind the card at low opacity, so the choice reads as the
   game before you have read the words. */
.game-card { position: relative; isolation: isolate; overflow: hidden; }
.game-card .game-card-art {
  position: absolute; right: -30px; top: -10px; bottom: -10px; z-index: -1;
  width: 60%; color: var(--md-primary); opacity: .17;
  display: flex; align-items: center; justify-content: flex-end;
  pointer-events: none;
}
.game-card .game-card-art .game-art { height: 100% !important; width: auto; }
.game-card[aria-pressed='true'] { box-shadow: inset 0 0 0 2px var(--md-primary); }
.game-card[aria-pressed='true'] .game-card-art { opacity: .3; }

/* A themed page keeps its accent on the app bar, so the game is present even
   when the banner has scrolled away. */
.top-bar[data-game] { border-bottom: 3px solid var(--md-primary); }

@media (prefers-reduced-motion: reduce) {
  .game-hero-art, .game-card-art { transition: none; }
}

/* ===========================================================================
   9. THE GUIDED DEMO
   ---------------------------------------------------------------------------
   Docked, never floating over the content.

   The spotlight-overlay style of product tour is the obvious alternative and
   it is the wrong one here. It traps focus, it breaks the moment the layout it
   was measured against changes, and — fatally for this demo — it stops you
   clicking the thing it is pointing at. The whole pitch is "poke at it", so
   the guidance takes its own space and leaves the entire app live underneath.
   =========================================================================== */

/* ---- the demo banner ---------------------------------------------------- */
.demo-bar {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 12px 16px;
  background: var(--md-secondary-container);
  color: var(--md-on-secondary-container);
  border-bottom: 1px solid var(--md-outline-variant);
}
.demo-bar .icon { margin-top: 2px; flex: none; }
/* Without min-width:0 the text block's min-content width keeps it wider than
   the row, so on a phone it wraps below the icon and the icon sits alone on a
   line of its own. */
.demo-bar .spacer { min-width: 0; }
.demo-bar-actions { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.demo-bar-slim {
  align-items: center; padding: 6px 16px;
  background: var(--md-surface-container);
  color: var(--md-on-surface-variant);
}
@media (max-width: 599px) {
  .demo-bar { flex-wrap: wrap; }
  .demo-bar-actions { width: 100%; }
}

/* ---- the tour card ------------------------------------------------------
   Bottom sheet on a phone, docked panel on the right from `medium` up. Both
   sit above the content and below the nav, and neither covers the middle of
   the screen, which is where the thing being demonstrated is. */
.tour {
  position: fixed; z-index: 30;
  left: 0; right: 0; bottom: 0;
  display: flex; flex-direction: column;
  background: var(--md-surface-container-high);
  color: var(--md-on-surface);
  border-top: 1px solid var(--md-outline-variant);
  box-shadow: var(--elev-3);
  padding-bottom: env(safe-area-inset-bottom);
  animation: tour-in var(--dur-medium) var(--ease-emphasised);
}
@keyframes tour-in { from { transform: translateY(100%); } }

@media (min-width: 900px) {
  .tour {
    left: auto; right: 24px; bottom: 24px;
    width: 380px; border-radius: var(--shape-lg);
    border: 1px solid var(--md-outline-variant);
  }
  @keyframes tour-in { from { transform: translateY(24px); opacity: 0; } }
}

/* On a phone the tour sheet would sit on top of the bottom nav, so the nav
   moves out of the way rather than being covered by it. */
@media (max-width: 899px) {
  body:has(.tour) .nav { display: none; }
}

/* The sheet is docked, so it permanently covers whatever is under it. Give the
   content enough trailing room to scroll clear -- otherwise the last few rows
   of a queue are unreachable for as long as the tour is open, which on the
   "try reporting one" step is the very thing being pointed at. */
body:has(.tour) .pane { padding-bottom: 340px; }
@media (min-width: 900px) {
  body:has(.tour) .pane { padding-bottom: 96px; padding-right: 0; }
  body:has(.tour) { --tour-gutter: 404px; }
  body:has(.tour) .pane { max-width: min(1240px, calc(100vw - var(--tour-gutter))); margin-left: 0; }
}

.tour-progress { display: flex; gap: 4px; padding: 12px 16px 0; }
.tour-pip {
  height: 4px; flex: 1; border-radius: 2px;
  background: var(--md-outline-variant);
}
.tour-pip.done { background: color-mix(in srgb, var(--md-primary) 55%, transparent); }
.tour-pip.on { background: var(--md-primary); }

.tour-body { padding: 12px 16px 4px; }
.tour-count { color: var(--md-primary); margin: 0 0 2px; }
.tour-title { font: var(--title-large); margin: 0 0 6px; }
.tour-text { font: var(--body-medium); margin: 0; color: var(--md-on-surface-variant); }

.tour-actions {
  display: flex; align-items: center; gap: 8px;
  padding: 12px 12px 14px; flex-wrap: wrap;
}

@media (prefers-reduced-motion: reduce) {
  .tour { animation: none; }
}

/* ---- sortable table headers ---------------------------------------------
   The whole header cell is the control. A small arrow glyph as the only hit
   target is a 12px tap area, which fails the minimum target size and is
   miserable on a phone regardless. */
table.data th button.th-sort {
  display: flex; align-items: center; gap: 6px;
  width: 100%; min-height: 32px; padding: 0;
  border: 0; background: none; cursor: pointer;
  color: inherit; font: inherit; text-align: left;
  border-radius: var(--shape-xs);
}
table.data th button.th-sort:hover { color: var(--md-on-surface); }
table.data th .th-arrow { opacity: .45; display: inline-flex; }
table.data th[aria-sort]:not([aria-sort='none']) { color: var(--md-primary); }
table.data th[aria-sort]:not([aria-sort='none']) .th-arrow { opacity: 1; }

/* ===========================================================================
   10. THE VENUE DISPLAY
   ---------------------------------------------------------------------------
   Read from four metres, not forty centimetres. Everything is sized in `vmin`
   so one stylesheet serves a 24" monitor on a table and a projector on a wall,
   and the layout carries far fewer things than the organiser's run view
   because at that distance you can hold about five.
   =========================================================================== */

.tv {
  position: relative;
  min-height: 100dvh;
  display: flex; flex-direction: column;
  background: var(--game-deep, var(--md-surface-container-lowest));
  color: var(--md-on-surface);
  padding: clamp(16px, 2.2vmin, 40px);
  gap: clamp(12px, 1.6vmin, 28px);
  overflow: hidden;
}

/* ---- the control bar ---------------------------------------------------
   For whoever is plugging the laptop in, not for the room. Fades away and
   returns on pointer movement, the way a video player's controls do — same
   problem, same solution. */
.tv-controls {
  position: absolute; z-index: 5; top: 0; left: 0; right: 0;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 10px 14px;
  background: color-mix(in srgb, var(--md-surface) 88%, transparent);
  border-bottom: 1px solid var(--md-outline-variant);
  opacity: 0; transform: translateY(-100%);
  transition: opacity var(--dur-medium) var(--ease-standard),
              transform var(--dur-medium) var(--ease-standard);
}
.tv:hover .tv-controls,
.tv:focus-within .tv-controls { opacity: 1; transform: translateY(0); }

/* ---- header ------------------------------------------------------------ */
.tv-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 24px; flex: none;
}
.tv-head > div:first-child { min-width: 0; }
.tv-eyebrow {
  margin: 0; font-size: clamp(.7rem, 1.5vmin, 1.2rem);
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--md-primary); font-weight: 500;
}
.tv-title {
  margin: 0; font-size: clamp(1.5rem, 4.2vmin, 3.4rem);
  font-weight: 700; line-height: 1.05; letter-spacing: -.01em;
}
.tv-head-right {
  flex: 0 0 auto; max-width: 38%; min-width: 0; text-align: right;
}
.tv-join, .tv-count {
  margin: 0; font-size: clamp(.85rem, 1.9vmin, 1.5rem);
  color: var(--md-on-surface-variant);
}
.tv-join { overflow-wrap: anywhere; }
.tv-join b { color: var(--md-primary); font-family: var(--font-mono); letter-spacing: .1em; }
.tv-count.complete { color: var(--md-primary); font-weight: 700; }

/* ---- body -------------------------------------------------------------- */
.tv-body {
  flex: 1; min-height: 0;
  display: grid; gap: clamp(12px, 2vmin, 32px);
  grid-template-columns: 1fr;
  grid-template-rows: auto 1fr;
}
@media (min-width: 900px) {
  .tv-body { grid-template-columns: 1.15fr 1fr; grid-template-rows: 1fr; }
}
.tv-section {
  margin: 0 0 clamp(6px, 1vmin, 14px);
  font-size: clamp(.75rem, 1.6vmin, 1.25rem);
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--md-on-surface-variant); font-weight: 500;
}

/* ---- on now ------------------------------------------------------------ */
.tv-stations { display: grid; gap: clamp(8px, 1.2vmin, 18px); }
.tv-station {
  padding: clamp(10px, 1.6vmin, 24px);
  border-radius: var(--shape-lg);
  background: var(--md-surface-container-low);
  box-shadow: inset 0 0 0 2px var(--md-outline-variant);
}
.tv-station-meta { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.tv-station.busy { box-shadow: inset 0 0 0 3px var(--md-primary); }
/* De-emphasised with colour, not opacity — the same mistake as the bye
   matches earlier in this sheet. Fading a container fades its text against
   the page too, and a 0.62-opacity station name measured below 4.5:1. */
.tv-station.free {
  background: var(--md-surface);
  box-shadow: inset 0 0 0 2px var(--md-outline-variant);
}
.tv-station.free .tv-station-name,
.tv-station.free .tv-free { color: var(--md-on-surface-variant); }
.tv-station.over { box-shadow: inset 0 0 0 3px var(--md-error); }
.tv-station-name {
  margin: 0 0 4px; font-size: clamp(.7rem, 1.5vmin, 1.15rem);
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--md-on-surface-variant);
}
.tv-station-state {
  flex: none; padding: 3px 8px; border-radius: var(--shape-full);
  font-size: clamp(.6rem, 1.1vmin, .85rem); letter-spacing: .08em;
  text-transform: uppercase; color: var(--md-on-surface-variant);
  background: var(--md-surface-container);
}
.tv-station.busy .tv-station-state { color: var(--md-on-primary-container); background: var(--md-primary-container); }
.tv-vs { margin: 0; display: flex; align-items: baseline; gap: .5em; flex-wrap: wrap; }
.tv-player {
  font-size: clamp(1.1rem, 3.1vmin, 2.6rem);
  font-weight: 700; line-height: 1.1;
}
.tv-vs-mark {
  font-size: clamp(.7rem, 1.5vmin, 1.15rem);
  color: var(--md-on-surface-variant); text-transform: uppercase; letter-spacing: .08em;
}
.tv-round {
  margin: 4px 0 0; font-size: clamp(.75rem, 1.6vmin, 1.25rem);
  color: var(--md-on-surface-variant);
}
.tv-station.over .tv-round { color: var(--md-error); font-weight: 600; }
.tv-free {
  margin: 0; font-size: clamp(.9rem, 1.9vmin, 1.5rem);
  color: var(--md-on-surface-variant);
}

/* ---- up next ----------------------------------------------------------- */
.tv-queue { list-style: none; margin: 0; padding: 0; display: grid; gap: clamp(4px, .8vmin, 12px); }
.tv-queue-row {
  display: grid; grid-template-columns: auto 1fr auto; align-items: baseline;
  gap: clamp(8px, 1.2vmin, 18px);
  padding: clamp(6px, 1vmin, 14px) clamp(8px, 1.2vmin, 18px);
  border-radius: var(--shape-md);
  background: var(--md-surface-container-low);
}
/* The next one out is the one people are looking for. */
.tv-queue-row.first { background: var(--md-primary-container); color: var(--md-on-primary-container); }
.tv-queue-num {
  font-size: clamp(.75rem, 1.5vmin, 1.15rem);
  color: var(--md-on-surface-variant); font-variant-numeric: tabular-nums;
}
.tv-queue-row.first .tv-queue-num { color: inherit; }
.tv-queue-names {
  min-width: 0; display: flex; align-items: baseline; gap: .4em;
  overflow: hidden; font-size: clamp(.95rem, 2.2vmin, 1.85rem);
  font-weight: 600; line-height: 1.15;
}
.tv-queue-player { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tv-queue-round {
  min-width: 0; max-width: 18ch; overflow: hidden; text-overflow: ellipsis;
  font-size: clamp(.7rem, 1.4vmin, 1.05rem);
  color: var(--md-on-surface-variant); white-space: nowrap;
}
.tv-queue-row.first .tv-queue-round { color: inherit; }
.tv-queue-more {
  margin: clamp(6px, 1vmin, 12px) 0 0;
  font-size: clamp(.75rem, 1.5vmin, 1.15rem); color: var(--md-on-surface-variant);
}

/* ---- bracket screen ----------------------------------------------------
   The top cut plus recent results, not the whole tree — see the note in
   views/tv.js. Sized to be read across a room, which is only possible because
   there are at most seven columns of at most four matches. */
.tv-body-bracket { grid-template-columns: 1fr; grid-template-rows: 1fr auto; }
.tv-cut { min-height: 0; display: flex; flex-direction: column; }
.tv-section-note {
  margin-left: .8em; font-size: .8em; letter-spacing: .04em;
  text-transform: none; color: var(--md-on-surface-variant);
}
.tv-bracket {
  flex: 1; display: flex; gap: clamp(10px, 1.4vmin, 26px);
  align-items: stretch; min-height: 0;
  /* --tv-scale is set per render from the widest column on screen, so a round
     of eight matches shrinks to fit rather than overflowing the bottom of the
     television and taking the recent-results strip with it. */
  --tv-scale: 1;
}
.tv-round-col { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.tv-round-col > h3 {
  flex: none; margin: 0 0 .4em;
  font-size: clamp(.6rem, 1.3vmin, 1.05rem);
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--md-on-surface-variant); font-weight: 500;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tv-round-body {
  flex: 1; display: flex; flex-direction: column;
  justify-content: space-around;
  gap: calc(var(--tv-scale) * clamp(6px, 1vmin, 16px));
  min-height: 0;
}
.tv-match {
  border-radius: var(--shape-sm); overflow: hidden;
  background: var(--md-surface-container-low);
  box-shadow: inset 0 0 0 1px var(--md-outline-variant);
}
.tv-match.live { box-shadow: inset 0 0 0 2px var(--md-primary); }
.tv-match.done { background: var(--md-surface-container); }
.tv-match.bye { background: var(--md-surface); }
.tv-side {
  display: flex; align-items: center; gap: .5em;
  padding: calc(var(--tv-scale) * clamp(6px, .9vmin, 14px))
           calc(var(--tv-scale) * clamp(8px, 1.2vmin, 18px));
  font-size: calc(var(--tv-scale) * clamp(.85rem, 1.9vmin, 1.6rem));
  line-height: 1.25;
}
.tv-side + .tv-side { border-top: 1px solid var(--md-outline-variant); }
.tv-side-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tv-side-score { font-variant-numeric: tabular-nums; font-weight: 700; }
.tv-side.won { background: color-mix(in srgb, var(--md-primary) 16%, transparent); font-weight: 700; }
.tv-side.lost, .tv-side.tbd { color: var(--md-on-surface-variant); }
.tv-note {
  margin: 0 0 8px; font-size: clamp(.75rem, 1.5vmin, 1.1rem);
  color: var(--md-on-surface-variant);
}

/* ---- empty + cycle progress -------------------------------------------- */
.tv-empty {
  grid-column: 1 / -1;
  display: grid; place-content: center; text-align: center; gap: .4em;
  font-size: clamp(1rem, 2.4vmin, 2rem); color: var(--md-on-surface-variant);
}
.tv-empty-lead { margin: 0; font-size: clamp(1.6rem, 5vmin, 4rem); font-weight: 700; color: var(--md-on-surface); }
.tv-empty-kicker, .tv-complete-kicker {
  margin: 0; color: var(--md-primary); font-size: clamp(.75rem, 1.5vmin, 1.15rem);
  font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
}
.tv-empty-lead span { color: var(--md-on-surface-variant); font-size: .55em; font-weight: 500; }
.tv-empty-supporting { margin: 0; color: var(--md-on-surface-variant); }
.tv-empty-code { margin: 0; }
.tv-empty-code b { color: var(--md-primary); font-family: var(--font-mono); letter-spacing: .1em; }

/* Completion is a real venue state, not an empty queue. Give the room one
   answer to remember — who won — and one next action instead of leaving a
   blank station wall after the last set. */
.tv-body-complete {
  grid-template-columns: 1.15fr 1fr; grid-template-rows: 1fr;
  align-items: stretch;
}
.tv-complete {
  display: flex; flex-direction: column; justify-content: center;
  min-width: 0; padding: clamp(18px, 3vmin, 48px);
  border-radius: var(--shape-lg); background: var(--md-primary-container);
  color: var(--md-on-primary-container);
  box-shadow: inset 0 0 0 2px var(--md-primary);
}
.tv-complete-kicker { color: inherit; }
.tv-complete h2 {
  margin: 10px 0 0; overflow-wrap: anywhere;
  font-size: clamp(2rem, 6vmin, 5.2rem); line-height: 1; letter-spacing: -.03em;
}
.tv-complete-label { margin: 10px 0 0; font-size: clamp(1rem, 2vmin, 1.5rem); }
.tv-complete-final { margin: 8px 0 0; font-size: clamp(.9rem, 1.8vmin, 1.35rem); }
.tv-complete-final span { display: block; margin-top: 3px; }
.tv-complete-next { display: flex; flex-direction: column; justify-content: center; }

@media (max-width: 899px) {
  .tv-head { gap: 12px; }
  .tv-head-right { max-width: 45%; }
  .tv-body-complete { grid-template-columns: 1fr; grid-template-rows: 1fr auto; }
}

.tv-progress {
  flex: none; height: 4px; border-radius: 2px; overflow: hidden;
  background: var(--md-surface-container-high);
}
.tv-progress > i {
  display: block; height: 100%; width: 0;
  background: var(--md-primary);
  transition: width .25s linear;
}

/* ---- recent results strip ----------------------------------------------
   What fills the bracket screen usefully early in an event, when the top cut
   is still empty and the tree would otherwise be a wall of dashes. */
.tv-recent { flex: none; }
.tv-recent-list {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: clamp(4px, .7vmin, 10px);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
}
.tv-recent-list li {
  display: flex; align-items: baseline; gap: .55em;
  padding: clamp(6px, .9vmin, 12px) clamp(8px, 1.2vmin, 16px);
  border-radius: var(--shape-md);
  background: var(--md-surface-container-low);
  font-size: clamp(.8rem, 1.7vmin, 1.35rem);
}
.tv-recent-list b { font-weight: 700; }
.tv-recent-score {
  font-variant-numeric: tabular-nums; font-weight: 700;
  color: var(--md-primary);
}
.tv-recent-loser { color: var(--md-on-surface-variant); }
.tv-recent-round {
  margin-left: auto; font-size: .8em; white-space: nowrap;
  color: var(--md-on-surface-variant);
}

/* The control bar overlays the top of the display, so the content starts
   below it. A fixed offset rather than a hover-driven one: shifting the whole
   screen when the operator moves the mouse is worse than losing 52px. */
.tv { padding-top: calc(clamp(16px, 2.2vmin, 40px) + 44px); }

/* ---- the three-tour chooser ---------------------------------------------
   Three people look at this demo and want different things: whoever is
   running the event, whoever is watching the screen in the corner, and
   whoever is playing. Offering one tour and hoping the other two extrapolate
   is how a demo fails to land. */
.demo-tours {
  display: grid; gap: 8px; margin-top: 12px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
}
.demo-tour {
  position: relative; isolation: isolate;
  display: flex; align-items: flex-start; gap: 10px;
  padding: 10px 12px; text-align: left;
  border: 1px solid var(--md-outline-variant); border-radius: var(--shape-md);
  background: var(--md-surface); color: inherit;
  font: inherit; cursor: pointer;
}
.demo-tour::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit; background: var(--md-on-surface); opacity: 0;
  transition: opacity var(--dur-short) var(--ease-standard);
}
.demo-tour:hover::before { opacity: var(--state-hover); }
.demo-tour .icon { flex: none; margin-top: 2px; color: var(--md-primary); }
.demo-tour > span { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.demo-tour b { font: var(--title-small); }
.demo-tour .body-small { color: var(--md-on-surface-variant); }
.demo-tour-count {
  margin-left: auto; flex: none; align-self: center;
  font: var(--label-small); color: var(--md-on-surface-variant); white-space: nowrap;
}

/* Broadcast presentation uses the same publisher wordmark, but gives the
   stations and player names the useful space. Do not change the bracket's
   depth window or per-column type scaling to fit this identity. */
.tv { background: var(--md-surface-container-lowest); }
.tv-brand { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px 16px; margin: 0 0 10px; font: bold clamp(1.1rem, 2.6vmin, 2rem)/1.15 var(--font-editorial); letter-spacing: -.04em; }
.tv-brand span { font: var(--label-small); letter-spacing: .08em; text-transform: uppercase; color: var(--md-on-surface-variant); }
.tv-head { padding-bottom: 14px; border-bottom: 3px solid var(--md-primary); }
.tv[data-game="tokon"] .tv-head { border-left: 0; padding-left: 0; }
.tv-station, .tv-queue-row, .tv-match, .tv-recent-list li, .tv-complete { border-radius: 0; }
.tv-station { border-left: 5px solid var(--md-outline-variant); box-shadow: none; display: grid; grid-template-columns: minmax(90px, .3fr) minmax(0, 1fr); column-gap: 16px; }
.tv-station.busy { border-left-color: var(--md-primary); box-shadow: none; }
.tv-station.free { box-shadow: none; }
.tv-station.over { border-left-color: var(--md-error); box-shadow: none; }
.tv-station-name { font-weight: 800; color: var(--md-on-surface); }
.tv-station-meta { grid-row: 1 / 3; display: flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: 8px; border-right: 1px solid var(--md-outline-variant); padding-right: 12px; }
.tv-station-state { border-radius: 0; }
.tv-station .tv-vs, .tv-station .tv-round, .tv-station .tv-free { grid-column: 2; }
.tv-queue-row { border-bottom: 1px solid var(--md-outline-variant); }
.tv-queue-row.first { border-left: 5px solid var(--md-primary); }
@media (max-width: 599px) {
  .tv-station { grid-template-columns: 1fr; }
  .tv-station-meta { grid-row: auto; flex-direction: row; justify-content: space-between; border-right: 0; }
  .tv-station .tv-vs, .tv-station .tv-round, .tv-station .tv-free { grid-column: 1; }
}
