/* =========================================================================
   GRIDIRON TELEMETRY
   A broadcast score bug crossed with a quant terminal: a stadium at night,
   floodlit and high contrast, rendering probabilistic data precisely.

   The organising rule is that colour carries meaning. Actual results and
   model output never share a colour, so you can always tell what has already
   happened from what is merely projected.
   ========================================================================= */

/* --- fonts ------------------------------------------------------------- */
@font-face {
  font-family: 'Big Shoulders';
  src: url('fonts/big-shoulders.woff2') format('woff2');
  font-weight: 300 900;
  font-display: swap;
}
@font-face {
  font-family: 'Instrument Sans';
  src: url('fonts/instrument-sans.woff2') format('woff2');
  font-weight: 400 700;
  font-display: swap;
}
@font-face {
  font-family: 'Plex Mono';
  src: url('fonts/plex-mono.woff2') format('woff2');
  font-weight: 400 600;
  font-display: swap;
}

/* --- tokens ------------------------------------------------------------ */
/* Dark is the default, deliberately and unconditionally. This is a night-game
   scoreboard, so there is no `prefers-color-scheme` rule here: only an explicit
   data-theme="light" repitches the palette, and only the toggle sets it. */
:root {
  color-scheme: dark;

  --display: 'Big Shoulders', 'Haettenschweiler', 'Arial Narrow', sans-serif;
  --body: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif;
  --mono: 'Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;

  /* stadium at night */
  --field: #070b0e;
  /* mown turf: two grass tones a whisker apart, banded every five yards so the
     mowing stripes land exactly on the yard-line rhythm */
  --turf-a: #070b0e;
  --turf-b: #0a1014;
  --surface: #111a21;
  --surface-2: #17242d;
  --chalk: #e8edf0;
  --chalk-dim: #8fa3ae;
  --rule: #24343e;

  /* semantic: banked reality vs modelled projection vs trouble */
  --live: #c6ff3d;
  --live-dim: #7d9e33;
  --model: #4dd8e6;
  --heat: #ff6b35;
  --edge: #3d5462;

  /* categorical series, for contribution bars — chosen to stay separable */
  --series-1: #c6ff3d;
  --series-2: #4dd8e6;
  --series-3: #ff9f1c;
  --series-4: #b48ce8;

  --radius: 10px;
  --gutter: clamp(1rem, 4vw, 2.5rem);
  --shadow: 0 1px 0 rgb(255 255 255 / 4%) inset, 0 8px 30px rgb(0 0 0 / 45%);

  /* The gridiron. Every mark the field is painted with is a token, because the
     canvas reads these back through getComputedStyle: the drawing code names
     colours by their job and never knows which theme is on. Alphas are tuned
     per theme rather than shared — chalk on a night field and ink on a day
     field do not read at the same strength. */
  --paint-chalk: rgb(232 237 240 / 7%);
  --paint-chalk-strong: rgb(232 237 240 / 15%);
  --paint-number: rgb(232 237 240 / 9%);
  --paint-hash: rgb(232 237 240 / 5.5%);
  --paint-endzone: rgb(198 255 61 / 3.5%);
  --paint-wordmark: rgb(198 255 61 / 9%);
  --paint-pylon: rgb(255 107 53 / 30%);

  /* How much pinned chrome sits over the top of the page: both bars on a
     desktop, the masthead alone on a phone, nothing in a short landscape
     viewport — the breakpoints that unpin each bar reset it. Scroll targets
     use it so a linked section lands below the chrome, not under it. */
  --sticky-chrome: calc(var(--masthead-height, 60px) + var(--viewer-height, 90px));
}

/* Day game. Same semantics, repitched for a light surface. Opt-in only. */
/* Day game — a one o'clock kickoff on grass, which is what the light theme
   should always have been. It used to be a parchment page with an olive
   accent: perfectly readable, and not football. The surface is turf, the cards
   sit on it like broadcast graphics, and the lines are white paint rather than
   ink, because that is what a field is in daylight.

   Every value here was picked against a contrast target rather than by eye,
   and tests/test_theme.py holds them to it: body text clears AA on the turf,
   on the mown stripe and on a card, and so does every semantic colour it can
   appear as text in. The one thing deliberately *below* a text threshold is
   the paint, which is a field marking and not something anyone reads.

   The night palette is untouched. This is the day game, not a repaint. */
:root[data-theme='light'] {
  color-scheme: light;

  --field: #d7e5c6;
  --turf-a: #d7e5c6;
  /* The mown stripe, one pass of the mower deeper. */
  --turf-b: #cee0bb;
  --surface: #fbfdf8;
  --surface-2: #e9f1de;
  --chalk: #12210f;
  --chalk-dim: #4a5b42;
  --rule: #b9cfa6;

  --live: #14702f;
  --live-dim: #6f9c58;
  --model: #0a6c86;
  --heat: #b93c0a;
  --edge: #a3bb8e;

  --series-1: #14702f;
  --series-2: #0a6c86;
  --series-3: #a35c00;
  --series-4: #6b48a8;
  --shadow: 0 1px 2px rgb(18 33 15 / 8%), 0 8px 24px rgb(18 33 15 / 10%);

  /* White paint on grass. The night field is chalk-coloured lines on near
     black at a few per cent; this is the same idea the right way up, and it
     needs far more alpha, because white on a mid-tone green is a much smaller
     step than pale chalk on black. */
  --paint-chalk: rgb(255 255 255 / 55%);
  --paint-chalk-strong: rgb(255 255 255 / 95%);
  --paint-number: rgb(255 255 255 / 70%);
  --paint-hash: rgb(255 255 255 / 45%);
  --paint-endzone: rgb(20 112 47 / 14%);
  --paint-wordmark: rgb(255 255 255 / 72%);
  --paint-pylon: rgb(185 60 10 / 40%);
}

/* --- reset ------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

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

body {
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  font-family: var(--body);
  font-size: 16px;
  line-height: 1.5;
  color: var(--chalk);
  background-color: var(--field);
  /* Mown turf and a floodlight wash from the near end of the field. This is
     the floor the canvas paints on top of, and it is also the whole background
     when scripting is off — hence stripes that already run across the field
     the way the drawn one does, rather than a pattern the gridiron would
     contradict. Fixed, so it is anchored to the viewport and not to a page
     whose length changes from one section to the next. */
  background-image:
    linear-gradient(180deg, rgb(198 255 61 / 4%), transparent 55%),
    repeating-linear-gradient(180deg,
      var(--turf-a) 0 4.166vh, var(--turf-b) 4.166vh 8.333vh);
  background-attachment: fixed;
  -webkit-font-smoothing: antialiased;
}

img, svg { display: block; max-width: 100%; }
a { color: inherit; }

/* Nothing on this site means anything by double-tap, so no tap should wait
   the 300ms the browser holds back to rule one out. Pinch-zoom still works. */
a, button, select, summary, label { touch-action: manipulation; }

/* --- the gridiron -------------------------------------------------------
   One end zone at the top of the screen, the other at the bottom, and the
   hundred yards between them drawn to scale — a canvas rather than an SVG or
   a stack of gradients because the yard numbers have to rotate, the hash
   marks number in the hundreds, and the whole thing is repainted only when
   the viewport or the theme actually changes.

   It sits under everything (`.wrap` is z-index 1) and behind the grain, and
   it is inert to the pointer, so nothing here can ever get between a reader
   and the scoreboard. */
.gridiron {
  position: fixed;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* --- the chain crew -----------------------------------------------------
   Where the page has driven to, marked at the yard it reached.

   The first version of this was the broadcast first-down line: one bright rule
   from sideline to sideline, riding the scroll. Across an empty field that is
   exactly right, and across a paragraph it is a strike-through — it landed on
   whatever sentence you happened to be reading and cut it in half. So the
   middle is gone and the ends stay. The marks stop where the text column
   begins, which is where the chain crew stands anyway, and the reader gets the
   yard line without getting a line through the reading.

   Both marks are pseudo-elements of one node: the scroll handler writes a
   single transform, and the field underneath is never repainted. */
.drive {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;

  /* The sideline, as the canvas draws it — see fieldGeometry in site.js. */
  --sideline: min(5.5vw, 26px);
  /* How far in a mark may reach: to the edge of the text column and not a
     pixel further. The column is min(1180px, 100%) with the gutter inside it,
     so the clear margin is (viewport − column) / 2 + gutter. Percentages here
     resolve against this element, which is the viewport minus any scrollbar —
     the same box the column is centred in. Capped, because past a couple of
     hundred pixels a mark stops reading as a yard marker and starts reading as
     a rule across the top of the page. */
  --reach: min(calc(max(0px, (100% - 1180px) / 2) + var(--gutter)), 210px);
  /* Where a mark starts: on the painted sideline, unless the column gets there
     first. On a phone the whole margin is the gutter, so the mark shortens to
     a tick at the very edge of the screen rather than lying across the cards —
     the point of all this is that nothing crosses anything worth reading. */
  --chain-start: min(var(--sideline), max(0px, calc(var(--reach) - 10px)));
  --chain: var(--live);
}

/* Inside the twenty. Nothing about the page changes, but the chains do what
   they do on television, and the reader knows the end zone is close. */
.drive.is-redzone { --chain: var(--heat); }

.drive-line {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 0;
  opacity: 0;
  transition: opacity 0.5s ease;
  will-change: transform;
}
.drive.is-live .drive-line { opacity: 0.85; }
.drive.is-live.is-moving .drive-yard { opacity: 1; }

/* One mark per sideline: a short post standing on the yard, and the line
   running in off it and fading out before it reaches anything worth reading.
   Painted as a solid colour behind a two-layer mask rather than as a gradient,
   so entering the red zone can transition the colour — a gradient's stops
   cannot be animated, and a snap from green to orange looks like a bug. */
.drive-line::before,
.drive-line::after {
  content: '';
  position: absolute;
  top: -6px;
  height: 12px;
  width: max(0px, calc(var(--reach) - var(--chain-start)));
  background-color: var(--chain);
  transition: background-color 0.45s ease;
  filter: drop-shadow(0 0 7px color-mix(in srgb, var(--chain) 45%, transparent));
  mask-image: linear-gradient(#000, #000), linear-gradient(90deg, #000, transparent);
  mask-size: 2px 12px, 100% 2px;
  mask-repeat: no-repeat;
}

.drive-line::before { left: var(--chain-start); mask-position: left center, left center; }

.drive-line::after {
  right: var(--chain-start);
  mask-position: right center, right center;
  mask-image: linear-gradient(#000, #000), linear-gradient(270deg, #000, transparent);
}

/* Reaching the far end zone is the only thing on this page that can be won by
   scrolling, so it gets a flare. The class is added once, at the bottom of the
   document, and the animation plays for as long as it is there. */
.drive.is-td .drive-line::before,
.drive.is-td .drive-line::after { animation: chain-flare 0.85s ease-out; }

@keyframes chain-flare {
  0% { opacity: 1; }
  22% { opacity: 1; filter: drop-shadow(0 0 22px var(--chain)); transform: scaleY(1.9); }
  100% { opacity: 1; }
}

/* The yard the chains are on, set in the sideline under the far mark. It stays
   hidden until the drive has actually left its own goal line: at the top of a
   page the mark sits under the masthead, which would put the label in the
   viewer bar and cut it in half. */
.drive-yard {
  position: absolute;
  right: var(--sideline);
  top: 9px;
  opacity: 0;
  transition: opacity 0.35s ease, color 0.45s ease;
  font-family: var(--mono);
  font-size: 0.58rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
  color: color-mix(in srgb, var(--chain) 78%, transparent);
}

/* Only where there is a sideline to write it in. Below about 1240px the margin
   is the gutter and nothing else, and a label in the gutter is a label on the
   scoreboard. The marks carry the idea perfectly well on their own. */
@media (max-width: 1239px) {
  .drive-yard { display: none; }
}

/* A fine grain overlay so the flat dark never looks like plastic. */
.grain {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.035;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
}

.wrap {
  width: min(1180px, 100%);
  margin-inline: auto;
  padding-inline: var(--gutter);
  position: relative;
  z-index: 1;
}

/* --- masthead ---------------------------------------------------------- */
.masthead {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--field);
  background: color-mix(in srgb, var(--field) 88%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--rule);
}

.masthead-inner {
  display: flex;
  align-items: center;
  gap: 1rem;
  min-height: 60px;
  flex-wrap: wrap;
}

.brand { display: flex; align-items: center; gap: 0.6rem; text-decoration: none; }

.brand-shield {
  width: 22px; height: 22px;
  object-fit: contain;
  filter: drop-shadow(0 0 12px color-mix(in srgb, var(--live) 45%, transparent));
}

.brand-text { display: flex; align-items: baseline; gap: 0.45rem; }

.brand-name {
  font-family: var(--display);
  font-weight: 800;
  font-size: 1.45rem;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.brand-season {
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--chalk-dim);
}

.tabs { display: flex; gap: 0.25rem; margin-inline-start: auto; flex-wrap: wrap; }

.tabs a {
  --tab-accent: transparent;
  position: relative;
  padding: 0.4rem 0.75rem;
  border-radius: 6px;
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--chalk-dim);
  transition: color 0.18s, background-color 0.18s;
}

.tabs a::after {
  content: '';
  position: absolute;
  left: 0.75rem; right: 0.75rem; bottom: 0.15rem;
  height: 2px;
  background: var(--tab-accent);
  border-radius: 2px;
  transition: background-color 0.18s;
}

.tabs a:hover { color: var(--chalk); background: var(--surface); }
.tabs a[aria-current='page'] { --tab-accent: var(--live); color: var(--chalk); }

/* The two ways off the site, in the corner a broadcast keeps for its network
   bug. The model link is cyan on purpose: every projected number on this site
   is cyan, and this is where those numbers come from. */
.off-site { display: flex; align-items: center; gap: 0.4rem; }

.model-link {
  font-family: var(--mono);
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--model);
  padding: 0.34rem 0.55rem;
  border: 1px solid color-mix(in srgb, var(--model) 32%, var(--rule));
  border-radius: 8px;
  transition: color 0.18s, border-color 0.18s, background-color 0.18s, box-shadow 0.18s;
}

.model-link:hover, .model-link:focus-visible {
  border-color: var(--model);
  background: color-mix(in srgb, var(--model) 12%, transparent);
  box-shadow: 0 0 14px color-mix(in srgb, var(--model) 22%, transparent);
}

.theme-toggle, .repo-link {
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: var(--surface);
  color: var(--chalk-dim);
  cursor: pointer;
  transition: color 0.18s, border-color 0.18s;
}

/* The mark lifts towards the pointer — a whole 1px, which is all it takes for
   an icon to feel like a button rather than a picture of one. */
.repo-link { transition: color 0.18s, border-color 0.18s, transform 0.18s ease; }
.repo-link:hover, .repo-link:focus-visible {
  color: var(--chalk);
  border-color: color-mix(in srgb, var(--chalk) 30%, var(--rule));
  transform: translateY(-1px);
}
.theme-toggle:hover { color: var(--live); border-color: var(--live); }

.theme-icon {
  width: 13px; height: 13px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: inset -4px -1px 0 0 var(--surface);
}
:root[data-theme='light'] .theme-icon { box-shadow: none; }

/* --- headline strip ---------------------------------------------------- */
.state {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 1px;
  margin: 1.75rem 0 1.25rem;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
}

.state-cell { background: var(--surface); padding: 0.8rem 1rem; }

.state-label {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--chalk-dim);
}

.state-value {
  font-family: var(--display);
  font-size: 1.65rem;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: 0.01em;
}

.state-value.sm { font-size: 1.1rem; }
.state-value .unit { font-size: 0.8rem; color: var(--chalk-dim); }
.state-note { color: var(--chalk-dim); font: 0.65rem/1.6 var(--mono); margin-top: 0.4rem; }
.week-snapshot { padding: 1rem; margin-bottom: 1rem; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--surface); }
.week-snapshot .section-head { gap: 0.75rem; flex-wrap: wrap; margin: 0; }
.week-snapshot .section-title { margin: 0; }
.week-snapshot p { font: 0.8rem/1.7 var(--body); margin: 0.65rem 0; }
.week-snapshot a { color: var(--model); }
.week-snapshot .week-results-label, .week-result-status { color: var(--chalk-dim); font: 0.6rem/1.6 var(--mono); }
.week-results { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 240px)); gap: 0.6rem; list-style: none; padding: 0; margin: 0.5rem 0; }
.week-results li { padding: 0.65rem; border: 1px solid var(--rule); border-radius: 5px; background: var(--surface-2); }
.week-results li > div { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; margin-top: 0.4rem; }
.week-results .team-chip { font-size: 0.65rem; }
.week-results .chip-logo { width: 20px; height: 20px; }
.week-snapshot .week-snapshot-note { color: var(--chalk-dim); font-size: 0.7rem; }
.row-week { display: block; margin-top: 0.4rem; color: var(--chalk-dim); font: 0.65rem/1.6 var(--mono); }

/* --- page headings ----------------------------------------------------- */
.page-head { margin: 2rem 0 1rem; }

.page-title {
  font-family: var(--display);
  font-size: clamp(2.1rem, 7vw, 3.4rem);
  font-weight: 800;
  line-height: 0.95;
  letter-spacing: 0.005em;
  text-transform: uppercase;
}

.page-sub { color: var(--chalk-dim); margin-top: 0.4rem; max-width: 62ch; }

.section-title {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--chalk-dim);
  margin: 2rem 0 0.7rem;
}

/* --- the board --------------------------------------------------------- */
.board { display: flex; flex-direction: column; gap: 6px; }

.row {
  --accent: var(--edge);
  position: relative;
  display: grid;
  grid-template-columns: 2.6rem minmax(0, 1fr) auto;
  grid-template-areas:
    'rank who score'
    'rank bars bars';
  gap: 0.15rem 0.85rem;
  align-items: center;
  padding: 0.7rem 0.9rem 0.7rem 1rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  text-decoration: none;
  overflow: hidden;
  transition: transform 0.16s ease, border-color 0.16s ease, background-color 0.16s;
  animation: rise 0.5s both cubic-bezier(0.22, 1, 0.36, 1);
  animation-delay: calc(var(--i, 0) * 38ms);
}

/* The accent rail wipes in from the left on hover. */
.row::before {
  content: '';
  position: absolute;
  inset-block: 0;
  left: 0;
  width: 3px;
  background: var(--accent);
  transform: scaleY(0);
  transform-origin: 50% 0;
  transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1);
}

/* `:focus-within`, not `:focus-visible`: the row is a <div> now and a <div>
   never takes focus. The links inside it do, and when one of them has it the
   row should light up exactly as it does under the pointer. `:has()` would say
   this more precisely, but it is the one selector this stylesheet still guards
   behind @supports, and the row lighting up is not worth a browser split. */
.row:hover, .row:focus-within {
  transform: translateY(-2px);
  border-color: color-mix(in srgb, var(--accent) 45%, var(--rule));
  background: var(--surface-2);
}
.row:hover::before, .row:focus-within::before { transform: scaleY(1); }

/* A floodlight passing over the row under the pointer. One pass, on the way in
   only — a sheen that ran continuously would be the brightest moving thing on
   a page whose entire job is to hold still and be read. It is painted as a
   background, so it is clipped by the row's own corners and can never leak
   over the row above. */
.row::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  /* Above the chips, which have to be raised over the row's stretched link to
     stay clickable. Without this the floodlight would pass behind four opaque
     chips and read as a broken pass rather than one. See `.row-link` for the
     whole stack. */
  z-index: 3;
  opacity: 0;
  background-image: linear-gradient(100deg,
    transparent 44%,
    color-mix(in srgb, var(--accent) 20%, transparent) 50%,
    transparent 56%);
  background-size: 260% 100%;
  background-repeat: no-repeat;
  background-position: 100% 0;
}
.row:hover::after, .row:focus-within::after { animation: sheen 0.8s ease-out; }

/* The leader sits under a floodlight until the viewer identifies themselves.
   At that point the selected entry is the one they are trying to find, so the
   permanent first-place floodlight steps back instead of competing with it. */
html:not([data-me]) .row.is-leader {
  --accent: var(--live);
  border-color: color-mix(in srgb, var(--live) 35%, var(--rule));
  background:
    radial-gradient(120% 180% at 0% 50%,
      color-mix(in srgb, var(--live) 11%, transparent), transparent 60%),
    var(--surface);
}
html:not([data-me]) .row.is-leader::before { transform: scaleY(1); }
.row.is-eliminated { opacity: 0.62; }

.row-rank {
  grid-area: rank;
  font-family: var(--display);
  font-size: 2rem;
  font-weight: 800;
  line-height: 1;
  color: var(--chalk-dim);
  font-variant-numeric: tabular-nums;
}
html:not([data-me]) .row.is-leader .row-rank { color: var(--live); }

.row-who { grid-area: who; min-width: 0; }

/* The row's click target, kept after the row stopped being one big anchor.
   The name is the real link; this pseudo-element is what makes the rest of the
   row — the rank, the score, the bars, the whitespace — go to the same place
   it does. It is inert paint: no background, no border, nothing to see. The
   row is `position: relative` and `overflow: hidden` already, so it is
   anchored and clipped to the row's own corners for free.

   Three layers, and the order is load-bearing enough to write down:

     3  .row::after   the floodlight, over everything, and never a click target
     2  .row-teams    the chips, which have somewhere else to go
     1  .row-link     this overlay, which is everywhere else in the row
     0  everything    the rank, the score, the bars

   `z-index: 1` rather than 0 because the score carries a
   `view-transition-name`, and that makes it a stacking context of its own —
   at `auto` it would paint over an overlay it comes after in the markup, and
   the biggest number on the row would be the one part of it you could not
   tap. */
.row-link { text-decoration: none; }
.row-link::after { content: ''; position: absolute; inset: 0; z-index: 1; }
.row-link:focus-visible { outline: 2px solid var(--live); outline-offset: 3px; }

.row-name {
  font-family: var(--display);
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  overflow-wrap: anywhere;
}

/* Lifted over that overlay, or the chips would be four links nobody can reach.
   Everything else in the row is deliberately left underneath it. */
.row-teams {
  position: relative;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 0.3rem;
}

.row-score { grid-area: score; text-align: right; }

.row-points {
  font-family: var(--mono);
  font-size: 1.5rem;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.row-sub {
  font-family: var(--mono);
  font-size: 0.68rem;
  color: var(--chalk-dim);
  margin-top: 0.25rem;
  white-space: nowrap;
}

/* --- the live overlay on the board --------------------------------------
   Written by the client during games: the same provisional maths the Game
   Center's board runs, applied to the official rows in place. The gain badge
   wears the live colour because live is what that colour already means. */
.row-live-gain {
  display: block;
  font: 600 0.66rem var(--mono);
  letter-spacing: 0.04em;
  color: var(--live);
  margin-top: 0.2rem;
  white-space: nowrap;
}
.row-live-gain[hidden] { display: none; }

/* How many of this entry's games are on right now, under the weekly line —
   "3 games left" alone hides that two of them are in progress. The dot
   breathes on the same pulse the Gameday live strip uses; the global
   reduced-motion rule stills it like everything else. */
.row-live-now {
  display: block;
  font: 600 0.65rem/1.6 var(--mono);
  letter-spacing: 0.04em;
  color: var(--live);
  margin-top: 0.15rem;
  white-space: nowrap;
}
.row-live-now::before {
  content: '';
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  margin-right: 0.4rem;
  background: var(--live);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--live) 50%, transparent);
  animation: live-pulse 1.6s infinite;
}
.row-live-now[hidden] { display: none; }

/* This week's story on each row's four chips. Green ring = win in the books,
   red ring = loss, and BOTH finals glow — a lit chip always means the game is
   over, in its result's colour. A pulsing green ring is a game on right now:
   the chip brightened while its team leads, grayed while it trails, plain
   while tied. Rings are box-shadows so the chips never change size, and the
   global reduced-motion rule stills the pulse. */
.row-teams .team-chip.is-won {
  box-shadow: 0 0 0 2px var(--live), 0 0 14px color-mix(in srgb, var(--live) 60%, transparent);
}
.row-teams .team-chip.is-lost {
  box-shadow: 0 0 0 2px var(--heat), 0 0 14px color-mix(in srgb, var(--heat) 60%, transparent);
}
.row-teams .team-chip.is-live {
  box-shadow: 0 0 0 2px var(--live);
  animation: chip-live-ring 1.6s infinite;
}
.row-teams .team-chip.is-live-up {
  filter: brightness(1.2) saturate(1.25);
  box-shadow: 0 0 0 2px var(--live), 0 0 14px color-mix(in srgb, var(--live) 60%, transparent);
  animation: chip-live-ring-up 1.6s infinite;
}
.row-teams .team-chip.is-live-down { filter: grayscale(0.75) opacity(0.8); }

@keyframes chip-live-ring {
  0%, 100% { box-shadow: 0 0 0 2px var(--live), 0 0 0 0 color-mix(in srgb, var(--live) 45%, transparent); }
  50% { box-shadow: 0 0 0 2px var(--live), 0 0 0 6px transparent; }
}
@keyframes chip-live-ring-up {
  0%, 100% { box-shadow: 0 0 0 2px var(--live), 0 0 0 0 color-mix(in srgb, var(--live) 45%, transparent), 0 0 14px color-mix(in srgb, var(--live) 60%, transparent); }
  50% { box-shadow: 0 0 0 2px var(--live), 0 0 0 6px transparent, 0 0 14px color-mix(in srgb, var(--live) 60%, transparent); }
}

.board-live-status {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--chalk-dim);
  line-height: 1.8;
  margin-block: 0.8rem;
}

.row-bars {
  grid-area: bars;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 0.35rem;
  flex-wrap: wrap;
}

/* --- chips and badges -------------------------------------------------- */
.team-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.28rem;
  font-family: var(--mono);
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  padding: 0.12rem 0.36rem;
  border-radius: 4px;
  background: var(--surface-2);
  border: 1px solid var(--rule);
  color: var(--chalk-dim);
  white-space: nowrap;
}
.team-chip.scored { color: var(--chalk); border-color: color-mix(in srgb, var(--live) 30%, var(--rule)); }

/* The larger chip is the team directory's row label. Give its border a stable
   width rather than making the last glyph define the right edge: Plex Mono's
   ink can overhang its advance box by a fraction of a pixel, which is enough
   to put the final letter of a three-letter code through a tightly-fit edge. */
.team-chip.is-large {
  box-sizing: border-box;
  min-width: 4.25rem;
  justify-content: flex-start;
  padding-inline: 0.45rem 0.55rem;
}

/* Wearing the club's colours. Identity only — every chip still carries its
   three-letter code, because a dozen NFL primaries are indistinguishable from
   each other and several are indistinguishable to colour-blind vision. The
   text colour is computed per team in Python by contrast ratio, and the edge
   is the club's secondary so near-black teams keep a readable outline. */
.team-chip[style*='--team-bg'] {
  background: var(--team-bg);
  color: var(--team-fg);
  border-color: var(--team-edge);
  text-shadow: none;
}

/* A team that has not scored yet is dimmed rather than hidden. The mix has to
   be per-theme: 22% of a dark club colour over a dark surface stays dark and
   keeps its hue, but the same mix over a near-white surface comes out pastel
   and every chip on the page looks bleached in the preseason. */
.team-chip[style*='--team-bg'].dim {
  background: color-mix(in srgb, var(--team-bg) 22%, var(--surface));
  color: var(--chalk-dim);
  border-color: color-mix(in srgb, var(--team-edge) 35%, var(--rule));
}

:root[data-theme='light'] .team-chip[style*='--team-bg'].dim {
  background: color-mix(in srgb, var(--team-bg) 45%, var(--surface));
  color: var(--chalk);
  border-color: color-mix(in srgb, var(--team-edge) 55%, var(--rule));
}

a.team-chip { text-decoration: none; transition: transform 0.14s ease, filter 0.14s ease; }
a.team-chip:hover { transform: translateY(-1px); filter: brightness(1.12); }

.delta {
  font-family: var(--mono);
  font-size: 0.68rem;
  font-weight: 600;
  padding: 0.05rem 0.3rem;
  border-radius: 4px;
  animation: pop 0.4s both;
  animation-delay: calc(300ms + var(--i, 0) * 38ms);
}
.delta.up { color: var(--live); background: color-mix(in srgb, var(--live) 12%, transparent); }
.delta.down { color: var(--heat); background: color-mix(in srgb, var(--heat) 12%, transparent); }
.delta.flat { color: var(--chalk-dim); }

.badge {
  display: inline-block;
  font-family: var(--mono);
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 0.14rem 0.4rem;
  border-radius: 4px;
  border: 1px solid currentColor;
}
.badge.out { color: var(--heat); }
.badge.money { color: var(--live); }

/* --- byes ---------------------------------------------------------------
   The one thing a schedule cannot show you, because a bye has no game to
   render. A team that is off scores nobody anything that week, which is the
   usual reason somebody's week looks thin for no visible reason.

   Struck through rather than merely dimmed: dim already means "has not scored
   yet" everywhere else on the site, and a bye is a different statement. */
.byes {
  margin: 0.85rem 0 0.25rem;
  padding: 0.7rem 0.9rem;
  background: color-mix(in srgb, var(--surface) 70%, transparent);
  border: 1px dashed var(--rule);
  border-radius: var(--radius);
}

.byes-label {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--chalk-dim);
}

.bye-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 0.9rem;
  margin-top: 0.5rem;
  padding: 0;
  list-style: none;
}

/* Chip and owners side by side, not nested. The strikethrough belongs to the
   team that is not playing; the people who hold it have not been crossed out. */
.bye-team { display: inline-flex; align-items: center; gap: 0.35rem; }
.bye-team .owners { gap: 0.2rem; }

/* Struck through rather than merely dimmed: dim already means "has not scored
   yet" everywhere else on the site, and a bye is a different statement. */
.team-chip.bye { text-decoration: line-through; text-decoration-thickness: 1px; }

/* A team nobody in the pool holds is on the list for completeness and nothing
   else — it cannot cost anyone a point. */
.bye-team.is-spare { opacity: 0.45; }

.bye-note { display: inline-flex; align-items: center; gap: 0.3rem; flex-wrap: wrap; }

/* --- generic tables ---------------------------------------------------- */
/* `overflow-x: auto` forces the computed `overflow-y` to `auto` as well, which
   makes this element a scroll container. That matters for the sticky header
   below: a sticky offset resolves against the nearest scrollport, so a `top`
   meant to clear the page masthead would instead push the header that far
   *down into* the table and sit it on top of the first row. The header
   therefore sticks to `top: 0` — this container's own top edge — and never to
   a viewport-relative offset. */
/* `overscroll-behavior` keeps a sideways swipe that reaches the edge of the
   table from turning into the browser's own back-gesture mid-read. */
.table-scroll {
  overflow-x: auto;
  border-radius: var(--radius);
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}

/* Long tables scroll inside themselves so the header can genuinely stay put
   while you read down. Short ones never reach this height, so no inner
   scrollbar appears and the sticky header is simply inert. */
.table-scroll.is-tall { max-height: min(68vh, 620px); }

table {
  width: 100%;
  border-collapse: collapse;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
}

caption { text-align: left; color: var(--chalk-dim); padding: 0.6rem 0.9rem; font-size: 0.85rem; }

th, td {
  padding: 0.55rem 0.75rem;
  text-align: left;
  border-bottom: 1px solid var(--rule);
  font-size: 0.88rem;
}

thead th {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--chalk-dim);
  background: var(--surface-2);
  position: sticky;
  top: 0; /* this container's top edge — see .table-scroll above */
  z-index: 1; /* otherwise rows scroll over the header instead of under it */
  /* border-collapse drops a sticky cell's own border, so the rule under the
     header has to be drawn as a shadow or the first row runs into it. */
  box-shadow: inset 0 -1px 0 var(--rule);
}

tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: var(--surface-2); }
tbody tr:nth-child(even) { background: color-mix(in srgb, var(--surface-2) 40%, transparent); }
tbody tr:nth-child(even):hover { background: var(--surface-2); }

.num {
  font-family: var(--mono);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* --- entrant detail ---------------------------------------------------- */
.hero {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
  padding: 2rem 0 1.25rem;
  border-bottom: 1px solid var(--rule);
}

.hero-name {
  font-family: var(--display);
  font-size: clamp(2.6rem, 11vw, 5.5rem);
  font-weight: 800;
  line-height: 0.88;
  letter-spacing: 0.005em;
  text-transform: uppercase;
}

.hero-total {
  font-family: var(--mono);
  font-size: clamp(2rem, 8vw, 3.2rem);
  font-weight: 600;
  line-height: 1;
  color: var(--live);
  font-variant-numeric: tabular-nums;
}

.entrant-gameday-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 1rem;
  padding: 0.8rem 1rem;
  color: var(--model);
  background: color-mix(in srgb, var(--model) 8%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--model) 45%, var(--rule));
  border-radius: var(--radius);
  text-decoration: none;
  transition: border-color 0.18s, background-color 0.18s, transform 0.18s;
}
.entrant-gameday-link strong { display: block; font-family: var(--display); font-size: 1.1rem; }
.entrant-gameday-link small { display: block; margin-top: 0.15rem; color: var(--chalk-dim); font: 0.62rem var(--mono); }
.entrant-gameday-link > span:last-child { font-size: 1.35rem; }
.entrant-gameday-link:hover,
.entrant-gameday-link:focus-visible {
  color: var(--model);
  border-color: var(--model);
  background: color-mix(in srgb, var(--model) 14%, var(--surface));
  transform: translateY(-1px);
}

/* --- a team's own page --------------------------------------------------
   The same hero an entrant gets, with the club's badge set into the name.
   A team is an object on this site — it has a page, a colour and a record —
   and the crest is what makes the page recognisable before a word is read. */
.team-hero .hero-name { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }

.team-crest {
  width: clamp(34px, 6vw, 52px);
  height: auto;
  object-fit: contain;
  /* The same floodlight the masthead shield stands under. */
  filter: drop-shadow(0 0 16px color-mix(in srgb, var(--live) 30%, transparent));
}

.hero-score { text-align: right; }

/* A season down one column wants the week beside the kickoff, in the same
   muted mono the time is set in — it is an index, not a headline. */
.game-week {
  font-family: var(--mono);
  font-size: 0.63rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--chalk-dim);
}

/* The share bars on a team page sit in the same slot the forecast bars do,
   and have to fill it the same way. */
.forecast-bar .meter { width: 100%; height: auto; }

/* A bye in the middle of a season list. Deliberately not a game row: it has no
   sides, no price and no owners, and dressing it as one would put an empty
   fixture on the page. */
.game-bye {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  padding: 0.5rem 0.9rem 0.5rem 1rem;
  border: 1px dashed var(--rule);
  border-radius: var(--radius);
  font-size: 0.8rem;
  color: var(--chalk-dim);
}

/* The reserve lists, compact: position, player, and why they are not playing.
   Two columns where the width allows because the list is scanned, not read —
   the question it answers is "is my guy on it". */
.roster-sidelined {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 0.35rem 1.25rem;
  margin: 0.75rem 0 0;
  padding: 0;
  list-style: none;
  font-size: 0.88rem;
}

.roster-sidelined li { display: flex; align-items: baseline; gap: 0.5rem; }

/* The position is an index into the list, set like every other index on the
   site: muted mono, fixed width so the names line up into a column. */
.roster-pos {
  font-family: var(--mono);
  font-size: 0.63rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--chalk-dim);
  min-width: 2.2em;
}

/* One paragraph, not a table: the practice squad is a footnote to a roster,
   and sixteen more table rows would outweigh what they say. */
.roster-squad {
  margin-top: 1rem;
  font-size: 0.83rem;
  color: var(--chalk-dim);
  max-width: 72ch;
}

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 0.75rem;
  margin-top: 1rem;
}

.card {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0.9rem 1rem;
}

/* A team card wears its club's colour as a top rail and a faint wash. The wash
   is kept low enough that body text on it still clears contrast, so the colour
   is atmosphere rather than something the reader has to fight. */
.team-card {
  position: relative;
  overflow: hidden;
  background:
    linear-gradient(160deg,
      color-mix(in srgb, var(--team-bg, transparent) 14%, transparent),
      transparent 60%),
    var(--surface);
}

.team-card::before {
  content: '';
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 3px;
  background: linear-gradient(90deg, var(--team-bg, var(--rule)), var(--team-edge, var(--rule)));
}

/* A team card that is a link — an entrant's four picks each go to that team's
   own page. The whole card is the target, and the hover says so without the
   text dressing up as a hyperlink. */
a.team-card {
  display: block;
  color: inherit;
  text-decoration: none;
  /* The entrance properties keep the arrival timing every card gets; only the
     hover affordances run on their own faster clock. */
  transition: opacity 0.42s cubic-bezier(0.22, 1, 0.36, 1),
              transform 0.42s cubic-bezier(0.22, 1, 0.36, 1),
              border-color 0.18s ease, box-shadow 0.18s ease;
}
a.team-card:hover, a.team-card:focus-visible {
  border-color: var(--team-edge, var(--edge));
  box-shadow: 0 8px 22px rgb(0 0 0 / 0.22);
  outline: none;
}

.card-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; }

.card-team {
  font-family: var(--display);
  font-size: 1.6rem;
  font-weight: 800;
  letter-spacing: 0.02em;
}

/* The card head is a link to that team's page. No underline — a rule through a
   26px display face is a scar — so the badge and the code brighten together
   instead, the same move a chip makes. */
a.card-team { text-decoration: none; transition: filter 0.14s ease; }
a.card-team:hover, a.card-team:focus-visible { filter: brightness(1.15); }

.card-points {
  font-family: var(--mono);
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--live);
  font-variant-numeric: tabular-nums;
}

.card-meta {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--chalk-dim);
  margin-top: 0.3rem;
  display: flex;
  gap: 0.7rem;
  flex-wrap: wrap;
}

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 0.75rem;
  margin-top: 1rem;
}

.stat { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius); padding: 0.8rem 1rem; }
.stat-label { font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--chalk-dim); }
.stat-value { font-family: var(--mono); font-size: 1.5rem; font-weight: 600; font-variant-numeric: tabular-nums; margin-top: 0.15rem; }
.stat-value.live { color: var(--live); }
.stat-value.model { color: var(--model); }
.stat-note { font-size: 0.72rem; color: var(--chalk-dim); margin-top: 0.2rem; }

.back {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--chalk-dim);
  text-decoration: none;
  margin-top: 1.5rem;
}
.back:hover { color: var(--live); }

/* --- legend, prose, notes ---------------------------------------------- */
.legend {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  font-family: var(--mono);
  font-size: 0.66rem;
  color: var(--chalk-dim);
  margin-top: 0.75rem;
}
.legend span { display: inline-flex; align-items: center; gap: 0.35rem; }
.legend i { width: 14px; height: 8px; border-radius: 2px; display: inline-block; }
.key-banked { background: var(--live); }
.key-guaranteed { background: var(--live-dim); }
.key-ceiling { border: 1px solid var(--edge); }

/* Anything modelled is cyan and labelled, so a forecast is never mistaken for
   something that already happened. */
.model { color: var(--model); }

.model-tag {
  font-family: var(--mono);
  font-size: 0.55rem;
  letter-spacing: 0.14em;
  padding: 0.12rem 0.35rem;
  margin-left: 0.4rem;
  border-radius: 3px;
  color: var(--model);
  border: 1px solid currentColor;
  vertical-align: middle;
}

.net { font-size: 0.72rem; }
.net.up { color: var(--live); }
.net.down { color: var(--heat); }

.band-cell { min-width: 190px; }
.band-cell .row-sub { display: block; margin-top: 0.1rem; }

.prose { max-width: 68ch; }
.prose h2 { font-family: var(--display); font-size: 1.8rem; font-weight: 700; text-transform: uppercase; margin-top: 2rem; }
.prose h3 { font-size: 1.05rem; margin-top: 1.4rem; }
.prose p, .prose li { color: var(--chalk-dim); margin-top: 0.6rem; }
.prose strong { color: var(--chalk); }
.prose ul { padding-left: 1.1rem; }

.empty {
  text-align: center;
  padding: 3rem 1rem;
  color: var(--chalk-dim);
  border: 1px dashed var(--rule);
  border-radius: var(--radius);
}

/* --- deadlines & payment (rules page) ----------------------------------- */
/* The one box on the site that asks the reader to do something, so it gets
   the accent border nothing else uses and sits above the scoring maths. The
   QR panel stays white in both themes — a QR code needs a light quiet zone
   around it, or half the phones pointed at a dark page will not read it. */
.pay-box {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1.25rem 2rem;
  margin: 1.5rem 0 2rem;
  padding: 1.25rem 1.4rem;
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--live) 45%, var(--rule));
  border-radius: var(--radius);
}

.pay-details { flex: 1 1 30ch; max-width: 68ch; }

.pay-title {
  font-family: var(--display);
  font-size: 1.8rem;
  font-weight: 700;
  text-transform: uppercase;
}

.pay-deadlines { padding-left: 1.2rem; }
.pay-deadlines li { color: var(--chalk-dim); margin-top: 0.6rem; }
.pay-deadlines strong { color: var(--chalk); }
.pay-how { color: var(--chalk-dim); margin-top: 0.9rem; }
.pay-how strong { color: var(--chalk); }

.venmo-link {
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--chalk);
  word-break: break-all;
}

.venmo-qr {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  margin-inline: auto;
  text-decoration: none;
}

.venmo-qr img {
  display: block;
  width: 220px;
  max-width: 100%;
  height: auto;
  padding: 10px;
  background: #fff;
  border-radius: var(--radius);
  border: 1px solid var(--rule);
}

.venmo-qr-caption {
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--chalk-dim);
}

.venmo-qr:hover .venmo-qr-caption { color: var(--chalk); }

/* --- trend chart ------------------------------------------------------- */
.chart-card {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0.9rem 1rem;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}

.chart-card .trend { width: 100%; height: auto; min-width: 520px; }

/* One colour for everyone. Thirty hues would be unreadable, and colouring by
   rank would make people change colour week to week. */
.trend-line {
  stroke: var(--chalk-dim);
  opacity: 0.22;
  transition: opacity 0.15s, stroke 0.15s;
}

.trend-line.is-lead { stroke: var(--live); opacity: 0.95; }

.trend-line:hover {
  stroke: var(--live);
  opacity: 1;
  cursor: pointer;
}

.trend-label {
  font-family: var(--mono);
  font-size: 11px;
  fill: var(--chalk);
}

/* --- names inside a chart -----------------------------------------------
   An axis label is the same object as the chip or the name in the prose beside
   it, so it goes to the same page. SVG gives a link nothing to lean on — there
   is no border to lift, and an underline through 10px uppercase mono is a
   smudge — so the affordance is the fill coming up to full strength, which is
   the move every chart here already makes when it is pointed at. */
svg a { cursor: pointer; }
svg a:focus-visible { outline: 2px solid var(--model); outline-offset: 2px; }
svg a:hover .trend-label,
svg a:focus-visible .trend-label,
svg a:hover .cmp-label,
svg a:focus-visible .cmp-label,
svg a:hover .ridge-label,
svg a:focus-visible .ridge-label,
svg a:hover .heat-head,
svg a:focus-visible .heat-head { fill: var(--chalk); }
/* The one exception: a contribution segment's label is already at full strength
   on a solid fill, so brightening it has nowhere to go. It takes the underline
   the others cannot afford — at 9px on a wide block of colour, it reads. */
svg a:hover .contrib-label,
svg a:focus-visible .contrib-label { text-decoration: underline; }

.trend-tick {
  font-family: var(--mono);
  font-size: 9px;
  fill: var(--chalk-dim);
}

.contrib-label {
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 600;
  fill: var(--field);
  letter-spacing: 0.04em;
}

/* --- forecast ---------------------------------------------------------- */
/* Everything on the forecast page is modelled, so it is all cyan. The one rule
   the whole site rests on is that a projection never wears the colour of a
   banked result. */
.forecast-rows { display: flex; flex-direction: column; gap: 4px; margin-top: 0.75rem; }

.forecast-row {
  --accent: var(--model);
  position: relative;
  display: grid;
  grid-template-columns: minmax(7rem, 12rem) minmax(0, 1fr) 3.5rem;
  align-items: center;
  gap: 0.9rem;
  padding: 0.55rem 0.9rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  text-decoration: none;
  transition: transform 0.16s ease, border-color 0.16s ease, background-color 0.16s;
}

.forecast-row:hover, .forecast-row:focus-visible {
  transform: translateY(-1px);
  border-color: color-mix(in srgb, var(--model) 45%, var(--rule));
  background: var(--surface-2);
  outline: none;
}

.forecast-row.is-me { border-color: color-mix(in srgb, var(--model) 55%, var(--rule)); }

.forecast-name {
  font-family: var(--display);
  font-size: 1.15rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  overflow-wrap: anywhere;
}

.forecast-bar { min-width: 0; }
.forecast-bar .finish { width: 100%; height: auto; }

.forecast-odds {
  font-family: var(--mono);
  font-size: 1.05rem;
  font-weight: 600;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.finish-label {
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 600;
  fill: var(--field);
  letter-spacing: 0.04em;
}

/* Ridgeline: translucent fills so overlap reads as overlap. */
.chart-card .ridge { width: 100%; height: auto; min-width: 560px; }

.ridge-fill { fill: var(--model); opacity: 0.16; }
.ridge-line { stroke: var(--model); stroke-width: 1.6; opacity: 0.85; }
.ridge-label {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  fill: var(--chalk-dim);
}

/* Heatmap: the number is in every cell, so colour is reinforcement only and
   the grid stays readable in greyscale or with any colour vision. */
/* Fills the column, but never grows past the size it was drawn at. Everything
   inside an SVG scales with its viewBox, so a two-person grid — 278px wide,
   with 62px cells and 10px labels — stretched across a 1100px column is drawn
   at four times life size: cells like postcards, and an axis caption running
   off its own axis. `--heat-w` is the drawn width, written onto the element by
   the generator, so this holds for a field of any size.

   The floor is the same idea from the other end: a wide grid keeps its cells
   legible and lets `.table-scroll` scroll it sideways, rather than squashing
   eleven columns into one card. A grid narrower than the floor does not need
   one, hence the min(). Coarse pointers get a second, generated floor below:
   it scales with the number of entrants so each interactive box stays 44px. */
.heat {
  width: 100%;
  height: auto;
  max-width: var(--heat-w, 100%);
  min-width: min(520px, var(--heat-w, 520px));
}
.heat-head {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  fill: var(--chalk-dim);
}
.heat-cell {
  font-family: var(--mono);
  font-size: 12px;
  fill: var(--chalk-dim);
  font-variant-numeric: tabular-nums;
  /* The number is painted over the box that owns the events. Without this a
     tap that lands on the glyphs — the middle of the cell, where taps land —
     goes to the text and the box never hears it. */
  pointer-events: none;
}
.heat-cell.is-strong { fill: var(--chalk); font-weight: 600; }

/* The axis captions. One sentence broken across the two edges of the grid —
   without them a square of names against the same names has two opposite
   readings and no way to tell which one is on the screen. */
.heat-axis {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  fill: var(--chalk-dim);
  opacity: 0.75;
}

/* Pointing at a cell lights the pair it belongs to. The row and the column are
   the answer to "whose number is this", and the cell itself gets an outline so
   the eye lands somewhere definite. */
/* The delay is the deal-in below, and it costs nothing once the grid has
   arrived: `--i` is only ever non-zero while the wave is crossing, and the
   hover state it shares this transition with animates opacity, not transform. */
.heat-box, .heat-blank {
  transform-box: fill-box;
  transform-origin: center;
  transition: opacity 0.12s ease,
              transform 0.4s cubic-bezier(0.22, 1, 0.36, 1) calc(var(--i, 0) * 20ms);
}
.heat-head.is-lit { fill: var(--model); opacity: 1; }
.heat-box.is-picked {
  stroke: var(--model);
  stroke-width: 2;
  paint-order: stroke;
}

/* Everything off the lit pair steps back — on hover for a mouse, and under
   `has-pick` for the pair a tap is holding, because a finger has no hover. */
svg.heat:hover .heat-box:not(.is-lit),
svg.heat.has-pick .heat-box:not(.is-lit) { opacity: 0.28; }

/* Each cell answers a tap, and says so. `touch-action` keeps the browser from
   holding every tap back while it waits to see if a double-tap zoom is coming
   — the grid sits in a pinch-zoomable page either way. */
.heat-box { cursor: pointer; }
svg.heat { touch-action: manipulation; -webkit-tap-highlight-color: transparent; }

/* The hovered pair, written out. This is the part that actually removes the
   ambiguity for anyone who is not going to trace two fingers across a grid. */
.heat-wrap { margin-top: 0.5rem; }

/* A section heading with a control on the same line, pushed to the far end.
   The control belongs beside the title it changes rather than under the prose
   describing it, because the prose is one of the things it changes. */
.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1rem;
  margin-top: 2rem;
}
.section-head .section-title { margin-top: 0; }

.heat-picker {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  margin-inline-start: auto;
}

.heat-readout {
  margin-top: 0.6rem;
  min-height: 2.9em;
  font-family: var(--mono);
  font-size: 0.76rem;
  color: var(--model);
}

/* On a screen the grid does not fit, the sentence has to come to the thumb:
   a tapped cell can be two scrolls from where the readout naturally sits, and
   the names on the axes may be scrolled clean off the screen — the readout is
   then the only thing saying whose number was just tapped. Sticky to the
   bottom of the viewport while the grid is on screen, it plays the tooltip a
   hover would have been; once the reader scrolls past the grid it settles
   back into the page. Dressed as a card only here — on a desktop it reads
   fine where it is, and a floating box would just cover the grid. */
@media (max-width: 900px) {
  .heat-readout {
    position: sticky;
    bottom: 0.6rem;
    z-index: 6;
    margin-top: 0.75rem;
    padding: 0.6rem 0.8rem;
    background: var(--surface);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
  }
  .heat-readout.is-live {
    border-color: color-mix(in srgb, var(--model) 45%, var(--rule));
  }
}

/* The pair on one line, what it is worth to the pool on the next. Reserved
   height above, so filling it in does not shove the page down. */
.heat-claim, .heat-odds { display: block; }
.heat-odds { color: var(--chalk-dim); margin-top: 0.15rem; }

/* --- two questions, one forecast ----------------------------------------
   Chance of winning and expected payout are different questions with
   different answers, and the page used to show one number and leave the
   reader to decide which it was. Side by side, each with its own leader. */
.two-questions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 0.75rem;
  margin: 1.25rem 0 0.5rem;
}

.question {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--model);
  border-radius: var(--radius);
  padding: 0.9rem 1.1rem;
}

.question-label {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--chalk-dim);
}

.question-body { font-size: 0.84rem; color: var(--chalk-dim); margin-top: 0.35rem; }
.question-body strong { color: var(--chalk); }

.question-lede {
  font-family: var(--display);
  font-size: 1.6rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  margin-top: 0.5rem;
}

/* When the two questions disagree, say so. */
.callout {
  border-left: 3px solid var(--heat);
  padding: 0.5rem 0 0.5rem 0.85rem;
  margin-top: 0.75rem;
}

/* --- schedule ----------------------------------------------------------- */
/* One week is visible at a time, and which one is chosen by CSS rather than by
   script. `:target` means every week is a real URL, the back button works, and
   the page is fully usable with scripting off — the JS only ever moves the
   `is-default` class when the visitor's clock disagrees with the build's. */
/* Only one week is ever displayed, and every section sits at the same place in
   the document — so the browser's "scroll the target into view" leaves the week
   switcher just above the viewport, hidden behind the sticky masthead, and you
   have to scroll back up to change week again. A scroll margin taller than the
   page can be pins every jump to the top of the document instead, which lands
   with the switcher, the week's summary and its first games all on screen. */
.week { display: none; scroll-margin-top: 100vh; }
.week:target { display: block; }
.week.is-default { display: block; }

/* Once any week is targeted the baked default steps aside. Without `:has()` a
   deep link shows two weeks rather than none: wrong, but still readable. */
@supports selector(:has(*)) {
  .weeks:has(.week:target) .week.is-default:not(:target) { display: none; }
}

.week-switch {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin: 1.25rem 0 0.5rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--rule);
}

.week-switch a {
  min-width: 2.1rem;
  padding: 0.28rem 0.5rem;
  font-family: var(--mono);
  font-size: 0.78rem;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  color: var(--chalk-dim);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 6px;
  transition: color 0.14s ease, border-color 0.14s ease, background-color 0.14s;
}

.week-switch a:hover, .week-switch a:focus-visible {
  color: var(--chalk);
  background: var(--surface-2);
  border-color: var(--edge);
  outline: none;
}

/* The week happening right now, and the one you are looking at. They are
   usually the same, which is why the current week is the quieter of the two. */
.week-switch a.is-now { color: var(--live); border-color: color-mix(in srgb, var(--live) 40%, var(--rule)); }
.week-switch a[aria-current] {
  color: var(--field);
  background: var(--live);
  border-color: var(--live);
}

.week-switch .preseason-switch {
  color: var(--model);
  border-color: color-mix(in srgb, var(--model) 45%, var(--rule));
}

.preseason-week .page-sub { max-width: 76ch; }
.preseason-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 1rem 0 0.55rem;
}
.preseason-controls button {
  padding: 0.4rem 0.7rem;
  color: var(--chalk-dim);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 999px;
  font: 600 0.62rem var(--mono);
  cursor: pointer;
}
.preseason-controls button:hover,
.preseason-controls button:focus-visible,
.preseason-controls button[aria-pressed='true'] {
  color: var(--model);
  border-color: var(--model);
  outline: none;
}
.preseason-summary {
  min-height: 1.2em;
  margin-bottom: 0.65rem;
  color: var(--chalk-dim);
  font: 0.68rem var(--mono);
}
.preseason-games {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.55rem;
}
.preseason-game {
  min-width: 0;
  padding: 0.7rem 0.8rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}
.preseason-game.is-in {
  border-color: color-mix(in srgb, var(--heat) 60%, var(--rule));
  box-shadow: 0 0 18px color-mix(in srgb, var(--heat) 10%, transparent);
}
.preseason-when,
.preseason-matchup,
.preseason-side {
  display: flex;
  align-items: center;
}
.preseason-when {
  justify-content: space-between;
  gap: 0.8rem;
  margin-bottom: 0.55rem;
  color: var(--chalk-dim);
  font: 0.61rem var(--mono);
}
.preseason-status { color: var(--model); text-align: right; }
.preseason-game.is-in .preseason-status { color: var(--heat); }
.preseason-matchup { gap: 0.55rem; }
.preseason-side {
  flex: 1;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.42rem 0.55rem;
  color: var(--chalk-dim);
  background: var(--surface-2);
  border-radius: 5px;
  text-decoration: none;
  font: 700 0.72rem var(--mono);
}
.preseason-side .team-chip { flex: none; }
.preseason-side.is-winner { color: var(--chalk); }
.preseason-side.is-winner strong { color: var(--live); }
.preseason-side strong { font-family: var(--display); font-size: 1.35rem; }
.preseason-meta {
  margin-top: 0.55rem;
  overflow: hidden;
  color: var(--chalk-dim);
  font-size: 0.65rem;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- the playoffs --------------------------------------------------------
   A week section like any other, so the switcher, `:target` and the back
   button all work on it for free. The drawing is the tournament shape: two
   conference wings converging on the Super Bowl in the middle, each wing's
   rounds spaced so a game floats between the games that feed it, with
   connector stubs reaching inward. On a phone the wings stack, chronological
   again, and the stubs stand down — the one layout thirteen games can hold
   at 390px. */
.to-bracket { font-family: var(--display); letter-spacing: 0.08em; }

.bracket {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 2fr) minmax(0, 5fr);
  grid-template-areas: "afc sb nfc";
  gap: 1rem;
  align-items: stretch;
  margin-top: 1rem;
}

.bracket-wing[data-conf="AFC"] { grid-area: afc; }
.bracket-wing[data-conf="NFC"] { grid-area: nfc; }
.bracket-center { grid-area: sb; }

.bracket-wing { display: flex; flex-direction: column; }

.bracket-conf {
  font-family: var(--display);
  font-size: 1.05rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--chalk-dim);
  margin-bottom: 0.5rem;
}

/* The horizontal gap is the connectors' room: every stub below is drawn into
   it, so the two numbers move together. */
.wing-rounds {
  --stub: 1.1rem;
  flex: 1;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0 var(--stub);
}

.bracket-round { position: relative; display: flex; flex-direction: column; gap: 0.5rem; }

/* The NFC wing runs right-to-left, so the two conferences fight toward each
   other. Order, not direction: the cards themselves still read normally. */
.bracket-wing[data-conf="NFC"] .bracket-round[data-round="WC"] { order: 3; }
.bracket-wing[data-conf="NFC"] .bracket-round[data-round="DIV"] { order: 2; }
.bracket-wing[data-conf="NFC"] .bracket-round[data-round="CON"] { order: 1; }
.bracket-wing[data-conf="NFC"] .bracket-conf,
.bracket-wing[data-conf="NFC"] .bracket-label { text-align: end; }

.bracket-label {
  font-family: var(--mono);
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--chalk-dim);
}

/* The bracket geometry. The wild-card column sets the wing's height; auto
   margins float the later rounds' games into the space between their feeders
   — two divisional games at the quarter points, the championship in the
   middle — which is the whole reason the shape reads as a tournament rather
   than as four lists. */
.bracket-wing .tie { margin-block: auto; }

.tie {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0.5rem 0.6rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

/* Settling in respects the bracket's own order — `--i` is a tie's distance
   from the outside of its wing, so the rounds arrive as a wave breaking
   toward the trophy. The delay is only on the entrance properties: tracing a
   team (filter, below) has to answer the pointer immediately. */
.bracket .tie {
  transition: opacity 0.42s cubic-bezier(0.22, 1, 0.36, 1) calc(var(--i, 0) * 70ms),
              transform 0.42s cubic-bezier(0.22, 1, 0.36, 1) calc(var(--i, 0) * 70ms),
              filter 0.22s ease, border-color 0.22s ease;
}

/* A tie whose teams are not known yet is drawn as an outline, the same way
   everything else on this site draws a thing that has not happened. */
.tie.is-open { background: none; border-style: dashed; }

/* The connectors. Every tie sends a stub toward the middle of the page, and
   every round past the first carries a faint spine down its receiving edge
   for those stubs to land on. The spine is deliberately not a game-to-game
   line: reseeding means which winner goes where genuinely is not knowable
   until the round before is done, and a drawn pairing would be a guess
   dressed as a fact. */
.bracket-wing .tie::after,
.bracket-center .tie::after {
  content: '';
  position: absolute;
  top: 50%;
  width: var(--stub, 1rem);
  border-top: 2px solid var(--rule);
  transition: border-color 0.22s ease;
}
.bracket-wing[data-conf="AFC"] .tie::after { left: 100%; }
.bracket-wing[data-conf="NFC"] .tie::after { right: 100%; }
/* The Super Bowl reaches back into both conferences at once. */
.bracket-center .tie::after { left: 100%; width: 1rem; }
.bracket-center .tie::before {
  content: '';
  position: absolute;
  top: 50%;
  right: 100%;
  width: 1rem;
  border-top: 2px solid var(--rule);
  transition: border-color 0.22s ease;
}

.bracket-wing .bracket-round:not([data-round="WC"])::before {
  content: '';
  position: absolute;
  top: 3.2rem;
  bottom: 1.2rem;
  border-left: 2px solid color-mix(in srgb, var(--rule) 60%, transparent);
}
.bracket-wing[data-conf="AFC"] .bracket-round:not([data-round="WC"])::before { left: calc(var(--stub) / -2 - 1px); }
.bracket-wing[data-conf="NFC"] .bracket-round:not([data-round="WC"])::before { right: calc(var(--stub) / -2 - 1px); }

/* Tracing: point at a team anywhere in the bracket and its whole road to the
   trophy comes forward — every game it is in, stubs included — while the
   rest of the field steps back. Filter rather than opacity, because opacity
   is the entrance's property and the two must not queue behind each other. */
.bracket.is-tracing .tie { filter: saturate(0.3) opacity(0.38); }
.bracket.is-tracing .tie.has-path {
  filter: none;
  border-color: color-mix(in srgb, var(--live) 55%, var(--rule));
}
.bracket.is-tracing .tie.has-path::after,
.bracket.is-tracing .tie.has-path::before { border-color: var(--live); }
.bracket.is-tracing .tie-side.is-path .team-chip {
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--live) 70%, transparent),
              0 0 14px color-mix(in srgb, var(--live) 30%, transparent);
}

.tie-side {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex-wrap: wrap;
  min-height: 1.6rem;
}
.tie-side.lost { opacity: 0.55; }

.tie-seed {
  font-family: var(--mono);
  font-size: 0.58rem;
  color: var(--chalk-dim);
}

.tie-slot {
  font-family: var(--mono);
  font-size: 0.66rem;
  color: var(--chalk-dim);
}

/* A wild-card host is playing for nothing, because the bonus is for the upset.
   Zero is the most interesting number in the round and should not be dressed
   up as a stake. */
.tie .stake { margin-inline-start: auto; }
.tie .stake.is-nil { color: var(--chalk-dim); opacity: 0.6; }
.tie .game-score { margin-inline-start: auto; }
.tie .owners { flex-basis: 100%; }

/* The middle: the trophy the whole drawing converges on, the game that hands
   it over, and — once it has been played — the one team the season crowned. */
.bracket-center {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.6rem;
}
.bracket-center .bracket-conf { margin-bottom: 0; }
.bracket-center .tie { width: 100%; text-align: start; }
.bracket-center > .lombardi { margin-top: auto; }
.bracket-center > :last-child { margin-bottom: auto; }

.lombardi {
  width: 42px;
  height: auto;
  color: var(--chalk-dim);
  opacity: 0.85;
  transition: color 0.3s ease, filter 0.3s ease;
}
/* Somebody actually holds it now. */
.bracket-center:has(.champ) .lombardi {
  color: var(--live);
  opacity: 1;
  filter: drop-shadow(0 0 12px color-mix(in srgb, var(--live) 45%, transparent));
}

.champ {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  padding: 1rem 0.8rem;
  border: 1px solid color-mix(in srgb, var(--team-edge, var(--live)) 55%, var(--rule));
  border-radius: var(--radius);
  background:
    radial-gradient(120% 90% at 50% 0%,
      color-mix(in srgb, var(--team-bg, transparent) 26%, transparent),
      transparent 70%),
    var(--surface);
}
.champ-crest { filter: drop-shadow(0 6px 18px rgb(0 0 0 / 0.35)); }
.champ-name a {
  font-family: var(--display);
  font-size: 2rem;
  font-weight: 800;
  letter-spacing: 0.02em;
  color: var(--chalk);
  text-decoration: none;
}
.champ-name a:hover, .champ-name a:focus-visible { color: var(--live); }
.champ-label {
  font-family: var(--mono);
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--live);
}
.champ-owners { font-size: 0.8rem; color: var(--chalk-dim); max-width: 24ch; }

/* Below this the three-column wings have nowhere near the room they need, so
   the whole drawing becomes a stack: the AFC's rounds in order, then the
   NFC's, then the Super Bowl — each wing reading chronologically again. */
@media (max-width: 1099px) {
  .bracket {
    grid-template-columns: 1fr;
    grid-template-areas: "afc" "nfc" "sb";
  }
  .wing-rounds { grid-template-columns: 1fr; gap: 0.6rem; }
  /* Same specificity as the desktop ordering rules, or they win and the NFC
     wing stacks backwards. */
  .bracket-wing[data-conf="NFC"] .bracket-round[data-round] { order: 0; }
  .bracket-wing[data-conf="NFC"] .bracket-conf,
  .bracket-wing[data-conf="NFC"] .bracket-label { text-align: start; }
  .bracket-wing .tie { margin-block: 0; }
  .bracket-wing .tie::after,
  .bracket-center .tie::after,
  .bracket-center .tie::before,
  .bracket-wing .bracket-round::before { display: none; }
  .bracket-round {
    border-top: 1px solid var(--rule);
    padding-top: 0.6rem;
  }
  .bracket-center { border-top: 1px solid var(--rule); padding-top: 0.8rem; }
}

.week-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.75rem;
}
.week-dates { font-family: var(--mono); font-size: 0.78rem; color: var(--chalk-dim); }

.biggest {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.9rem;
  padding: 0.55rem 0.85rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--edge);
  border-radius: var(--radius);
  font-size: 0.9rem;
}
.biggest-label {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--chalk-dim);
}
.biggest-teams { display: inline-flex; align-items: center; gap: 0.4rem; color: var(--chalk-dim); }
.biggest-swing {
  margin-inline-start: auto;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--chalk-dim);
}

.games {
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 1rem;
}

.game {
  position: relative;
  display: grid;
  grid-template-columns: 10.5rem minmax(0, 1fr) 1.6rem minmax(0, 1fr) 4.5rem 1.4rem;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0.85rem 0.5rem 1.1rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
}

.game:target {
  scroll-margin-top: calc(var(--sticky-chrome) + 1rem);
  border-color: var(--model);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--model) 22%, transparent), var(--shadow);
}

/* Swing as a rail rather than a chart: a custom property and no extra elements,
   on a page that already carries 272 rows. It runs the full height and encodes
   the value in colour, because a part-height bar on a 40px row is too small a
   difference to read. The exact figure is in the callout above, where one
   precise number lands better than sixteen. */
.game::before {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 4px;
  /* --edge is the colour of everything potential rather than banked, so a hot
     game warms towards it rather than towards the colour of a real result. */
  background: color-mix(in srgb, var(--accent) calc(var(--swing, 0) * 100%), var(--rule));
  opacity: calc(0.45 + 0.55 * var(--swing, 0));
}

.game { --accent: var(--live-dim); }
.game.is-derby { --accent: var(--live); }
/* Nobody in the pool holds either side — a third of the season. Still listed,
   because it is the schedule, but it should not compete for attention. */
.game.is-idle { opacity: 0.5; }
.game.is-idle:hover { opacity: 0.9; }

/* --- opening a game up ---------------------------------------------------
   Every game already carries what it is worth to each side and to each of
   you; the row has only ever had room for your own number. The rest is in the
   markup and folded away — and folded away only when there is a client that
   can unfold it again, which is what `data-js` says. With scripting off,
   every panel is simply open. */
html[data-js='on'] .game-more { display: none; }
.game.is-open .game-more { display: block; }

.game-more {
  grid-column: 1 / -1;
  margin-top: 0.35rem;
  padding-top: 0.6rem;
  border-top: 1px dashed var(--rule);
}

.game-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1.75rem;
  margin: 0;
}
.game-facts div { display: flex; align-items: baseline; gap: 0.4rem; }
.game-facts dt {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--chalk-dim);
}
/* The team named in a fact is a link to that team, but the label is a 10px
   caption and cannot afford a chip. The affordance is the one the owner names
   next to it use — nothing at rest, a lift and a rule under it on the way
   past. */
.game-facts dt a { text-decoration: none; border-bottom: 1px solid transparent; }
.game-facts dt a:hover, .game-facts dt a:focus-visible {
  color: var(--chalk);
  border-bottom-color: color-mix(in srgb, var(--chalk) 45%, transparent);
}

.game-facts dd {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
}

.game-note {
  margin-top: 0.5rem;
  font-size: 0.78rem;
  color: var(--chalk-dim);
}
.game-note .owner { margin-inline-end: 0.15rem; }

/* The affordance. Small, at the end of the row, and a real button — the row
   itself is full of links, and a click handler on a row is a convenience for
   a mouse rather than a control anything else can reach. */
.game-open {
  display: grid;
  place-items: center;
  width: 1.4rem;
  height: 1.4rem;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 6px;
  background: none;
  color: var(--chalk-dim);
  cursor: pointer;
  transition: color 0.16s, border-color 0.16s, background-color 0.16s;
}
.game-open:hover, .game-open:focus-visible {
  color: var(--chalk);
  border-color: var(--rule);
  background: var(--surface-2);
}

/* A chevron drawn from a rotated corner, so there is no glyph to load and no
   second element to place. */
.game-open-mark {
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}
.game.is-open .game-open-mark { transform: translateY(1px) rotate(225deg); }

/* --- only the games that matter -----------------------------------------
   A third of any week is games nobody in the pool holds. They are dimmed
   always and can be dropped entirely, which is what turns a 16-game week into
   the handful worth reading. The filter is one attribute on the root, so it
   costs the client nothing per row and survives a week switch. */
html[data-slate='pool'] .game.is-idle,
html[data-slate='pool'] .bye-team.is-spare { display: none; }

/* Only reachable with the filter on, and only in a week the pool has no side
   in at all — where the list would otherwise simply go blank. */
.slate-empty { display: none; }
html[data-slate='pool'] .slate-empty { display: block; }

/* The current week has very few idle games once a large pool has collectively
   picked most of the league. Make the toggle itself confirm that the filter is
   on, even when only one row disappears below the fold. */
html[data-slate='pool'] [data-slate-toggle] {
  color: var(--live);
  border-color: color-mix(in srgb, var(--live) 78%, var(--rule));
  background: color-mix(in srgb, var(--live) 13%, var(--surface));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--live) 12%, transparent);
}

.game-when {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.1rem 0.4rem;
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--chalk-dim);
  line-height: 1.3;
}
.game-status { color: var(--chalk-dim); opacity: 0.75; text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.62rem; }
/* The neutral-site marker is a footnote, not a headline — it must never be the
   thing that decides how tall a row is. */
.game-when .badge { font-size: 0.55rem; padding: 0 0.25rem; }

.game-side { display: flex; align-items: center; gap: 0.45rem; min-width: 0; }
.game-side.home { justify-content: flex-start; }
.game-at {
  font-family: var(--mono);
  font-size: 0.65rem;
  color: var(--chalk-dim);
  text-align: center;
  opacity: 0.6;
}

.stake {
  font-family: var(--mono);
  font-size: 0.82rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--chalk);
}
.p-win { font-family: var(--mono); font-size: 0.72rem; font-variant-numeric: tabular-nums; }
.game-score {
  font-family: var(--mono);
  font-size: 0.95rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.game-side.lost .game-score, .game-side.lost .stake { color: var(--chalk-dim); }
.game-side.won .game-score { color: var(--live); }

.owners { display: flex; flex-wrap: wrap; gap: 0.25rem; min-width: 0; }
.owner {
  font-family: var(--mono);
  font-size: 0.63rem;
  padding: 0.05rem 0.3rem;
  color: var(--chalk-dim);
  border: 1px solid var(--rule);
  border-radius: 4px;
  white-space: nowrap;
}
.owner.is-me { color: var(--live); border-color: color-mix(in srgb, var(--live) 45%, var(--rule)); }

/* A name is a link to that person's page wherever it appears. Underlining it
   would put a rule through a 10px chip in a row that already has five of them;
   the border is the affordance, and it brightens under the pointer. */
a.owner { text-decoration: none; transition: color 0.14s ease, border-color 0.14s ease; }
a.owner:hover, a.owner:focus-visible {
  color: var(--chalk);
  border-color: color-mix(in srgb, var(--chalk) 40%, var(--rule));
}
a.owner.is-me:hover { color: var(--live); border-color: var(--live); }

.game-mine {
  font-family: var(--mono);
  font-size: 0.82rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--live);
}
/* Every entrant's stake is in the markup; exactly the one that is yours is
   revealed, by the same mechanism as the "you" badge. */
.mine { display: none; }
.mine.is-me { display: inline-grid; justify-items: end; gap: 0.08rem; }
.mine-locked {
  padding: 0.08rem 0.28rem;
  color: var(--field);
  background: var(--live);
  border-radius: 999px;
  font-size: 0.48rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  line-height: 1.25;
  text-transform: uppercase;
}

@supports selector(:has(*)) {
  .game:has(.mine.is-me) { --accent: var(--live); border-color: color-mix(in srgb, var(--live) 28%, var(--rule)); }
}

/* --- compare chart ----------------------------------------------------- */
/* The emphasis chart above answers "what is the shape of the season". This one
   answers "how am I doing against Paul", which is a different question and
   needs the opposite treatment: the viewer names a handful of people, so a few
   genuinely distinct colours are correct here where thirty would not be. */
.compare-picks {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.75rem 0;
}

.pick {
  --pick-color: var(--chalk-dim);
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--mono);
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.3rem 0.6rem;
  border-radius: 999px;
  border: 1px solid var(--rule);
  background: var(--surface);
  color: var(--chalk-dim);
  cursor: pointer;
  transition: color 0.16s, border-color 0.16s, background-color 0.16s, transform 0.16s;
}

.pick::before {
  content: '';
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--pick-color);
  opacity: 0.35;
  transition: opacity 0.16s;
}

.pick:hover { color: var(--chalk); transform: translateY(-1px); }

.pick[aria-pressed='true'] {
  color: var(--chalk);
  border-color: var(--pick-color);
  background: color-mix(in srgb, var(--pick-color) 14%, var(--surface));
}
.pick[aria-pressed='true']::before { opacity: 1; }

.pick.is-me { border-style: dashed; }
.pick .pick-you {
  font-size: 0.55rem;
  letter-spacing: 0.12em;
  color: var(--chalk-dim);
}

.compare-card { position: relative; }
.compare-card .compare { width: 100%; height: auto; min-width: 520px; }

/* Unpicked lines stay on the page as context — the shape of the field is
   informative even when you are only reading two of them — but drop back far
   enough that they never compete with a picked line. */
.cmp-line {
  stroke: var(--chalk-dim);
  stroke-width: 1.2;
  opacity: 0.16;
  transition: opacity 0.2s, stroke-width 0.2s;
}

.cmp-line.is-picked {
  stroke: var(--pick-color);
  stroke-width: 2.6;
  opacity: 1;
}

.cmp-dot, .cmp-label { opacity: 0; transition: opacity 0.2s; }
.cmp-dot.is-picked, .cmp-label.is-picked { opacity: 1; }
.cmp-dot.is-picked { fill: var(--pick-color); }
.cmp-label.is-picked { fill: var(--chalk); }

/* Every label is a link to that entrant, but an unpicked one is drawn at zero
   opacity — a target over nothing anyone can see. It does not take a click
   until the name is on screen; `initCompare` does the same for the tab order. */
.cmp-label { pointer-events: none; }
.cmp-label.is-picked { pointer-events: auto; }

.cmp-label {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
}

.compare-empty {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--chalk-dim);
  margin-top: 0.5rem;
}

/* --- "this is me" ------------------------------------------------------ */
/* Pick your name once and it follows you around the site. Six near-identical
   rows on a phone is exactly the situation where finding yourself is the whole
   job, so this earns its keep. */
.row.is-me {
  --accent: var(--model);
  border-color: color-mix(in srgb, var(--model) 40%, var(--rule));
  background:
    radial-gradient(120% 180% at 0% 50%,
      color-mix(in srgb, var(--model) 11%, transparent), transparent 60%),
    var(--surface);
}
.row.is-me::before { transform: scaleY(1); }
.row.is-me .row-rank { color: var(--model); }

.badge.you { color: var(--model); }
[data-me-only] { display: none; }
.is-me [data-me-only] { display: inline-block; }

/* --- viewer bar -------------------------------------------------------- */
/* A thin strip under the masthead for the two settings that change what you
   see. It has to read as chrome rather than as content, or it competes with
   the standings for the top of the page — hence the muted surface, the small
   type, and the right alignment that tucks it under the nav. */
/* Sticky directly beneath a masthead whose measured height can change when the
   phone nav wraps. Its denser background keeps the field legible through it;
   the masthead stays one layer above so the two pieces of chrome never cross. */
.viewer-bar {
  position: sticky;
  top: var(--masthead-height, 60px);
  z-index: 19;
  background: var(--surface);
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--rule);
}

.viewer-inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto auto;
  align-items: center;
  gap: 1.25rem;
  padding-block: 0.45rem;
}

/* --- how old is this? ---------------------------------------------------
   Two stamps at the top of every page. The age beside the first one is what
   people actually read: "8:51 PM" answers nothing on its own, and "2h ago"
   answers the entire question. It is filled in by the client because only the
   client knows what time it is now. */
.freshness {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  flex-wrap: wrap;
  margin-inline-end: auto;
}

/* The stamp wraps between its label and its time; the time itself never
   breaks. Holding the whole item on one line is what it used to do, and on a
   390px phone that one line was wider than the phone — which scrolls the
   entire document sideways, leaderboard included. */
.fresh-item {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  flex-wrap: wrap;
  min-width: 0;
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--chalk-dim);
}

.fresh-item time, .fresh-value { color: var(--chalk); white-space: nowrap; }

.fresh-age {
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.1rem 0.36rem;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--live) 35%, var(--rule));
  color: var(--live);
}

/* Past a day the data is old enough that the badge should stop agreeing with
   itself. Same shape, the site's one colour for trouble. */
.fresh-age.is-stale {
  color: var(--heat);
  border-color: color-mix(in srgb, var(--heat) 45%, var(--rule));
}

/* Label beside the control, not above it, so the bar stays one line tall. */
.viewer-inner .me-picker,
.viewer-inner .tz {
  flex-direction: row;
  align-items: center;
  gap: 0.45rem;
}

.viewer-label {
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--chalk-dim);
  white-space: nowrap;
}

/* The 5-second updates toggle: the one live-data preference, kept with the
   other viewer choices so no live page has to grow its own button. The state
   pill is painted by the client; with scripting off the control is inert and
   the pages simply poll at their calm default. */
.fast-refresh {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.3rem 0.5rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 6px;
  cursor: pointer;
}

.fast-refresh-state {
  font: 600 0.66rem var(--mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--chalk-dim);
}

.fast-refresh:hover { border-color: var(--edge); }
.fast-refresh:focus-visible { outline: 2px solid var(--model); outline-offset: 2px; }
.fast-refresh[aria-pressed='true'] { border-color: var(--model); }
.fast-refresh[aria-pressed='true'] .fast-refresh-state { color: var(--model); }

.me-picker { display: flex; flex-direction: column; gap: 0.15rem; }
.me-picker select, .tz select, .heat-picker select {
  font-family: var(--mono);
  font-size: 0.76rem;
  padding: 0.3rem 0.5rem;
  color: var(--chalk);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 6px;
  max-width: 100%;
}

/* A long entrant name should widen the option list, not the page chrome. */
.me-picker select { width: min(15rem, 30vw); }
.tz select { width: min(9rem, 20vw); }
.me-picker select:focus-visible,
.tz select:focus-visible,
.heat-picker select:focus-visible { outline: 2px solid var(--live); }

/* iOS Safari zooms the whole page into any form control it focuses whose text
   is under 16px, and then leaves it zoomed — so on touch screens the selects
   get the one size that never triggers it. Scoped to coarse pointers rather
   than a width: it is the finger, not the phone, that Safari is reacting to. */
@media (pointer: coarse) {
  .me-picker select, .tz select, .heat-picker select { font-size: 16px; }
}

/* The one select on the site that changes a chart rather than a preference,
   so it is cyan like everything else the model owns. */
.heat-picker select {
  color: var(--model);
  border-color: color-mix(in srgb, var(--model) 32%, var(--rule));
}

/* --- sortable tables --------------------------------------------------- */
/* The indicator is an inline ::after rather than an absolutely positioned one.
   Absolute positioning would need the th to be `position`ed, which collides
   with the phone rule that makes thead th static — and an attribute selector
   outranks it, so the arrows would silently dictate the header's positioning.
   Flowing the glyph inline sidesteps that entirely. */
th[data-sort] { cursor: pointer; user-select: none; white-space: nowrap; }
th[data-sort]:hover { color: var(--chalk); }

th[data-sort]::after { content: ' ↕'; opacity: 0.35; }
th[data-sort][aria-sort='ascending']::after { content: ' ↑'; opacity: 1; color: var(--live); }
th[data-sort][aria-sort='descending']::after { content: ' ↓'; opacity: 1; color: var(--live); }

/* --- definitions --------------------------------------------------------
   Half the words on this site mean something specific here and something
   vaguer everywhere else. The button beside them opens the full definition.

   The popover is `position: fixed` and placed by the client rather than
   absolutely positioned against the button, because several of these live
   inside a table — and a table is a clipping context, so an absolutely
   positioned tooltip would be sliced off at the edge of its own cell. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.term { position: relative; display: inline; }

/* Child selectors here for the same specificity reason as `.term > .def`. */
.term > .info {
  display: inline-grid;
  place-items: center;
  width: 15px; height: 15px;
  margin-inline-start: 0.28em;
  padding: 0;
  vertical-align: baseline;
  translate: 0 1px;
  border: 1px solid var(--rule);
  border-radius: 50%;
  background: transparent;
  color: var(--chalk-dim);
  cursor: help;
  transition: color 0.14s, border-color 0.14s, background-color 0.14s;
}

.term > .info > .info-mark {
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 600;
  line-height: 1;
  font-style: italic;
}

.term > .info:hover, .term.is-open > .info {
  color: var(--field);
  background: var(--model);
  border-color: var(--model);
}
.term > .info:focus-visible { outline: 2px solid var(--model); outline-offset: 2px; }

/* The tap target is bigger than the dot it draws — 15px is a fine target for
   a mouse and a poor one for a thumb. */
.term > .info::after {
  content: '';
  position: absolute;
  inset: -10px;
  border-radius: 50%;
}

/* Closed with `display: none`, not merely hidden. A hidden-but-laid-out box is
   still 300px wide somewhere in the document, and a `fixed` element inside any
   transformed ancestor positions against that ancestor rather than the
   viewport — which the reveal animation makes true of every block not yet
   scrolled to. On a 390px phone that pushed the page out to 502px wide and
   scrolled the whole scoreboard sideways. Taken out of layout it cannot.

   Written as a child selector purely for specificity. These definitions get
   dropped inside whatever markup already labels the thing they explain, and
   some of that markup styles its children by type — `.legend span` is the one
   that bit: at (0,1,1) it outranked a bare `.def` at (0,1,0) and un-hid every
   definition in a legend, which put a tooltip in the top-left corner of the
   schedule page eighteen times over. `.term > .def` is (0,2,0) and wins. */
.term > .def {
  position: fixed;
  display: none;
  z-index: 60;
  top: 0;
  left: 0;
  width: min(300px, calc(100vw - 2rem));
  padding: 0.65rem 0.8rem;
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--model) 35%, var(--rule));
  border-radius: 8px;
  box-shadow: var(--shadow);
  font-family: var(--body);
  font-size: 0.78rem;
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: normal;
  text-transform: none;
  text-align: left;
  color: var(--chalk-dim);
  opacity: 1;
  pointer-events: none;
}

.term.is-open > .def {
  display: block;
  animation: def-in 0.14s ease both;
}

@keyframes def-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}

.def-term {
  display: block;
  font-family: var(--mono);
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--model);
  margin-bottom: 0.25rem;
}

.def-body { display: block; }

/* The same dot, shown inline in a sentence on the rules page so "these
   buttons" has something to point at. */
.info-example {
  display: inline-grid;
  place-items: center;
  width: 15px; height: 15px;
  border: 1px solid var(--rule);
  border-radius: 50%;
  font-family: var(--mono);
  font-size: 9px;
  font-style: italic;
  color: var(--chalk-dim);
  vertical-align: middle;
}

/* --- skip link --------------------------------------------------------- */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 0.6rem 1rem;
  background: var(--live);
  color: var(--field);
  font-family: var(--mono);
  font-size: 0.75rem;
  font-weight: 600;
  border-radius: 0 0 6px 0;
}
.skip:focus { left: 0; }

/* --- footer ------------------------------------------------------------ */
/* Positioned for the same reason as `.viewer-bar`: its rule is a border on an
   unpositioned block, and the field would otherwise be painted over it. */
.footer {
  position: relative;
  z-index: 1;
  margin-top: auto;
  padding: 1.5rem 0 2.5rem;
  border-top: 1px solid var(--rule);
}

.footer-inner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}

.stamps { display: flex; gap: 1.5rem; flex-wrap: wrap; }

.stamp { font-family: var(--mono); font-size: 0.72rem; color: var(--chalk-dim); }
.stamp-label { display: block; font-size: 0.6rem; letter-spacing: 0.14em; text-transform: uppercase; opacity: 0.75; }

.tz { display: flex; flex-direction: column; gap: 0.15rem; }

/* --- motion ------------------------------------------------------------ */
@keyframes rise {
  from { opacity: 0; transform: translateY(9px); }
  to { opacity: 1; transform: none; }
}

@keyframes pop {
  from { opacity: 0; transform: scale(0.8); }
  to { opacity: 1; transform: none; }
}

@keyframes sheen {
  from { opacity: 1; background-position: 100% 0; }
  70% { opacity: 1; }
  to { opacity: 0; background-position: 0 0; }
}

/* --- arriving ------------------------------------------------------------
   Blocks settle into place as you reach them. The hidden state is added by
   the client and taken off again on first sight, so with scripting off none
   of this exists and nothing is ever invisible. Short and once: a page you
   scroll back up should not replay itself at you. */
.will-reveal {
  opacity: 0;
  transform: translateY(12px);
}

.state-cell, .stat, .card, .chart-card, .table-scroll, .forecast-row, .heat-wrap {
  transition: opacity 0.42s cubic-bezier(0.22, 1, 0.36, 1),
              transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);
}

/* The grid deals itself in on the diagonal, every cell holding for as long as
   it is far from the corner. Row by row would be a hundred cells arriving in
   reading order, which is long enough to be a wait; a diagonal wave crosses
   the whole thing in one pass. The numbers are not scaled with the cells —
   type that grows into place reads as a zoom, and the wave is not about the
   text. */
.will-reveal .heat-box, .will-reveal .heat-blank { transform: scale(0.16); }
.will-reveal .heat-cell { opacity: 0; }
.heat-cell { transition: opacity 0.4s ease calc(var(--i, 0) * 20ms); }

/* A bar that grows out of its own baseline as it arrives. Scaling the whole
   graphic sidesteps having to know anything about what is inside it, which
   is the point — these are server-rendered SVG and their geometry is not the
   client's business. */
.forecast-bar svg, .outlook, .band-cell svg, .meter {
  transform-origin: left center;
  transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.will-reveal .forecast-bar svg,
.will-reveal .outlook,
.will-reveal .band-cell svg,
.will-reveal .meter {
  transform: scaleX(0.02);
}

/* Clicking a row morphs it into the detail page hero. Needs no framework —
   this is a plain multi-page site, and the browser does the tweening.
   The names are assigned per entrant in the templates: a row's name and score
   carry the same `view-transition-name` as the hero name and total on that
   entrant's page, which is what tells the browser they are the same object
   moving rather than two elements to cross-fade. Names must be unique within a
   document, and a slug is unique by construction. */
@view-transition { navigation: auto; }

::view-transition-old(root), ::view-transition-new(root) { animation-duration: 0.25s; }

/* The morph should feel like the row lifting into place, not a slow zoom. */
::view-transition-group(*) {
  animation-duration: 0.34s;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}

/* A morphing element is lifted out of the page, so the row's own entry
   animation must not fire underneath it and fight the transition. */
html:active-view-transition .row { animation: none; }

/* The tab you are on is the same object on every page, so the browser slides
   it from where it was to where it is instead of fading one underline out
   under another. Two pages, one bar of light moving between them. */
.tabs a[aria-current='page'] { view-transition-name: tab-current; }

/* --- the floodlights ----------------------------------------------------
   Changing the theme is a wipe: a circle of the new palette opening out of
   the switch that asked for it, which is roughly what a stadium does when the
   lights come up. The geometry is written onto the root by initTheme, because
   only the client knows where the button ended up. */
html.is-relighting::view-transition-old(root) {
  animation: none;
  mix-blend-mode: normal;
}

html.is-relighting::view-transition-new(root) {
  animation: relight 0.55s cubic-bezier(0.22, 1, 0.36, 1);
  mix-blend-mode: normal;
}

@keyframes relight {
  from { clip-path: circle(0 at var(--sweep-x) var(--sweep-y)); }
  to { clip-path: circle(var(--sweep-r) at var(--sweep-x) var(--sweep-y)); }
}

/* Anything carrying a `view-transition-name` is lifted clean out of the page
   and animated as its own object — exactly right when a row is becoming a
   hero, and exactly wrong here, because whatever is lifted out is not inside
   the circle and would change palette on its own while the wipe went round
   it. For the length of the wipe there is one object on the screen, and it is
   the page. The `!important` is not decoration: the row names carry theirs in
   a style attribute, which nothing weaker can outrank. */
html.is-relighting .tabs a[aria-current='page'],
html.is-relighting [style*='view-transition-name'] {
  view-transition-name: none !important;
}

/* --- phones ------------------------------------------------------------ */
/* The leaderboard must never scroll sideways. Below 640px the row folds so
   rank, name and points stay on one line and everything else drops beneath. */
@media (max-width: 640px) {
  .masthead-inner { min-height: 54px; padding-block: 0.5rem; }
  .brand-name { font-size: 1.2rem; }
  /* One row, scrolled sideways, rather than wrapping to two. The masthead is
     sticky, so a second row of tabs costs that space on every screen for the
     whole session — and it only gets worse as pages are added. */
  .tabs {
    order: 3;
    width: 100%;
    margin-inline-start: 0;
    justify-content: flex-start;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .tabs::-webkit-scrollbar { display: none; }
  .tabs a { padding: 0.35rem 0.5rem; font-size: 0.74rem; white-space: nowrap; }

  /* Full-width bar with the name and the headline odds above it — at 390px the
     three-column layout squeezes the bar to about 55px, which shows nothing. */
  .forecast-row {
    grid-template-columns: minmax(0, 1fr) 3.2rem;
    grid-template-areas:
      'name odds'
      'bar  bar';
    gap: 0.4rem 0.6rem;
  }
  .forecast-name { grid-area: name; font-size: 1rem; }
  .forecast-odds { grid-area: odds; }
  .forecast-bar { grid-area: bar; }
  /* Tabs have dropped to their own row, so the top row is the brand and the
     three controls — pushed right as one group, and tightened up, because at
     390px the brand and the controls are within a few pixels of touching. */
  .masthead-inner { gap: 0.6rem; }
  .off-site { margin-inline-start: auto; }
  .model-link { padding-inline: 0.4rem; letter-spacing: 0.1em; }

  /* A game row is five columns of small type; at 390px that is unreadable long
     before it overflows. Stack the two sides instead, which also puts the away
     team above the home team — the way "at" already reads. */
  .game {
    grid-template-columns: minmax(0, 1fr) 4.2rem;
    grid-template-areas:
      'when mine'
      'away away'
      'home home';
    gap: 0.35rem 0.6rem;
    padding: 0.6rem 0.7rem 0.65rem 0.85rem;
  }
  .game-when { grid-area: when; flex-direction: row; flex-wrap: wrap; gap: 0.4rem; }
  .game-mine { grid-area: mine; }
  .game-side.away { grid-area: away; }
  .game-side.home { grid-area: home; }
  /* Stacked, the "at" between them has nowhere to sit — so it moves onto the
     home side, which is the one it was labelling all along. */
  .game-at { display: none; }
  .game-side.home::before {
    content: 'at';
    font-family: var(--mono);
    font-size: 0.65rem;
    color: var(--chalk-dim);
    opacity: 0.6;
  }
  .week-switch { gap: 0.25rem; }
  .biggest-swing { margin-inline-start: 0; }

  .row {
    grid-template-columns: 1.9rem minmax(0, 1fr) auto;
    padding: 0.6rem 0.7rem 0.65rem 0.85rem;
    align-items: start;
  }
  .row-rank { font-size: 1.5rem; line-height: 1.25; }
  .row-name { font-size: 1.15rem; }
  .row-points { font-size: 1.2rem; }
  .row-sub { font-size: 0.62rem; white-space: normal; }
  .row-bars { gap: 0.4rem; }
  /* The trend line is the first thing to go — the bar carries the story. */
  .row-bars .spark { display: none; }
  .row-bars .outlook { width: 100%; }
  .team-chip { font-size: 0.6rem; padding: 0.1rem 0.28rem; }

  /* The stamps take the full width and the controls sit under them. Small
     type, because this is chrome and the scoreboard is what people came for —
     but still above the fold, which is the whole point of it being here. */
  .freshness {
    width: 100%;
    gap: 0.25rem 0.9rem;
    margin-inline-end: 0;
  }
  .fresh-item { font-size: 0.62rem; }
  .fresh-age { font-size: 0.54rem; }

  /* Two controls will not fit on one 390px line beside their labels, so each
     takes half the width with its label stacked above. Still one bar, still
     above the fold. */
  .viewer-inner { justify-content: stretch; gap: 0.55rem; }
  .viewer-inner .me-picker,
  .viewer-inner .tz {
    flex: 1 1 45%;
    min-width: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0.1rem;
  }
  .viewer-inner select { width: 100%; }

  /* Three cells into an auto-fit grid leaves a fourth slot showing the grid's
     own background — an empty grey box beside the leader. Fixing the column
     count makes the wrap knowable, and then the last cell can simply take the
     rest of the row. */
  .state { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .state-cell:last-child { grid-column: 1 / -1; }
  .state-value { font-size: 1.3rem; }
  /* No inner scroll region on a phone: a nested scrollport inside a page that
     already scrolls is a trap for a thumb, so the table just runs full length
     and the page carries it. */
  .table-scroll.is-tall { max-height: none; }
  thead th { position: static; box-shadow: none; border-bottom: 1px solid var(--rule); }
  th, td { padding: 0.45rem 0.55rem; font-size: 0.8rem; }
  .footer-inner { flex-direction: column; align-items: flex-start; }
}



/* --- team logos ---------------------------------------------------------
   The mark rides inside the chip it identifies. Colour keeps its one job:
   a club that has scored shows in full colour, a club that has not is
   greyed — the same rule the rest of the site already lives by. */
.chip-logo { width: 14px; height: 14px; object-fit: contain; flex: none; }
.chip-logo-lg { width: 18px; height: 18px; }
.card-team { display: inline-flex; align-items: center; gap: 0.4rem; }
.card-team .chip-logo-lg { width: 20px; height: 20px; }
.team-chip.dim .chip-logo { filter: grayscale(1) opacity(0.65); }
:root[data-theme='light'] .team-chip.dim .chip-logo { filter: grayscale(1) opacity(0.55); }

/* --- the Details toggle -------------------------------------------------
   The board answers "who is winning" on its own. The bar, the sparkline and
   the ceiling maths are for whoever wants them, one tap away, remembered
   per browser. Off is the default on purpose. */
.page-head { position: relative; }
.detail-toggle {
  position: absolute;
  right: 0;
  top: 0.45rem;
  font-family: var(--mono);
  font-size: 0.64rem;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--chalk-dim);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0.45rem 0.9rem;
  min-height: 32px;
  cursor: pointer;
}
.detail-toggle:hover { color: var(--chalk); border-color: var(--edge); }
.detail-toggle:focus-visible { outline: 2px solid var(--model); outline-offset: 2px; }
html[data-detail='on'] .detail-toggle {
  color: var(--live);
  border-color: color-mix(in srgb, var(--live) 40%, var(--rule));
}
html:not([data-detail='on']) [data-detail-only] { display: none; }
html:not([data-detail='on']) .board .row-bars { display: none; }
html:not([data-detail='on']) .board .row {
  grid-template-areas: 'rank who score';
  align-items: center;
}

/* --- the viewer's own line ----------------------------------------------
   Filled by the client once "who are you?" is set. Answers "how am I doing"
   before a single scroll. */
.you-strip { margin: 0 0 0.9rem; }
.you-strip a {
  display: flex;
  align-items: baseline;
  gap: 0.8rem;
  padding: 0.6rem 0.95rem;
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--live) 28%, var(--rule));
  border-left: 3px solid var(--live);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--chalk);
  text-decoration: none;
  box-shadow: var(--shadow);
}
.you-strip a:hover { border-color: color-mix(in srgb, var(--live) 55%, var(--rule)); }
.you-strip .you-rank {
  font-family: var(--display);
  font-size: 1.35rem;
  font-weight: 700;
  line-height: 1;
  color: var(--live);
}
.you-strip .you-label {
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--chalk-dim);
}
.you-strip .you-pts { margin-left: auto; font-weight: 600; }

/* --- reference lives in the footer -------------------------------------- */
.footer-nav { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.footer-external { display: none; }
.footer-nav a {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--chalk-dim);
  text-decoration: none;
  padding: 0.55rem 0.6rem;
}
.footer-nav a:hover { color: var(--chalk); }

@media (max-width: 640px) {
  .chip-logo { width: 12px; height: 12px; }
  .brand-shield { width: 19px; height: 19px; }
  .detail-toggle { top: 0.1rem; padding: 0.4rem 0.75rem; }
  .you-strip a { font-size: 0.72rem; gap: 0.55rem; flex-wrap: wrap; }
  /* Tap targets: the tab row and footer links go to a comfortable height. */
  .tabs a { padding: 0.6rem 0.55rem; font-size: 0.74rem; }
  .footer-nav a { padding: 0.6rem 0.55rem; }
}

/* --- Gameday war room -------------------------------------------------- */
.eyebrow {
  font-family: var(--mono);
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--live);
}

.gameday-section-head { align-items: end; justify-content: space-between; }
.gameday-section-head .eyebrow { margin-bottom: 0.25rem; }
.rooting-tools { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 0.45rem 0.75rem; }
.rooting-filter {
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--surface);
  color: var(--chalk-dim);
  padding: 0.45rem 0.7rem;
  font: 600 0.62rem var(--mono);
  cursor: pointer;
}
.rooting-filter:hover { color: var(--chalk); border-color: var(--model); }
.rooting-filter[aria-pressed='true'] {
  color: var(--model);
  border-color: var(--model);
  background: color-mix(in srgb, var(--model) 12%, var(--surface));
}
.rooting-filter-status { margin: -0.2rem 0 0.8rem; color: var(--model); font: 0.68rem var(--mono); }
.rooting-filter-status:empty { display: none; }
.section-note, .scenario-note, .attribution {
  color: var(--chalk-dim);
  font-size: 0.76rem;
}
.section-note { font-family: var(--mono); }

.gameday-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.85rem;
}
.gameday-grid:not(.is-rewind) .gameday-card:nth-child(n+6) { display: none; }
.gameday-grid.is-help-filtered .gameday-card { display: none; }
.gameday-grid.is-help-filtered .gameday-card.is-help-game {
  display: block;
  border-color: color-mix(in srgb, var(--model) 65%, var(--rule));
}

.gameday-card {
  position: relative;
  min-width: 0;
  padding: 1rem;
  background:
    linear-gradient(135deg, color-mix(in srgb, var(--heat) 6%, transparent), transparent 45%),
    var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
  transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s;
}
.gameday-card:hover {
  border-color: color-mix(in srgb, var(--heat) 50%, var(--rule));
  transform: translateY(-2px);
}
.gameday-card.is-final { background: var(--surface); }
.gameday-card-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.gameday-card-head time { font-family: var(--mono); font-size: 0.7rem; color: var(--chalk-dim); }
.drama-score { display: flex; align-items: baseline; gap: 0.4rem; font-family: var(--mono); }
.drama-score span { font-size: 0.55rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--chalk-dim); }
.drama-score strong { font-family: var(--display); font-size: 1.65rem; line-height: 1; color: var(--heat); }
.drama-track { height: 2px; margin: 0.65rem 0 0.9rem; background: var(--rule); overflow: hidden; }
.drama-track span { display: block; width: calc(var(--drama) * 100%); height: 100%; background: var(--heat); transform-origin: left; }

.gameday-matchup {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 0.7rem;
}
.gameday-side { display: grid; justify-items: start; gap: 0.2rem; min-width: 0; }
.gameday-side:last-child { justify-items: end; text-align: right; }
.gameday-side > strong { font-family: var(--display); font-size: 1.55rem; line-height: 1; }
.gameday-side > strong span { font-family: var(--mono); font-size: 0.55rem; color: var(--chalk-dim); }
.gameday-side small { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.68rem; color: var(--chalk-dim); }
.gameday-side.is-needed .team-chip {
  outline: 2px solid var(--model);
  outline-offset: 3px;
}
.gameday-side.is-needed::after {
  content: 'outside help';
  color: var(--model);
  font: 600 0.55rem var(--mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.gameday-side.won > strong { color: var(--live); }
.gameday-side.lost { opacity: 0.55; }
.gameday-at { font-family: var(--mono); font-size: 0.65rem; color: var(--chalk-dim); }
.gameday-score { font-size: 2rem !important; }

.game-context { display: flex; flex-wrap: wrap; gap: 0.35rem; list-style: none; padding: 0; margin-top: 0.85rem; }
.game-context li {
  padding: 0.22rem 0.42rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 0.57rem;
  color: var(--chalk-dim);
}
.game-context .weather-chip { border-color: color-mix(in srgb, var(--model) 35%, var(--rule)); color: var(--model); }

.rooting-anon, .rooting-readout {
  margin-top: 0.85rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--rule);
}
.rooting-anon { display: flex; justify-content: space-between; gap: 1rem; font-size: 0.72rem; color: var(--chalk-dim); }
.rooting-anon strong { color: var(--chalk); }
.rooting-readout { display: none; }
html[data-me] .rooting-anon { display: none; }
.rooting-readout.is-me { display: block; }
.rooting-call { display: flex; align-items: center; gap: 0.55rem; }
.rooting-verb { font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.09em; text-transform: uppercase; color: var(--chalk-dim); }
.rooting-call > strong { color: var(--live); }
.rooting-odds { display: grid; gap: 0.18rem; margin-top: 0.55rem; font-family: var(--mono); font-size: 0.61rem; }
.rooting-fallback { margin-top: 0.45rem; color: var(--chalk-dim); font-size: 0.68rem; }
.attribution { margin-top: 0.55rem; }
.attribution a { color: var(--model); }

/* Live Game Center ----------------------------------------------------- */
.game-center { margin-top: 2.75rem; }
.game-center-link { display: inline-flex; gap: 0.6rem; margin-block: 0.65rem; color: var(--model); font: 600 0.75rem var(--mono); }
.live-tools { display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap; justify-content: end; }
.live-tools button, .live-copy { min-height: 44px; padding: 0.5rem 0.85rem; border: 1px solid var(--rule); border-radius: 6px; background: var(--surface); color: var(--chalk); font: 500 0.72rem var(--mono); cursor: pointer; }
.live-tools button[aria-pressed='true'] { border-color: var(--model); color: var(--model); }
[data-live-fast-state] { margin-left: 0.4rem; font-weight: 700; }
.live-connection { max-width: 25rem; color: var(--chalk-dim); font: 0.64rem/1.7 var(--mono); }
.live-slate-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 0.65rem; }
.live-game-rail { display: flex; gap: 0.55rem; overflow-x: auto; padding: 0.1rem 0.1rem 0.9rem; margin-bottom: 0.7rem; scrollbar-color: var(--edge) transparent; }
.live-game-tile { flex: 0 0 auto; min-width: 180px; padding: 0.85rem 1rem; text-align: left; color: var(--chalk); background: var(--surface); border: 1px solid var(--rule); border-radius: 8px; cursor: pointer; transition: border-color 0.15s; }
.live-game-tile:hover { border-color: var(--edge); }
.live-game-tile[aria-pressed='true'] { border-color: var(--model); box-shadow: 0 0 0 1px var(--model) inset; }
.live-tile-status { display: block; color: var(--chalk-dim); font: 0.62rem var(--mono); margin-bottom: 0.45rem; }
.live-game-tile.is-playing .live-tile-status { color: var(--heat); }
.live-game-tile.is-playing .live-tile-status::before { content: '● '; }
.live-tile-score { display: block; font: 600 0.82rem var(--mono); white-space: nowrap; }
.live-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(275px, 0.5fr); gap: 1.4rem; align-items: start; }
.live-game-center { min-width: 0; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--surface); overflow: hidden; }
.live-match-header { padding: 1.25rem; background: linear-gradient(135deg, var(--surface-2), var(--surface)); }
.live-game-meta { display: flex; align-items: center; justify-content: space-between; gap: 0.8rem; color: var(--chalk-dim); font: 0.65rem/1.6 var(--mono); }
.live-game-meta > span { min-width: 0; }
[data-live-game-status] { color: var(--heat); }
.live-game-meta a { color: var(--model); white-space: nowrap; }
.live-scoreboard { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin: 1.1rem 0 0.4rem; }
.live-score-side { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; min-width: 0; }
.live-score-side:last-child { flex-direction: row-reverse; padding-left: 1rem; border-left: 1px solid var(--rule); }
.live-big-score { font: 700 clamp(3rem, 7vw, 5rem)/1 var(--display); font-variant-numeric: tabular-nums; }
.live-score-side.has-possession .team-chip::after { content: '●'; color: var(--heat); margin-left: 0.3rem; font-size: 0.6rem; }
.live-situation { min-height: 1.4rem; color: var(--chalk); font: 0.72rem/1.7 var(--mono); text-align: center; }
.live-quarter-scores .live-stats-table { font-size: 0.67rem; margin: 0.8rem 0; }
.live-quarter-scores caption { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.live-copy { min-height: 36px; padding: 0.3rem 0.6rem; font-size: 0.62rem; background: transparent; white-space: nowrap; }
.live-share-status:empty { display: none; }
.live-share-status { color: var(--model); font-size: 0.7rem; }
.live-view-tabs { display: flex; border-block: 1px solid var(--rule); background: var(--field); }
.live-view-tabs button { flex: 1; min-height: 48px; padding: 0.8rem; border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--chalk-dim); font: 600 0.75rem var(--mono); cursor: pointer; }
.live-view-tabs button[aria-selected='true'] { border-bottom-color: var(--model); color: var(--model); background: var(--surface); }
.live-game-center [role='tabpanel'] { padding: 0 1.25rem 1.25rem; }
.live-game-center [role='tabpanel'][hidden] { display: none; }
.live-detail-status { padding: 0.75rem 1.25rem; margin: 0; color: var(--chalk-dim); font: 0.62rem/1.7 var(--mono); }
.live-field-wrap { padding: 1rem; border: 1px solid var(--rule); border-radius: 8px; background: var(--surface-2); }
.live-field-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; margin-bottom: 0.8rem; }
.live-possession { display: flex; align-items: center; gap: 0.65rem; min-width: 0; }
.live-possession strong { display: block; font: 600 0.85rem/1.4 var(--body); }
.live-field-kicker { display: block; color: var(--chalk-dim); font: 0.55rem/1.8 var(--mono); text-transform: uppercase; letter-spacing: 0.12em; }
.live-drive-direction { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; color: var(--chalk); font: 0.62rem var(--mono); }
.live-drive-direction[data-direction='left'] { flex-direction: row-reverse; }
.live-direction-arrow { color: var(--live); font: 1.8rem/1 var(--body); }
.live-field-label { color: var(--chalk); font: 500 0.73rem/1.7 var(--mono); margin: 0 0 0.65rem; }
.live-field-play { color: var(--chalk-dim); font: 400 0.8rem/1.6 var(--body); margin: 0.8rem 0 0; overflow-wrap: anywhere; }
.live-field { overflow: hidden; border: 1px solid #829b86; border-radius: 4px; background: #214e35; box-shadow: 0 3px 14px #0003; }
.live-field svg { display: block; width: 100%; height: auto; }
.live-endzone rect { fill: var(--team-bg, #193b35); }
.live-endzone path { stroke: var(--team-edge, #c7d8c6); stroke-width: 8; }
.live-endzone text { fill: var(--team-fg, #fff); font: 700 52px var(--display); letter-spacing: 3px; }
.live-yard-lines path { stroke: #f0f4de; stroke-width: 2; opacity: 0.4; }
.live-yard-lines .is-major { stroke-width: 3; opacity: 0.7; }
.live-yard-hashes { stroke: #f0f4de; stroke-width: 2; opacity: 0.65; }
.live-yard-numbers { fill: #edf2dc; font: 600 35px var(--display); opacity: 0.85; }
.live-field-boundary { fill: none; stroke: #edf2dc; stroke-width: 4; }
.live-field-marker path { stroke-width: 5; }
.live-first-down path { stroke: #ffcf40; }
.live-scrimmage path { stroke: #69cfff; }
.live-attack-arrow path { stroke: #f7ffe6; stroke-width: 6; fill: none; stroke-linecap: round; stroke-linejoin: round; opacity: 0.9; }
.live-ball { filter: drop-shadow(0 3px 4px #0008); }
.live-field-marker, .live-ball, .live-attack-arrow { transition: transform 0.45s ease; }
.live-field-legend { display: flex; align-items: center; flex-wrap: wrap; gap: 0.55rem 1rem; margin-top: 0.75rem; color: var(--chalk-dim); font: 0.6rem/1.6 var(--mono); }
.live-field-legend > span { display: inline-flex; align-items: center; gap: 0.35rem; }
.live-field-legend strong { color: var(--chalk); font-weight: 500; }
.live-line-key { width: 3px; height: 0.85rem; border-radius: 2px; }
.live-line-key.is-scrimmage { background: #69cfff; }
.live-line-key.is-first-down { background: #ffcf40; }
.live-goal-distance { margin-left: auto; }
.live-field-head[hidden], .live-field-legend[hidden], .live-field-legend [hidden] { display: none; }
.live-pool-stakes { margin-block: 1.4rem; padding-block: 1rem; border-block: 1px solid var(--rule); }
.live-game-center h3 { margin: 0 0 0.8rem; font: 600 0.85rem var(--body); }
.live-pool-stakes p { display: flex; align-items: center; gap: 0.75rem; color: var(--chalk-dim); font-size: 0.75rem; line-height: 1.7; }
.live-scoring-summary ol { list-style: none; margin: 0; padding: 0; }
.live-scoring-summary li { padding-block: 0.7rem; border-bottom: 1px solid var(--rule); }
.live-scoring-summary b { color: var(--model); font: 0.64rem var(--mono); }
.live-scoring-summary p { margin: 0.4rem 0 0; font-size: 0.78rem; line-height: 1.65; }
.live-empty { padding: 1rem 0; margin: 0; color: var(--chalk-dim); font-size: 0.78rem; line-height: 1.7; }
.live-play-notice { color: var(--model); font: 0.65rem/1.6 var(--mono); }
.live-play-notice:empty { display: none; }
.live-drive { margin-block: 0.65rem; border: 1px solid var(--rule); border-radius: 7px; }
.live-drive > summary { min-height: 44px; padding: 0.9rem; cursor: pointer; background: var(--surface-2); color: var(--chalk); font: 0.7rem/1.7 var(--mono); }
.live-drive-heading { display: inline-flex; align-items: center; gap: 0.65rem; max-width: 100%; vertical-align: middle; }
.live-drive-heading strong { font: 600 0.85rem/1.5 var(--body); }
.live-drive-heading .team-chip { flex-shrink: 0; font-size: 0.65rem; }
.live-drive-heading .chip-logo { width: 20px; height: 20px; }
.live-drive-meta { display: block; margin-top: 0.5rem; color: var(--chalk-dim); }
.live-possession-change { margin: 0; padding: 0.55rem 0.9rem; border-left: 2px solid var(--model); color: var(--chalk); font: 500 0.75rem/1.7 var(--body); overflow-wrap: anywhere; }
.live-drive ol { list-style: none; margin: 0; padding: 0 0.9rem; }
.live-play { padding: 0.9rem 0; border-top: 1px solid var(--rule); }
.live-play b { color: var(--chalk-dim); font: 0.62rem var(--mono); }
.live-play p { margin: 0.4rem 0 0; font-size: 0.8rem; line-height: 1.75; overflow-wrap: anywhere; }
.live-play.is-scoring b { color: var(--live); }
.live-play.is-turnover b { color: var(--heat); }
.live-table-wrap { max-width: 100%; overflow-x: auto; }
.live-stats-table { width: 100%; border-collapse: collapse; margin-bottom: 1.6rem; font: 0.7rem/1.6 var(--mono); }
.live-stats-table caption { text-align: left; text-transform: capitalize; font: 600 0.85rem var(--body); color: var(--chalk); padding-bottom: 0.8rem; }
.live-stats-table th, .live-stats-table td { padding: 0.6rem 0.45rem; text-align: right; border-bottom: 1px solid var(--rule); white-space: nowrap; }
.live-stats-table th { color: var(--chalk-dim); font-weight: 400; }
.live-stats-table th:first-child { text-align: left; }
.live-pool-board { position: sticky; top: calc(var(--sticky-chrome-height, 130px) + 1rem); border: 1px solid var(--rule); border-radius: var(--radius); padding: 1.25rem; background: var(--surface); }
.live-board-note { color: var(--chalk-dim); font-size: 0.72rem; line-height: 1.8; margin-block: 0.8rem; }
.live-board-labels, .live-pool-board li { display: grid; grid-template-columns: minmax(0, 1fr) 4.5rem 4.5rem; gap: 0.5rem; align-items: baseline; }
.live-board-labels { font: 0.55rem var(--mono); color: var(--chalk-dim); padding: 0.9rem 0 0.5rem; }
.live-board-labels span:not(:first-child) { text-align: right; }
.live-pool-board ol { list-style: none; padding: 0; margin: 0; }
.live-pool-board li { padding: 0.8rem 0; border-top: 1px solid var(--rule); font-size: 0.8rem; }
.live-pool-board li a { overflow-wrap: anywhere; color: var(--chalk); }
.live-pool-board li.is-me a { color: var(--live); font-weight: 700; }
.live-pool-board li strong, .live-gain { text-align: right; font: 600 0.8rem var(--mono); }
.live-gain { color: var(--model); font-size: 0.7rem; }
.live-pool-board > a { display: inline-block; min-height: 35px; padding-top: 0.5rem; color: var(--model); font-size: 0.75rem; }
.live-attribution { color: var(--chalk-dim); font: 0.62rem/1.8 var(--mono); padding-top: 1rem; }
@media (max-width: 900px) { .live-layout { grid-template-columns: minmax(0, 1fr); } .live-pool-board { position: static; } }
@media (max-width: 600px) {
  .live-tools { justify-content: start; }
  .live-match-header { padding: 1rem; } .live-game-center [role='tabpanel'] { padding: 0 1rem 1rem; }
  .live-scoreboard { gap: 0.65rem; } .live-score-side:last-child { padding-left: 0.65rem; }
  .live-score-side .team-chip { font-size: 0.85rem; gap: 0.3rem; } .live-score-side .team-chip img { width: 24px; height: 24px; }
  .live-game-meta { flex-wrap: wrap; }
  .live-field-wrap { padding: 0.7rem; }
  .live-field-head { flex-direction: column; align-items: flex-start; gap: 0.45rem; }
  .live-possession { gap: 0.4rem; } .live-possession strong { font-size: 0.73rem; }
  .live-possession .team-chip { font-size: 0.65rem; gap: 0.2rem; padding-inline: 0.35rem; }
  .live-possession .chip-logo { width: 18px; height: 18px; }
  .live-drive-direction { gap: 0.3rem; font-size: 0.56rem; }
  .live-goal-distance { flex-basis: 100%; margin-left: 0; }
  .live-pool-stakes p { align-items: start; } .live-game-tile { min-width: 165px; }
}
@media (prefers-reduced-motion: reduce) { .live-game-tile, .live-field-marker, .live-ball, .live-attack-arrow { transition: none; } }

.live-strip {
  align-items: center;
  gap: 0.55rem;
  margin: -0.2rem 0 0.8rem;
  padding: 0.42rem 0.55rem;
  background: color-mix(in srgb, var(--heat) 12%, var(--surface-2));
  border-left: 2px solid var(--heat);
  border-radius: 4px;
  font-family: var(--mono);
  font-size: 0.64rem;
}
.live-strip:not([hidden]) { display: flex; }
.live-strip [data-live-score] { color: var(--chalk); font-weight: 600; }
.live-strip [data-live-situation] { margin-inline-start: auto; color: var(--chalk-dim); }
.live-pulse { width: 7px; height: 7px; border-radius: 50%; background: var(--heat); box-shadow: 0 0 0 0 color-mix(in srgb, var(--heat) 50%, transparent); animation: live-pulse 1.6s infinite; }
@keyframes live-pulse { 50% { box-shadow: 0 0 0 6px transparent; } }
.live-detail { margin-top: 0.55rem; font-size: 0.68rem; color: var(--chalk-dim); }
.live-detail:not([hidden]) { display: block; }
.live-detail p::before { content: 'Last play  '; font-family: var(--mono); font-size: 0.56rem; text-transform: uppercase; color: var(--heat); }

.scenario { margin-top: 2.75rem; }
.scenario-presets { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-inline-start: auto; }
.scenario-presets button, .scenario-game button, .share-scenario {
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--surface);
  color: var(--chalk-dim);
  padding: 0.45rem 0.7rem;
  font: 600 0.62rem var(--mono);
  cursor: pointer;
}
.scenario-presets button:hover, .scenario-game button:hover, .share-scenario:hover {
  color: var(--chalk);
  border-color: var(--live);
}
.scenario-presets button[aria-pressed='true'],
.scenario-game button[aria-pressed='true'] {
  color: var(--live);
  border-color: var(--live);
  background: color-mix(in srgb, var(--live) 12%, var(--surface));
}
.scenario-game button.is-outside-help[aria-pressed='true'] {
  color: var(--model);
  border-color: var(--model);
  background: color-mix(in srgb, var(--model) 12%, var(--surface));
}
.scenario-note { margin: 0.45rem 0 0.9rem; max-width: 70ch; }
.scenario-layout { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(260px, 0.75fr); gap: 1rem; align-items: start; }
.scenario-games { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.5rem; }
.scenario-game { display: flex; justify-content: center; gap: 0.35rem; min-width: 0; padding: 0.7rem; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--surface); }
.scenario-game legend { width: 100%; padding: 0 0.3rem; font-family: var(--mono); font-size: 0.62rem; color: var(--chalk-dim); text-align: center; }
.scenario-game { flex-wrap: wrap; }
.scenario-drilldown {
  width: 100%;
  margin-top: 0.35rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--rule);
}
.scenario-drilldown summary {
  color: var(--model);
  cursor: pointer;
  font: 0.59rem var(--mono);
}
.scenario-drilldown[open] { display: grid; gap: 0.5rem; }
.scenario-drilldown p { display: grid; gap: 0.2rem; }
.scenario-drilldown p strong { color: var(--chalk); font-family: var(--display); font-size: 1.15rem; }
.scenario-drilldown [data-scenario-impact] { color: var(--chalk-dim); font-size: 0.65rem; line-height: 1.4; }
.scenario-drilldown nav { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.scenario-drilldown a {
  padding: 0.3rem 0.5rem;
  color: var(--chalk-dim);
  border: 1px solid var(--rule);
  border-radius: 999px;
  font: 0.58rem var(--mono);
  text-decoration: none;
}
.scenario-drilldown a:hover,
.scenario-drilldown a:focus-visible,
.scenario-drilldown a.is-called { color: var(--model); border-color: var(--model); }
.scenario-board-wrap { position: sticky; top: calc(var(--masthead-height, 60px) + var(--viewer-height, 90px) + 1rem); padding: 0.8rem; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); }
.scenario-board { display: grid; gap: 0.3rem; padding: 0; list-style: none; counter-reset: none; }
.scenario-board li { display: grid; grid-template-columns: 2rem minmax(0, 1fr) auto; align-items: baseline; gap: 0.55rem; padding: 0.35rem 0.45rem; border-radius: 5px; transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), background-color 0.2s; }
.scenario-board li.is-me { background: color-mix(in srgb, var(--live) 10%, transparent); }
.scenario-rank { font-family: var(--display); font-size: 1.25rem; color: var(--chalk-dim); }
.scenario-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.scenario-total { font-family: var(--mono); color: var(--live); }
.share-scenario { margin-top: 0.8rem; width: 100%; color: var(--model); }
.share-scenario-help {
  margin-top: 0.45rem;
  color: var(--chalk-dim);
  font-size: 0.62rem;
  line-height: 1.45;
  text-align: center;
}
.scenario-status { min-height: 1.4em; margin-top: 0.35rem; color: var(--chalk-dim); font-size: 0.65rem; text-align: center; }
/* A game the live feed reported final: called by the real score, locked. */
.scenario-game.is-live-final button[data-scenario-choice] { cursor: default; opacity: 0.45; }
.scenario-game.is-live-final button[data-scenario-choice][aria-pressed='true'] { opacity: 1; }
.scenario-game.is-live-final legend::after { content: ' · Final'; color: var(--heat); }
html.is-goal-line .scenario-board-wrap { animation: goal-line 0.75s ease; box-shadow: 0 0 0 2px var(--live), 0 0 34px color-mix(in srgb, var(--live) 32%, transparent); }
@keyframes goal-line { 50% { transform: scale(1.018); } }

@media (max-width: 760px) {
  .gameday-grid, .scenario-layout, .scenario-games,
  .preseason-games { grid-template-columns: 1fr; }
  .scenario-board-wrap { position: static; }
  .gameday-card:hover { transform: none; }
  .scenario-presets { margin-inline-start: 0; }
}

/* --- mobile hardening --------------------------------------------------
   The header's desktop row needs almost 900px once real labels and controls
   are present. Between the old phone breakpoint and that width flex-wrap made
   several accidental arrangements: tabs above the brand at one width, the
   external links alone on a second row at another, and three sticky rows in a
   narrow landscape viewport. Give every compact screen the same intentional
   two-row header instead. Both rows scroll only on their own axis; the page
   itself never does. */
@media (max-width: 959px) {
  .masthead-inner {
    min-height: 54px;
    padding-block: 0.35rem;
    column-gap: 0.6rem;
    row-gap: 0.25rem;
  }

  .brand-name { font-size: 1.2rem; }
  .off-site { margin-inline-start: auto; }
  .model-link { padding-inline: 0.4rem; letter-spacing: 0.1em; }

  .tabs {
    order: 3;
    width: 100%;
    margin-inline-start: 0;
    justify-content: flex-start;
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .tabs::-webkit-scrollbar { display: none; }
  .tabs a {
    flex: 0 0 auto;
    white-space: nowrap;
    scroll-snap-align: center;
  }

  /* Viewer preferences take one predictable row beneath their timestamps,
     then scroll away with the opening screen instead of becoming 100px of
     permanent chrome. */
  .viewer-bar { position: relative; top: auto; }
  :root { --sticky-chrome: var(--masthead-height, 54px); }
  .viewer-inner {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    justify-content: stretch;
    gap: 0.55rem;
  }
  .freshness {
    grid-column: 1 / -1;
    width: 100%;
    gap: 0.25rem 0.9rem;
    margin-inline-end: 0;
  }
  .viewer-inner .me-picker,
  .viewer-inner .tz {
    min-width: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0.1rem;
  }
  .viewer-inner select { width: 100%; }
  /* The toggle takes its own full row under the two pickers, label on the
     left and state on the right, so a thumb has the whole bar to hit. */
  .viewer-inner .fast-refresh {
    grid-column: 1 / -1;
    justify-content: space-between;
    min-height: 40px;
  }
}

/* A viewer bar over 100px tall is useful setup and poor permanent chrome.
   Let it leave with the rest of the opening screen on a phone; the primary
   section navigation remains available above it. */
@media (max-width: 640px) {
  /* Native selects keep their intrinsic text width unless explicitly allowed
     to shrink. The head-to-head labels are long enough to push a 320px page
     sideways without this one compact layout. */
  .heat-picker {
    width: 100%;
    margin-inline-start: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0.2rem;
  }
  .heat-picker select { width: 100%; min-width: 0; max-width: 100%; }

  /* Weeks are another navigation rail, not four rows of tiny buttons. The
     client brings the current/deep-linked week into view on every change. */
  .week-switch {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .week-switch::-webkit-scrollbar { display: none; }
  .week-switch a { flex: 0 0 auto; scroll-snap-align: center; }

  /* The disclosure belongs in the top-right cell rather than on a stray row
     beneath each matchup. Its visible mark stays small; its button does not. */
  .game {
    grid-template-columns: minmax(0, 1fr) auto 2.75rem;
    grid-template-areas:
      'when mine open'
      'away away away'
      'home home home';
  }
  .game-open {
    grid-area: open;
    width: 2.75rem;
    height: 2.75rem;
    align-self: center;
  }

  /* In normal flow the page-level control can grow to a thumb-sized target
     without sitting over the first line of the explanatory copy. */
  .detail-toggle {
    position: static;
    margin-top: 0.7rem;
  }

  .hero > *, .hero-name { min-width: 0; max-width: 100%; }
  .hero-name { overflow-wrap: anywhere; }
}

/* At 320px the brand, two external shortcuts and theme switch are ten pixels
   wider than the content row. The destinations remain in the footer, where
   these copies appear only while their masthead versions are absent. */
@media (max-width: 359px) {
  .off-site { display: none; }
  .theme-toggle { margin-inline-start: auto; }
  .footer-external { display: inline-flex; }
}

/* A short landscape phone cannot spare half its height for pinned controls.
   Keep all the controls, but let both bars scroll naturally with the page. */
@media (max-height: 500px) {
  .masthead, .viewer-bar { position: relative; top: auto; }
  :root { --sticky-chrome: 0px; }
}

/* Coarse pointers need controls sized for a finger. Dense identity chips use
   a compact 28px floor because they appear in wrapping score rows; primary
   actions and navigation use the full 44px target. */
@media (pointer: coarse) {
  .brand,
  .tabs a,
  .footer-nav a,
  .week-switch a,
  .detail-toggle,
  .rooting-filter,
  .preseason-controls button,
  .scenario-presets button,
  .scenario-game button,
  .share-scenario,
  .pick,
  .back {
    min-height: 44px;
  }

  .theme-toggle, .repo-link { width: 44px; height: 44px; }
  .me-picker select, .tz select, .heat-picker select { min-height: 44px; }
  .week-switch a, .scenario-game button { min-width: 44px; }
  .preseason-side { min-height: 44px; }
  .scenario-drilldown summary {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding-block: 0.75rem;
  }
  .scenario-drilldown a { min-height: 36px; display: inline-flex; align-items: center; }
  a.team-chip, a.owner { min-height: 28px; display: inline-flex; align-items: center; }
  th[data-sort] { padding-block: 0.75rem; }
  .heat {
    min-width: min(
      var(--heat-w, 100%),
      max(520px, var(--heat-touch-w, 520px))
    );
  }

  /* Touch browsers can retain :hover after a tap. Do not leave cards and rows
     visually lifted after the navigation or disclosure has already fired. */
  .row:hover, .forecast-row:hover, a.team-chip:hover, .pick:hover,
  .gameday-card:hover { transform: none; }

  /* Fixed background attachment is both inconsistently supported and costly
     during a flick on iOS; the fixed canvas still carries the field artwork. */
  body { background-attachment: scroll; }
}

/* Keep the visible information dot small while making its hit area match the
   rest of the controls. This does not reflow table headings or prose. */
@media (pointer: coarse) {
  .term > .info::after { inset: -15px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  @view-transition { navigation: none; }
}

@media print {
  .masthead, .viewer-bar, .footer, .grain, .gridiron, .drive { display: none; }
  body { background: none; color: #000; }
  /* Whatever the reveal observer had not got to yet must still print. */
  .will-reveal { opacity: 1; transform: none; }
}

/* --- landing on a section -------------------------------------------
   Every section of the standings page, and every row on the board, is a real
   destination: `/#games`, `/#entry-aunt-carol`. Two sticky bars sit over the
   top of the page on a desktop, so a target scrolled flush to the top would be
   under them; the margin is the height of whatever is pinned, which the
   client measures and the phone breakpoints above reset as each bar stops
   being pinned. `:where()` keeps this at zero specificity so the schedule's
   weeks, which deliberately do not scroll at all, are left alone. */
:where(main [id]) { scroll-margin-top: calc(var(--sticky-chrome) + 0.75rem); }

/* The row a link landed on, lit the way a selected game is on the schedule:
   somebody sent "here's your line", and the page should point at it. */
.row:target {
  --accent: var(--model);
  border-color: var(--model);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--model) 22%, transparent), var(--shadow);
}
.row:target::before { transform: scaleY(1); }

/* The controls beside a page title: Details and Copy link share one corner. */
.page-tools {
  position: absolute;
  right: 0;
  top: 0.45rem;
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.page-tools .detail-toggle { position: static; top: auto; }

.copy-link {
  font-family: var(--mono);
  font-size: 0.64rem;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--chalk-dim);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0.45rem 0.9rem;
  min-height: 32px;
  cursor: pointer;
  white-space: nowrap;
}
.copy-link:hover { color: var(--chalk); border-color: var(--edge); }
.copy-link:focus-visible { outline: 2px solid var(--model); outline-offset: 2px; }
.copy-link.is-copied {
  color: var(--live);
  border-color: color-mix(in srgb, var(--live) 40%, var(--rule));
}
.section-head .copy-link { margin-inline-start: auto; }

/* --- the viewer bar's fold ----------------------------------------------
   On a wide screen the wrapper dissolves into the grid and the button does
   not exist. On a phone, with a client present to open it again, the two
   secondary settings start folded behind the button — the standings were
   arriving a whole screen below a settings panel, and a settings panel is not
   what anyone opened the site for. With scripting off nothing is folded. */
.viewer-more { display: contents; }
.viewer-more-toggle { display: none; }

@media (max-width: 959px) {
  html[data-js='on'] .viewer-more-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.45rem;
    align-self: end;
    min-height: 40px;
    padding: 0.3rem 0.6rem;
    background: var(--surface);
    border: 1px solid var(--rule);
    border-radius: 6px;
    color: var(--chalk-dim);
    cursor: pointer;
  }
  html[data-js='on'] .viewer-more-toggle:hover { border-color: var(--edge); }
  html[data-js='on'] .viewer-more-toggle:focus-visible { outline: 2px solid var(--model); outline-offset: 2px; }
  .viewer-more-mark { font-size: 0.8rem; transition: transform 0.18s; }
  .viewer-bar.is-open .viewer-more-mark { transform: rotate(180deg); }
  html[data-js='on'] .viewer-bar:not(.is-open) .viewer-more { display: none; }
  html[data-js='on'] .viewer-bar.is-open .viewer-more-toggle { color: var(--chalk); border-color: var(--edge); }
  /* Opened, the time zone takes a full row under the updates toggle rather
     than squeezing in beside the fold button's empty column. */
  html[data-js='on'] .viewer-inner .tz { grid-column: 1 / -1; }
}

@media (max-width: 640px) {
  /* In normal flow under the title, like the lone Details button was. */
  .page-tools {
    position: static;
    margin-top: 0.7rem;
    flex-wrap: wrap;
  }
  .page-tools .detail-toggle { margin-top: 0; }
  .section-head .copy-link { margin-inline-start: 0; }
}
