/* NFLScoreUpdater v2
   Built for a portrait-oriented wall display: two columns, eight rows, all 16
   games on one screen with no scrolling. Every density knob is a custom
   property flipped at :root, so changing one restyles the board instantly
   without touching the DOM. */

:root {
  --cols: 2;
  --scale: 1;
  --row-pad: 0.55rem;
  --row-gap: 0.5rem;

  --bg: #0b0f14;
  --card: #151b23;
  --card-live: #18212c;
  --line: #232c38;
  --qtr-line: #44536a;   /* win-prob quarter separators: readable, still a guide */

  /* Chrome -- the top bar and the settings overlay -- sizes itself from this
     rather than from the root font, because the root font *is* the text-size
     setting. Sized from the root, the panel reached 960px wide on a 1080px
     display at 2.5x, and the bar shrank with the slider until the week picker
     and buttons were hard to hit (both reported 2026-09-18). Tied to the
     viewport instead: the controls stay a usable size whatever the board is
     doing, which is the point of them being controls. */
  --ui-font: clamp(14px, 2.1vmin, 24px);
  --text: #e8edf4;
  --dim: #8896a8;
  --accent: #4a9eff;

  --red: #ff3b30;        /* red zone: a persistent state */
  --red-dim: #5c1a16;
  --green: #22c55e;      /* a score: a transient event */
  --green-deep: #15803d; /* flash fill, dark enough to keep text readable */
  --green-glow: rgba(34, 197, 94, 0.45);
  --amber: #f59e0b;      /* turnovers and missed kicks: notable, but not points */

  font-size: calc(16px * var(--scale));
}

/* Team colours for the detail panel's win-probability graph. Signed off by Rich
   2026-09-19; the derivation and the data behind it are in `TEAM_COLORS.md`, and
   `shots/team-colors.png` draws every pair the way the panel draws them.

   Two per team. The **primary** is what a team shows by default. The
   **secondary** is what a *visiting* team falls back to when its primary is too
   close to the home team's -- Rich's rule, 2026-09-19, and it earns its keep:
   over the 319 real matchups, 80 collide on primaries and the fallback separates
   78 of them. Resolved per game in `detail.js`, not here, because only the
   markup knows who is playing.

   Not ESPN's raw hexes -- most are invisible here (LV and PIT publish #000000,
   HOU #021018). Each is a brand hue with lightness and chroma lifted until it
   clears 3:1 against --card, hue never rotated. The 3:1 bar is set by the *line*,
   which takes the colour at full opacity; the 14% fill would survive far less.

   Two hexes are not from ESPN, because ESPN does not publish them: SEA's Wolf
   Grey (Rich asked for the grey; ESPN offers only navy and Action Green) and
   DEN's primary, which is their orange promoted over the navy at his request.

   To change one, change it here -- nothing else defines them. An abbreviation
   with no token falls back to --accent, so an ESPN rename degrades, not breaks.

   Where a team's secondary is a flat grey, that is its black or white: ESPN
   publishes nothing else for it. Ten teams share #666666 for that reason. */
:root {
  --tc-ARI: #c02d3d;   --tc-ARI-2: #c28903;   /* Arizona Cardinals */
  --tc-ATL: #bd3341;   --tc-ATL-2: #666666;   /* Atlanta Falcons */
  --tc-BAL: #625aba;   --tc-BAL-2: #9a7611;   /* Baltimore Ravens */
  --tc-BUF: #3164c2;   --tc-BUF-2: #d50a0a;   /* Buffalo Bills */
  --tc-CAR: #0085ca;   --tc-CAR-2: #666666;   /* Carolina Panthers */
  --tc-CHI: #4667a1;   --tc-CHI-2: #e64100;   /* Chicago Bears */
  --tc-CIN: #fb4f14;   --tc-CIN-2: #666666;   /* Cincinnati Bengals */
  --tc-CLE: #905c20;   --tc-CLE-2: #ff3c00;   /* Cleveland Browns */
  --tc-DAL: #3f69a0;   --tc-DAL-2: #959595;   /* Dallas Cowboys */
  --tc-DEN: #fc4c02;   --tc-DEN-2: #3f69a0;   /* Denver Broncos */
  --tc-DET: #0076b6;   --tc-DET-2: #959595;   /* Detroit Lions */
  --tc-GB: #2b7449;    --tc-GB-2: #c28903;    /* Green Bay Packers */
  --tc-HOU: #eb0028;   --tc-HOU-2: #0080c6;   /* Houston Texans */
  --tc-IND: #3569a7;   --tc-IND-2: #959595;   /* Indianapolis Colts */
  --tc-JAX: #027487;   --tc-JAX-2: #be8c05;   /* Jacksonville Jaguars */
  --tc-KC: #e31837;    --tc-KC-2: #c28903;    /* Kansas City Chiefs */
  --tc-LAC: #0080c6;   --tc-LAC-2: #bb8d09;   /* Los Angeles Chargers */
  --tc-LAR: #2d62c4;   --tc-LAR-2: #b39200;   /* Los Angeles Rams */
  --tc-LV: #959595;    --tc-LV-2: #666666;    /* Las Vegas Raiders */
  --tc-MIA: #008e97;   --tc-MIA-2: #fc4c02;   /* Miami Dolphins */
  --tc-MIN: #7852b2;   --tc-MIN-2: #ba8e06;   /* Minnesota Vikings */
  --tc-NE: #3f69a0;    --tc-NE-2: #ca1533;    /* New England Patriots */
  --tc-NO: #b29047;    --tc-NO-2: #666666;    /* New Orleans Saints */
  --tc-NYG: #3267ae;   --tc-NYG-2: #c9243f;   /* New York Giants */
  --tc-NYJ: #0e7556;   --tc-NYJ-2: #959595;   /* New York Jets */
  --tc-PHI: #037183;   --tc-PHI-2: #868686;   /* Philadelphia Eagles */
  --tc-PIT: #c28903;   --tc-PIT-2: #666666;   /* Pittsburgh Steelers */
  --tc-SEA: #959595;   --tc-SEA-2: #59ae04;   /* Seattle Seahawks */
  --tc-SF: #c32b20;    --tc-SF-2: #b09147;    /* San Francisco 49ers */
  --tc-TB: #c4263c;    --tc-TB-2: #f55c2b;    /* Tampa Bay Buccaneers */
  --tc-TEN: #4495d2;   --tc-TEN-2: #8fbefa;   /* Tennessee Titans */
  --tc-WSH: #9d514c;   --tc-WSH-2: #c28903;   /* Washington Commanders */
}

/* Retuned 2026-09-18: "Compact is now very roomy, and Roomy is unnaturally
   large." Both ends were too big, so the whole ladder moved down rather than
   just narrowing. The old 0.35/0.55/0.9 was set while the settings panel was
   still in the flow, which meant every judgement about density was made against
   a board auto-fit had shrunk to 0.84 to make room for the panel -- so the
   values were tuned against a view nobody actually looks at. */
/* These are now the only thing setting a card's padding. They were not, until
   2026-09-18: auto-fit poured its leftover height into the cards as well, and it
   absorbed exactly what a denser setting gave up -- the three produced card
   padding of 51.6px, 50.3px and 49.7px, indistinguishable, which is why
   "compact" looked as roomy as "roomy". The board no longer stretches, so a
   dense board is simply shorter. */
[data-density="compact"] { --row-pad: 0.18rem; --row-gap: 0.14rem; }
[data-density="normal"]  { --row-pad: 0.38rem; --row-gap: 0.32rem; }
[data-density="roomy"]   { --row-pad: 0.62rem; --row-gap: 0.54rem; }

* { box-sizing: border-box; margin: 0; padding: 0; }

/* Column layout, so the feed can fill the viewport and scroll on its own
   instead of stretching the page. */
body {
  display: flex; flex-direction: column; min-height: 100vh;
  background: var(--bg);
  color: var(--text);
  font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI",
               Inter, Roboto, system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* ------------------------------------------------------------------ top bar */

.bar {
  position: sticky; top: 0; z-index: 20; flex: none;
  display: flex; align-items: center; justify-content: space-between;
  /* Wraps only when it has to, and since the week select stopped hogging width
     (see `.week` below) it no longer has to at phone sizes. Kept as the safety
     net for a narrower screen or a larger `--ui-font`: wrapping keeps every
     control reachable, and the alternative was hiding one. The buttons are not
     optional. */
  flex-wrap: wrap;
  /* `em` off --ui-font from here down, so the controls keep their size when the
     board's text size changes underneath them. */
  font-size: var(--ui-font);
  gap: 0.35em 1em;
  padding: 0.5em 0.75em;
  background: rgba(11, 15, 20, 0.92);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
}
.bar-left, .bar-right { display: flex; align-items: center; gap: 0.6em; }
/* On a phone the bar's contents are wider than the bar: 610px of children in a
   390px viewport, which pushed the refresh and settings buttons clean off the
   right edge. The page does not scroll sideways to reach them, so they were
   simply gone. The left group is the side that gives -- the week select carries
   the intrinsic width of its longest option ("Conference Championship") and has
   no business winning that argument against the controls. */
.bar-left { flex: 0 1 auto; min-width: 0; }
.bar-right { flex: none; }
.bar h1 { font-size: 1.05em; font-weight: 700; letter-spacing: 0.08em; flex: none; }
.week {
  color: var(--dim); font-size: 0.8em; font-variant-numeric: tabular-nums;
  background: var(--bg); border: 1px solid var(--line); border-radius: 5px;
  padding: 0.1em 0.3em; font-family: inherit; max-width: 14em;
  /* Shrinkable, but not to the 24px it collapsed to once it was allowed to:
     the floor keeps "Week 1" legible and lets the select clip the longer
     post-season labels itself. */
  flex: 0 1 auto; min-width: 6em;
}
/* A `<select>` takes the intrinsic width of its *longest* option, so this one
   sat at its full 14em cap -- 157px -- to accommodate "Conference Championship"
   while displaying "Week 2" for most of the year. That surplus is what pushed
   the bar onto a second line on a phone: at 390px the children came to 376px
   against 369px of room, so it needed to give up 8px and had roughly 90 spare.
   9em fits comfortably at 375px and above, and the select truncates the
   post-season labels itself, which the cap above already assumed it would.

   Scoped to the narrow breakpoint so the wall display keeps the full 14em and
   spells those labels out. 560px matches `NARROW` in `settings.js`; it is a
   media query rather than the `data-feed` attribute because the bar has to
   behave the same whether or not the feed is switched on. */
@media (max-width: 560px) { .week { max-width: 9em; } }
.week:hover { color: var(--text); border-color: var(--dim); }

/* Only shown while the board is parked on some other week. */
.livebtn {
  background: none; border: 1px solid var(--accent); border-radius: 5px;
  color: var(--accent); cursor: pointer;
  font: inherit; font-size: 0.7em; padding: 0.1em 0.4em;
}
.livebtn:hover { background: var(--accent); color: #fff; }
.livebtn[hidden] { display: none; }

.weekbusy { color: var(--accent); font-size: 0.7em; animation: pulse 1.2s ease-in-out infinite; }
.weekbusy[hidden] { display: none; }

/* With the idle poll at an hour, how stale the board is stops being obvious --
   so say it. */
.updated {
  color: var(--dim); font-size: 0.72em; font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.conn {
  width: 0.5em; height: 0.5em; border-radius: 50%;
  background: var(--dim); transition: background 0.3s;
}
.conn[data-status="live"]   { background: var(--green); box-shadow: 0 0 6px var(--green); }
.conn[data-status="connecting"] { background: #eab308; animation: pulse 1.2s ease-in-out infinite; }
.conn[data-status="down"]   { background: var(--red); }

.iconbtn {
  background: none; border: 1px solid var(--line); border-radius: 6px;
  color: var(--dim); cursor: pointer; font-size: 0.95em;
  padding: 0.15em 0.45em;
}
.iconbtn:hover { color: var(--text); border-color: var(--dim); }
.iconbtn:disabled { opacity: 0.5; cursor: default; }
#refreshbtn[data-busy="true"] { animation: spin 0.9s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ----------------------------------------------------------------- settings */

/* An overlay on the detail panel's pattern, not a block above the board. In the
   flow it was part of what `fit.js` measures, deliberately -- so with auto-fit
   on, opening it took the board's top from 71px to 753px and the fit answered by
   shrinking the whole display to 0.84 from 1.42, settings included, until it was
   closed again (measured 2026-09-18). Fixed positioning takes it out of the flow
   fit.js measures, which leaves fit.js itself alone. */

.settings[hidden] { display: none; }  /* display:grid below would otherwise beat [hidden] */

.settings {
  position: fixed; inset: 0; z-index: 50;
  display: grid; place-items: center;
  /* `em` from here down, resolving against --ui-font rather than the root.
     That is what keeps the overlay off the text-size slider it contains. */
  font-size: var(--ui-font);
  padding: 1.5em;
  background: rgba(4, 7, 11, 0.72);
  backdrop-filter: blur(3px);
}
.settings-card {
  position: relative;
  /* Narrower than the detail card -- these are one-line controls, and at the
     wall display's scale a 56rem card would be most of the screen. */
  /* `em` here is the card's own 0.8em, so this is ~26 * 18px on the wall. */
  width: min(26em, 100%); max-height: 100%;
  overflow-y: auto;
  display: grid; gap: 0.75em;
  /* Extra at the top so the close button clears the first control's buttons. */
  padding: 1.7em 1.1em 1.1em;
  background: var(--card);
  border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
  font-size: 0.8em;
}
.settings label { display: grid; gap: 0.35em; color: var(--dim); }
.settings label.check { display: flex; align-items: center; gap: 0.45em; }
/* The card is narrower than the page the panel used to span, so two controls
   that never had to shrink now do: the favourites input carries a 270px
   intrinsic width and the density row a 264px min-content, which together made
   the card scroll sideways at 360px (measured 2026-09-18). */
.settings input[type="text"] {
  background: var(--bg); border: 1px solid var(--line); border-radius: 6px;
  color: var(--text); padding: 0.35em 0.5em; font: inherit;
  min-width: 0;
}
.seg { display: flex; flex-wrap: wrap; gap: 0.25em; }
.seg button {
  flex: 1; padding: 0.3em 0.5em; cursor: pointer;
  background: var(--bg); color: var(--dim);
  border: 1px solid var(--line); border-radius: 6px; font: inherit;
}
.seg button[aria-pressed="true"] {
  background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600;
}

/* -------------------------------------------------------------------- board */

/* With the feed in a column of its own, the viewport is a hard ceiling: the
   board is auto-fitted to it and the feed scrolls inside its track. `min-height`
   alone is not enough -- it is a floor, so a long feed simply grew the page
   (observed: 104 entries overflowed the document by 4080px). A definite height
   is what lets the flex child shrink and the feed's `overflow-y` engage.
   Stacked layouts deliberately keep scrolling: 16 games never fit a phone. */
html[data-feed="side"],
html[data-feed="side"] body {
  height: 100vh;
  overflow: hidden;
}

/* The stacked layout needs a *definite* height too, but for a different reason
   and without the clipping. `body { min-height: 100vh }` leaves the height
   indefinite, so the feed's `1fr` track below had nothing to divide and took
   max-content instead -- a 17,108px feed on a 844px phone, measured. A definite
   height gives the track the viewport to resolve against.

   Deliberately no `overflow: hidden` here, unlike the side layout: a full slate
   still overflows this box by ~76px and the page still scrolls, which is the
   behaviour Rich asked to keep (2026-09-20). */
html[data-feed="bottom"],
html[data-feed="bottom"] body {
  height: 100dvh;
}

.layout {
  flex: 1 1 auto;
  min-height: 0;          /* lets the feed scroll rather than stretch the page */
  display: grid;
  gap: var(--row-gap);
  padding: var(--row-gap);
}
[data-feed="none"] .layout   { grid-template-columns: 1fr; }
/* The feed's share is a floor on its track, not a ceiling on the element. It
   used to be `max-height: 30vh` on `.feed`, which could only ever shrink it --
   so a board with nothing much in it left the bottom of the phone empty while
   the feed carried on scrolling inside its 30vh. Measured at 390x844 with the
   element cap: 27px wasted at four tiles, 86px at three, 203px at one. Both
   tracks were `auto`, which in a stretched grid also let the board pane grow
   past its own content, so the waste came in two pieces -- a gap inside the
   pane and a second one under the feed. `minmax(30dvh, 1fr)` fixes both: the
   board track sizes to content, and the feed takes everything left over
   without ever dropping below the 30% Rich asked it to keep.

   `max-content` and not `auto` for the board's track, which is the part that
   took measuring. Both size to content, but `auto` is *flexible*: when the two
   tracks do not fit, the grid shrinks it instead of overflowing. On a full
   16-game slate that quietly cost the board 76px (591 -> 515) and removed the
   page scroll -- the exact behaviour Rich asked to keep. `max-content` is
   inflexible, so the board holds its 70dvh and the page overflows as before.
   The pane's own `max-height` is what stops `max-content` running away.

   `dvh` rather than `vh` because a phone counts its URL bar inside `vh`, so a
   `vh`-based split hides part of the feed behind it. The wall display has no
   URL bar and resolves the two identically. */
[data-feed="bottom"] .layout {
  grid-template-columns: 1fr;
  grid-template-rows: max-content minmax(30dvh, 1fr);
}
/* Bound the board so the two tracks sum to the viewport and the feed is never
   below the fold. Without this the feed keeps its share but is pushed past the
   end of a tall board: on a 390x844 phone against a full 16-game slate it sat
   1824px down, 2.16 screens of scrolling away (measured 2026-09-20).
   `.board-pane` already scrolls, so capping it is enough. A full slate still
   overflows the page by 76px and that is left alone deliberately (Rich,
   2026-09-20) -- this rule is about reclaiming slack, not about fitting. */
[data-feed="bottom"] .board-pane { max-height: 70dvh; }
/* The feed takes the final track; games share the ones before it. */
[data-feed="side"] .layout {
  grid-template-columns: repeat(var(--gamecols), minmax(0, 1fr)) minmax(0, 1fr);
  grid-template-rows: 1fr;
}
[data-feed="side"] .board-pane { grid-column: span var(--gamecols); }
[data-feed="none"] .feed  { display: none; }

/* Auto-fit normally makes the board fit exactly, but the side layout pins the
   page to the viewport with `overflow: hidden` -- so if the scale ever bottoms
   out (a huge slate, a very short window) the board would be clipped with no
   way to reach the rest. This pane is the escape hatch: it scrolls only when
   there is genuinely more board than space. */
.board-pane {
  min-height: 0; min-width: 0;
  overflow-y: auto;
  align-self: stretch;
}

.board {
  display: grid;
  grid-template-columns: repeat(var(--gamecols), minmax(0, 1fr));
  gap: var(--row-gap);
  align-content: start;
  align-self: start;      /* keep the board content-height so auto-fit can measure it */
  min-width: 0;
}

/* Narrow screens are handled in settings.js (layoutOf), which drops to one
   column of games and moves the feed underneath. */

/* ---------------------------------------------------------------------- feed */

.feed {
  align-self: stretch;
  min-height: 0; min-width: 0;
  overflow-y: auto;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.5rem 0.6rem;
}
.feed-title {
  position: sticky; top: 0; z-index: 2;
  display: flex; align-items: center; gap: 0.5em;
  margin: -0.5rem -0.6rem 0.4rem; padding: 0.4rem 0.6rem;
  background: var(--card);
  border-bottom: 1px solid var(--line);
}
.feed-title h2 {
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--dim);
}
/* Sits beside the title and takes the slack, so long matchups still fit. */
#feedgame {
  flex: 1; min-width: 0;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--line); border-radius: 5px;
  font: inherit; font-size: 0.68rem;
  padding: 0.15rem 0.3rem;
}
#feedgame:hover { border-color: var(--dim); }

.fe[hidden] { display: none; }
.feed-list { list-style: none; display: grid; gap: 0.4rem; }

.fe {
  display: grid; gap: 0.1rem;
  padding: 0.35rem 0.5rem;
  border-left: 3px solid var(--line);
  background: var(--bg);
  border-radius: 0 5px 5px 0;
}
.fe[data-kind="score"]       { border-left-color: var(--green); }
.fe[data-kind="turnover"]    { border-left-color: var(--amber); }
.fe[data-kind="missed_kick"] { border-left-color: var(--amber); }
.fe[data-kind="big_play"]    { border-left-color: var(--accent); }
.fe[data-kind="sack"]        { border-left-color: var(--amber); }
.fe[data-kind="failed_2pt"]  { border-left-color: var(--amber); }
.fe[data-kind="nullified"]   { border-left-color: var(--amber); }
.fe[data-kind="fourth_down"] { border-left-color: var(--accent); }

.fe-head {
  display: flex; gap: 0.4em; align-items: baseline;
  font-size: 0.62rem; color: var(--dim);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.fe-game { font-weight: 700; color: var(--text); }
.fe-text { font-size: 0.74rem; font-weight: 600; }
.fe[data-kind="score"] .fe-text       { color: var(--green); }
.fe[data-kind="turnover"] .fe-text,
.fe[data-kind="missed_kick"] .fe-text,
.fe[data-kind="sack"] .fe-text,
.fe[data-kind="failed_2pt"] .fe-text,
.fe[data-kind="nullified"] .fe-text    { color: var(--amber); }
.fe[data-kind="big_play"] .fe-text,
.fe[data-kind="fourth_down"] .fe-text { color: var(--accent); }
.fe-detail { font-size: 0.64rem; color: var(--dim); }
.fe-empty { color: var(--dim); font-size: 0.7rem; padding: 0.5rem; }

/* --------------------------------------------------------------------- card */

.game {
  cursor: pointer;
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  /* Team rows span the full width; the bottom row carries the down and distance
     on the left and the clock on the right. */
  grid-template-areas:
    "teams teams"
    "sit   status";
  gap: 0.1rem 0.9rem;
  align-items: center;
  padding: var(--row-pad) var(--row-pad) var(--row-pad) calc(var(--row-pad) + 4px);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
}

/* Win-probability background. Absolutely positioned, so the card's own content
   is lifted above it rather than the other way round. */
/* Stops short of the running total rather than crossing it. `--wp-right` is the
   distance from the card's right edge to the doubled rule before the score, and
   is measured in JS because that rule sits wherever the team names and quarter
   columns leave it. */
/* Anchored to the two team rows, not to the whole card. An absolutely
   positioned grid child with a `grid-area` takes that area as its containing
   block, so this needs no JS and no extra markup -- and it is what puts y=50 on
   the boundary between the two team names. Spanning the card instead put the
   midline 13.5px low (38% of the svg's height, measured 2026-09-19), because the
   card also carries the down-and-distance row underneath. Harmless while the
   fill grew off the floor; wrong once the fill diverges from the midline, which
   is what Rich noticed. */
.wp {
  position: absolute;
  grid-area: teams;
  top: 0; left: 0; bottom: 0;
  /* An explicit width, not `right` plus `width: auto`: an SVG is a replaced
     element, so `auto` resolves from the viewBox aspect ratio instead of
     stretching between the two offsets -- which sized it to the card's height. */
  width: calc(100% - var(--wp-right, 0px));
  height: 100%;
  z-index: 0;
  pointer-events: none;
}
.wp[hidden] { display: none; }
.wp-area   { fill: var(--accent); opacity: 0.10; stroke: none; }
.wp-line   { fill: none; stroke: var(--accent); stroke-width: 1.2; opacity: 0.5;
             vector-effect: non-scaling-stroke; }
.wp-mid    { stroke: var(--line); stroke-width: 1; opacity: 0.6;
             vector-effect: non-scaling-stroke; stroke-dasharray: 3 3; }

.teams, .status, .sit { position: relative; z-index: 1; }

/* The left accent bar. Neutral by default, red while a drive is genuinely
   inside the 20 -- it never moves, so it reads as a state, not an event. */
.game::before {
  content: ""; position: absolute; inset: 0 auto 0 0; width: 4px;
  background: transparent; transition: background 0.25s;
}
.game[data-state="in"] { background: var(--card-live); }
.game[data-state="in"]::before { background: var(--line); }
.game[data-state="post"] { opacity: 0.72; }
.game[data-redzone="true"]::before { background: var(--red); }

/* Both team rows are cells of ONE grid -- `.team` is `display: contents`, so its
   children become grid items directly. That is what keeps each quarter column
   aligned between the two rows; two separate grids would size independently. */
.teams {
  grid-area: teams;
  display: grid;
  /* `--qtracks` is a token stream set from JS: a repeat() for games with
     quarters, empty for scheduled ones. It cannot be `repeat(var(--q), ...)`
     because `repeat(0, ...)` is invalid CSS, which voids the whole declaration
     and drops every card into a single column. */
  grid-template-columns:
    1.1em                 /* possession */
    auto                  /* abbreviation */
    auto                  /* logo */
    var(--qtracks, )      /* one track per quarter, empty until played */
    1fr                   /* slack; holds the record pre-game */
    auto;                 /* total */
  align-items: baseline;
  column-gap: 0.3rem;
  row-gap: 0.1rem;
  min-width: 0;
}
.team { display: contents; }

.poss { font-size: 0.7em; line-height: 1; opacity: 0; }
.team[data-poss="true"] .poss { opacity: 1; }
.team[data-slot="record"] .poss {
  opacity: 1;
  font-size: 0.6rem; color: var(--dim);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* Team logo, matched to the football glyph beside it. `align-self: center`
   because the row is baseline-aligned and an image has no baseline of its own. */
.logo {
  width: 1em; height: 1em; object-fit: contain;
  align-self: center;
}
.logo[hidden] { display: none; }
.game[data-state="post"] .team[data-loser="true"] .logo { opacity: 0.45; }

.abbr {
  font-weight: 650; font-size: 1.05rem; letter-spacing: 0.02em;
  white-space: nowrap;
}
.rec {
  color: var(--dim); font-size: 0.72rem; font-weight: 400; white-space: nowrap;
}

/* Per-quarter scores, inline between the logo and the total. Separators are
   borders rather than "|" characters so they land on the column edge instead of
   drifting with the digit widths. */
.q {
  font-size: 0.62rem; color: var(--dim);
  text-align: center; font-variant-numeric: tabular-nums;
  padding: 0 0.25em;
}
.q + .q { border-left: 1px solid var(--line); }

/* Levels 2 and 3 of the shed ladder in `fit.js`; level 1 (the quarter tracks)
   has to happen in JS because those tracks are set inline. The abbreviation
   only goes when a logo is actually there to name the team in its place -- a
   team with no logo keeps its letters and clips instead, which is the lesser
   loss. */
html[data-shed="2"] .team:has(.logo:not([hidden])) .abbr,
html[data-shed="3"] .team:has(.logo:not([hidden])) .abbr { display: none; }

/* Pre-game the record sits in the `1fr` slack track and post-game it borrows
   the possession column, so both spellings have to go. */
html[data-shed="3"] .rec,
html[data-shed="3"] .team[data-slot="record"] .poss { display: none; }

.score {
  font-size: 1.35rem; font-weight: 700;
  font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
  transition: color 0.3s;
}
/* The doubled rule is the "||" that sets the running total apart from quarters. */
.game[data-hasq="true"] .score {
  border-left: 3px double var(--line);
  padding-left: 0.45em;
}
.game[data-state="pre"] .score { color: var(--dim); }
.team[data-loser="true"] .abbr,
.team[data-loser="true"] .score { color: var(--dim); }

/* ------------------------------------------------------------------- status */

.status {
  grid-area: status;
  text-align: right;
  font-size: 0.9rem; font-weight: 600; font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.game[data-state="in"] .status { color: var(--accent); }
.game[data-state="pre"] .status { color: var(--text); }

/* Down, distance and yard line, bottom-left. */
.sit {
  grid-area: sit;
  font-size: 0.74rem; color: var(--dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-variant-numeric: tabular-nums;
}
.game[data-redzone="true"] .sit { color: var(--red); font-weight: 600; }

/* ------------------------------------------- a score: transient, so it moves */

.game.scored { animation: scoreflash 8s ease-out; }
.score.bumped { animation: scorebump 8s ease-out; }

@keyframes scoreflash {
  0%   { background: var(--green-deep); box-shadow: 0 0 0 2px var(--green-glow); }
  12%  { background: var(--green-deep); box-shadow: 0 0 24px 3px var(--green-glow); }
  100% { background: var(--card-live); box-shadow: none; }
}

/* Keep every glyph legible against the fill for as long as it is saturated --
   the clock is normally blue, which was unreadable on bright green. */
.game.scored .status,
.game.scored .sit,
.game.scored .abbr,
.game.scored .score { animation: scoretext 8s ease-out; }
@keyframes scoretext {
  0%, 12% { color: #fff; }
  100%    { color: inherit; }
}
@keyframes scorebump {
  0%   { color: #fff; transform: scale(1.35); }
  15%  { color: #fff; transform: scale(1.35); }
  100% { color: inherit; transform: scale(1); }
}
@keyframes pulse { 50% { opacity: 0.35; } }

/* ------------------------------------------------------------- detail panel */

.detail[hidden] { display: none; }   /* `display: grid` below would beat [hidden] */

.detail {
  position: fixed; inset: 0; z-index: 50;
  display: grid; place-items: center;
  padding: 1.5rem;
  background: rgba(4, 7, 11, 0.72);
  backdrop-filter: blur(3px);
}
.detail-card {
  position: relative;
  width: min(56rem, 100%); max-height: 100%;
  overflow-y: auto;
  background: var(--card);
  border: 1px solid var(--line); border-radius: 10px;
  padding: 1rem 1.1rem;
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
}
/* Shared with the settings overlay, which is the same idiom. */
.detail-close,
.settings-close {
  position: absolute; top: 0.5rem; right: 0.6rem;
  background: none; border: none; cursor: pointer;
  color: var(--dim); font-size: 1.5rem; line-height: 1;
}
/* Same glyph, measured in the overlay's own units. The detail panel keeps rem,
   because it is sized from the root like the board it describes. */
.settings-close { top: 0.5em; right: 0.6em; font-size: 1.5em; }
.detail-close:hover,
.settings-close:hover { color: var(--text); }

/* `relative` so the header graph can be placed against it. The graph is an
   overlay rather than a grid cell because it spans both team rows, and each row
   is its own grid -- there is no shared column to put it in. */
.d-head { display: grid; gap: 0.15rem; margin-bottom: 0.7rem; position: relative; }

/* ------------------------------------------------- win probability, header */
.d-hwp { position: absolute; z-index: 1; }
/* The line gets everything above the axis strip; the scorelines get the strip.
   Reserving it is what keeps a label off the line it refers to. */
.d-hwp-plot { position: absolute; left: 0; right: 0; top: 0; bottom: 0.62rem; }
.d-hwp svg {
  position: absolute; inset: 0; width: 100%; height: 100%;
  /* Pointer events go to the wrapper, which is the full hit area -- the line
     itself is a few pixels tall and impossible to hover deliberately. */
  pointer-events: none;
}
/* The fill and the line are each one shape drawn twice, clipped to one half, so
   everything above the midline belongs to the away row and everything below it to
   the home row. `--tc-away` and `--tc-home` are set on the wrapper from the two
   teams' abbreviations; the `--accent` fallback covers an abbreviation with no
   token, so an ESPN rename degrades to today's blue rather than breaking.

   Colour reinforces what position already says rather than carrying identity on
   its own -- which matters, because it cannot carry it. Under deuteranopia
   Steelers gold and Bengals orange are deltaE 0.5 apart, and no assignment of
   ESPN's colours fixes that (measured 2026-09-19 over 319 real matchups). What
   makes that survivable is that each lobe sits inside its own team's row with
   that team's logo, abbreviation and score beside it. */
.d-hwp-area { stroke: none; }
.d-hwp-away { fill: color-mix(in srgb, var(--tc-away, var(--accent)) 14%, transparent); }
.d-hwp-home { fill: color-mix(in srgb, var(--tc-home, var(--accent)) 14%, transparent); }
.d-hwp-path {
  fill: none; stroke-width: 1.4;
  vector-effect: non-scaling-stroke; stroke-linejoin: round;
}
/* The line is split on the same clips as the fill, so it changes colour exactly
   where it crosses 50%. Full opacity, unlike the 14% fill -- which is why the
   palette is built to clear 3:1 rather than merely being tinted. */
.d-hwp-path-away { stroke: var(--tc-away, var(--accent)); }
.d-hwp-path-home { stroke: var(--tc-home, var(--accent)); }
/* The 50% midline and the quarter separators share one gridline geometry, so
   the two read as one set of guides rather than as two kinds of mark. Both are
   `non-scaling-stroke`: the svg is stretched to the gap with
   `preserveAspectRatio: none`, which would otherwise make the vertical lines a
   different thickness from the horizontal one. */
.d-hwp-mid, .d-hwp-qtr {
  stroke-width: 1;
  vector-effect: non-scaling-stroke; stroke-dasharray: 3 3;
}
.d-hwp-mid { stroke: var(--line); }
/* A step brighter than the midline. At `--line` the verticals were there in the
   DOM and effectively invisible on the wall display -- the midline gets away
   with it by running the full width, where a 1px vertical crossing a third of
   the card's height does not. */
.d-hwp-qtr { stroke: var(--qtr-line); }

/* A score change: a dot where it happened on the line, and the scoreline on the
   axis below. The two are positioned independently -- the dot marks the truth,
   the label is allowed to slide along the axis so a cluster stays readable. */
.d-hwp-dot {
  position: absolute; width: 4px; height: 4px; margin: -2px 0 0 -2px;
  border-radius: 50%; background: var(--text); pointer-events: none;
}
.d-hwp-score {
  position: absolute; bottom: 0; transform: translateX(-50%);
  font-size: 0.5rem; font-weight: 700; color: var(--dim);
  font-variant-numeric: tabular-nums; white-space: nowrap;
  pointer-events: none;
}

.d-hwp-cursor {
  position: absolute; top: 0; bottom: 0; width: 1px;
  background: var(--text); opacity: 0.5; pointer-events: none;
}
/* Before the rule below: `display: flex` in a class beats the UA's
   `[hidden] { display: none }`, so the readout sat on the graph as an empty
   pill until the pointer first moved over it. Same trap as `.settings[hidden]`. */
.d-hwp-tip[hidden] { display: none; }
.d-hwp-tip {
  position: absolute; top: -0.1rem; z-index: 2;
  display: flex; gap: 0.4em; align-items: baseline;
  padding: 0.15rem 0.4rem; border-radius: 4px;
  background: var(--bg); border: 1px solid var(--line);
  font-size: 0.6rem; color: var(--dim);
  font-variant-numeric: tabular-nums; white-space: nowrap;
  pointer-events: none;
}
.d-hwp-tip b { color: var(--text); }
.d-hwp-pct { color: var(--accent); }
.d-team {
  display: grid; grid-template-columns: 1.4em auto 1fr auto;
  align-items: center; gap: 0.45rem;
  font-size: 1.3rem; font-weight: 650;
}
.d-team img { width: 1.4em; height: 1.4em; object-fit: contain; }
.d-team .d-score { font-variant-numeric: tabular-nums; font-size: 1.5rem; }
.d-team[data-loser="true"] { color: var(--dim); }
.d-status { color: var(--accent); font-size: 0.85rem; font-weight: 600; }
.game-detail-meta { color: var(--dim); font-size: 0.72rem; }

.d-section { margin-top: 0.9rem; }

/* Story links at the very end of the panel, only for a game a story covers.
   Quiet on purpose: a line of text, not a section with a heading to fold. */
.d-stories {
  margin-top: 1.1rem; padding-top: 0.6rem; border-top: 1px solid var(--line);
  display: grid; gap: 0.35rem;
}
.d-stories a { color: var(--accent); font-size: 0.8rem; text-decoration: none; }
.d-stories a:hover, .d-stories a:focus-visible { text-decoration: underline; }

/* The Stories section at the foot of the settings panel. */
.settings-stories[hidden] { display: none; }
.settings-stories {
  display: grid; gap: 0.5em; margin-top: 0.4em; padding-top: 0.8em;
  border-top: 1px solid var(--line);
}
.settings-stories h3 {
  margin: 0; font-size: 0.8em; letter-spacing: 0.14em; text-transform: uppercase; color: var(--dim);
}
.settings-stories .story-link {
  display: grid; gap: 0.1em; padding: 0.5em 0.6em; border-radius: 6px;
  background: var(--bg); border: 1px solid var(--line); text-decoration: none;
}
.settings-stories .story-link:hover, .settings-stories .story-link:focus-visible { border-color: var(--accent); }
.settings-stories .story-link b { color: var(--text); font-weight: 600; }
.settings-stories .story-link span { color: var(--dim); font-size: 0.9em; }
.d-section > h3 {
  font-size: 0.66rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--dim); margin-bottom: 0.35rem;
  border-bottom: 1px solid var(--line); padding-bottom: 0.2rem;
}

/* Collapsible sections. The heading's text lives in a button, unset so it
   inherits the h3's small caps rather than a button's own font; the chevron
   is the only sign it can be pressed. A folded section keeps its heading. */
.d-toggle {
  all: unset; cursor: pointer; display: flex; align-items: center; gap: 0.45em;
  width: 100%;
}
.d-toggle::before { content: '\25BE'; font-size: 1.1em; letter-spacing: 0; }
.d-collapsed .d-toggle::before { content: '\25B8'; }
.d-toggle:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }
.d-collapsed > :not(h3) { display: none; }
.d-collapsed > h3 { margin-bottom: 0; }

/* Team stats: away | label | home, the label centred between the numbers. */
.d-stats {
  width: 100%; border-collapse: collapse; font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
}
.d-stats th, .d-stats td { padding: 0.15rem 0.45rem; text-align: center; }
.d-stats td { width: 30%; font-weight: 650; }
.d-stats thead th { color: var(--dim); font-size: 0.64rem; font-weight: 600; }
.d-stats tbody th { color: var(--dim); font-size: 0.72rem; font-weight: 500; }
.d-stats tbody tr + tr > * { border-top: 1px solid var(--line); }

/* Win probability: one bar, split at the home team's share. */
.d-wp { display: grid; gap: 0.2rem; }
.d-wp-bar {
  display: flex; height: 0.85rem; border-radius: 4px; overflow: hidden;
  background: var(--line);
}
/* The two teams' own colours, set on `.d-wp` from `graphColours` -- the same
   pair, chosen by the same clash rule, as the header graph's two lobes. The
   `--accent` fallback covers an abbreviation with no token, matching
   `.d-hwp-path-*` above. Full opacity rather than the graph's 14% fill,
   which the palette is already built for. */
.d-wp-away { background: var(--tc-away, var(--accent)); }
.d-wp-home { background: var(--tc-home, var(--accent)); }
.d-wp-labels {
  display: flex; justify-content: space-between;
  font-size: 0.68rem; color: var(--dim); font-variant-numeric: tabular-nums;
}

/* Decisive plays: one line each, tinted in the colour of the side the swing
   favoured. The colours are `graphColours`' pair, set on `.d-dp` exactly as on
   `.d-wp`. A left rule at full strength names the team; the fill is kept faint
   so the play text stays readable on every palette entry. The text truncates
   rather than wraps -- the full play is in the row's tooltip. */
.d-dp { display: grid; gap: 0.2rem; }
.d-dp-row {
  display: grid; grid-template-columns: 2.6em 3.4em 4.6em minmax(0, 1fr);
  gap: 0.5rem; align-items: baseline;
  font-size: 0.78rem; padding: 0.2rem 0.45rem;
  border-left: 3px solid var(--tc-row); border-radius: 2px;
  background: color-mix(in srgb, var(--tc-row) 16%, transparent);
}
.d-dp-away { --tc-row: var(--tc-away, var(--accent)); }
.d-dp-home { --tc-row: var(--tc-home, var(--accent)); }
.d-dp-row b { font-size: 0.72rem; }
.d-dp-row .d-val { font-variant-numeric: tabular-nums; font-weight: 650; }
.d-dp-when { color: var(--dim); font-size: 0.72rem; font-variant-numeric: tabular-nums; }
.d-dp-text { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }

.d-grid { display: grid; grid-template-columns: auto 1fr; gap: 0.2rem 0.8rem; font-size: 0.78rem; }
.d-grid dt { color: var(--dim); }
.d-grid dd { font-variant-numeric: tabular-nums; }

.d-leader-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem 1.4rem; }
.d-leader-cols h4 {
  font-size: 0.72rem; font-weight: 700; color: var(--text);
  padding-bottom: 0.15rem; margin-bottom: 0.15rem;
  border-bottom: 1px solid var(--line);
}
@media (max-width: 560px) { .d-leader-cols { grid-template-columns: 1fr; } }

.d-leader {
  display: grid; grid-template-columns: 3.2em auto 1fr;
  gap: 0.5rem; align-items: baseline;
  font-size: 0.78rem; padding: 0.15rem 0;
}
.d-leader b { color: var(--dim); font-size: 0.64rem; letter-spacing: 0.08em; }
.d-leader .d-val { color: var(--dim); font-variant-numeric: tabular-nums; }

.d-lines { width: 100%; border-collapse: collapse; font-size: 0.78rem; }
.d-lines th, .d-lines td {
  padding: 0.2rem 0.45rem; text-align: right; font-variant-numeric: tabular-nums;
}
.d-lines th { color: var(--dim); font-size: 0.64rem; font-weight: 600; }
.d-lines td:first-child, .d-lines th:first-child { text-align: left; font-weight: 650; }
.d-lines td:last-child { font-weight: 700; }
.d-lines tr + tr td { border-top: 1px solid var(--line); }

.d-events { display: grid; gap: 0.3rem; max-height: 18rem; overflow-y: auto; }
.d-empty { color: var(--dim); font-size: 0.75rem; }

/* ------------------------------------------------------------------- toasts */

.toasts {
  position: fixed; right: 0.75rem; bottom: 0.75rem; z-index: 40;
  display: flex; flex-direction: column; gap: 0.4rem; align-items: flex-end;
  pointer-events: none;
}
.toast {
  background: var(--card);
  /* `--toast` is re-pointed per kind below, so a turnover never wears the
     colour that means points. */
  --toast: var(--green);
  border: 1px solid var(--toast);
  border-left: 4px solid var(--toast);
  border-radius: 8px; padding: 0.5rem 0.75rem;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  animation: toastin 0.3s ease-out;
  max-width: min(26rem, 90vw);
}
.toast b { color: var(--toast); }
.toast[data-kind="turnover"] { --toast: var(--amber); }
.toast[data-kind="missed_kick"] { --toast: var(--amber); }
.toast[data-kind="failed_2pt"] { --toast: var(--amber); }
.toast[data-kind="nullified"] { --toast: var(--amber); }
.toast[data-kind="fourth_down"] { --toast: var(--accent); }
.toast .t-detail {
  display: block; color: var(--dim); font-size: 0.72rem; margin-top: 0.15rem;
}
.toast .t-score {
  display: block; font-size: 0.78rem; font-variant-numeric: tabular-nums;
  margin-top: 0.15rem;
}
@keyframes toastin { from { opacity: 0; transform: translateY(8px); } }

.empty { grid-column: 1 / -1; text-align: center; color: var(--dim); padding: 3rem 1rem; }

@media (prefers-reduced-motion: reduce) {
  *, *::before { animation: none !important; transition: none !important; }
}
