:root {
  --bg: #0b0f14;
  --bg-raised: #131a22;
  --bg-card: #17202b;
  --border: #26323f;
  --text: #e8edf2;
  --text-dim: #8ea0b3;
  --accent: #f2b134;
  --accent-dim: #9c7a2a;
  --flag: #e0432b;
  --flag-glow: rgba(224, 67, 43, 0.35);
  --good: #38b06b;
  --radius: 12px;
  --tap: 48px; /* minimum comfortable touch target -- WCAG target-size AA is 24px, this is deliberately roomier */
  /* Spacing scale -- a 4px base unit, used for page-level and component
     layout spacing (margin/padding/gap) throughout this file, so
     rhythm reads as deliberate rather than a different one-off number
     every time. .game-card and its children are the one deliberate
     exception: that card's height was reduced through direct pixel
     measurement against real overflow (see the comments around
     .game-card, .matchup-line, .status-badge, .flag-btn) -- its 1-9px
     values are load-bearing precision, not inconsistency, and rounding
     them onto this grid would silently undo verified work. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-7: 32px;
  --sp-8: 40px;
  --sp-9: 48px;
  --sp-10: 60px;
  font-size: 16px;
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }
/* min-height, not height: a fixed height: 100% pins html/body to *exactly*
   the viewport, and once content (a tall, zoomed-out timeline; a long
   agenda) grows past that, the fixed-height body becomes its own nested
   scroll container instead of the page/viewport just scrolling normally --
   content past the fold ends up unreachable. min-height still fills the
   screen when content is short, but lets the page grow past it when it
   isn't -- the standard "full-height page that can still grow" pattern. */
html, body {
  margin: 0; padding: 0; min-height: 100%;
  background: var(--bg); color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  overflow-x: hidden;
}
a { color: inherit; }
button { font-family: inherit; cursor: pointer; }

/* ---------- Gate ---------- */
.gate {
  position: fixed; inset: 0; z-index: 100;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle at 50% 20%, #1a2532 0%, #05080b 75%);
  padding: var(--sp-5);
}
.gate-card {
  width: 100%; max-width: 360px;
  background: var(--bg-card); border: 1px solid var(--border);
  border-radius: 16px; padding: var(--sp-7) var(--sp-6); text-align: center;
  box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
.gate-logo {
  font-size: 2.4rem; font-weight: 900; letter-spacing: 2px; color: var(--accent);
}
.gate-tag { color: var(--text-dim); font-size: 0.9rem; margin: var(--sp-2) 0 var(--sp-6); line-height: 1.4; }
.gate-label { font-size: 0.85rem; color: var(--text-dim); margin-bottom: var(--sp-2); }
.gate-question {
  display: block; font-size: 1.4rem; font-weight: 700; margin-bottom: var(--sp-4);
}
#gate-form { display: flex; flex-direction: column; gap: var(--sp-3); }
#gate-answer {
  background: var(--bg-raised); border: 1px solid var(--border); color: var(--text);
  border-radius: 8px; padding: var(--sp-3) var(--sp-4); font-size: 1.1rem; text-align: center;
}
#gate-form button {
  background: var(--accent); color: #1a1305; border: none; border-radius: 8px;
  padding: var(--sp-3) var(--sp-4); font-size: 1rem; font-weight: 700;
}
#gate-form button:active { transform: translateY(1px); }
.gate-error { color: var(--flag); font-size: 0.85rem; margin-top: var(--sp-3); }

/* ---------- Layout ---------- */
.site { min-height: 100%; display: flex; flex-direction: column; }
.topbar {
  display: flex; align-items: center; gap: var(--sp-5);
  padding: var(--sp-3) var(--sp-5); border-bottom: 1px solid var(--border);
  background: var(--bg-raised); position: sticky; top: 0; z-index: 20;
}
.brand { font-weight: 900; font-size: clamp(1.4rem, 2vw, 1.9rem); letter-spacing: 1px; text-decoration: none; }
.brand-dot { color: var(--flag); }
.topnav { display: flex; gap: var(--sp-2); flex: 1; }
.topnav a {
  text-decoration: none; color: var(--text-dim); font-weight: 700; font-size: clamp(1rem, 1.1vw, 1.15rem);
  padding: var(--sp-3) var(--sp-4); border-radius: 10px; border-bottom: 2px solid transparent;
  display: flex; align-items: center; min-height: var(--tap);
}
.topnav a.active { color: var(--text); border-color: var(--accent); background: rgba(242,177,52,0.08); }
.clock { font-variant-numeric: tabular-nums; color: var(--text-dim); font-size: clamp(0.95rem, 1vw, 1.1rem); }

.view { padding: var(--sp-4) 0 var(--sp-10); }
/* "Every view is a slide" -- headline text should fill the room the
   viewport actually gives it, not sit at one fixed size on a 4K monitor
   and a phone alike. Capped well short of what the widest desktop vw
   would otherwise produce so it never overpowers the toolbar/table below
   it. */
.view h1 { padding: 0 var(--sp-5); margin: var(--sp-2) 0 var(--sp-1); font-size: clamp(1.6rem, 3.4vw, 2.6rem); }
.subhead { padding: 0 var(--sp-5); color: var(--text-dim); margin: 0 0 var(--sp-4); font-size: clamp(1rem, 1.5vw, 1.25rem); line-height: 1.5; }

/* ---------- Timeline ---------- */
.timeline-toolbar {
  display: flex; align-items: center; gap: var(--sp-4); padding: 0 var(--sp-5) var(--sp-4); flex-wrap: wrap;
}
.btn-ghost {
  background: transparent; border: 1.5px solid var(--border); color: var(--text);
  border-radius: 24px; padding: var(--sp-2) var(--sp-5); font-size: clamp(0.95rem, 1.05vw, 1.1rem); font-weight: 700;
  min-height: var(--tap); display: inline-flex; align-items: center;
}
.btn-ghost:active { background: var(--bg-raised); }
.legend { color: var(--text-dim); font-size: clamp(0.85rem, 0.95vw, 1rem); display: flex; align-items: center; gap: var(--sp-2); }
.now-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--flag); box-shadow: 0 0 8px var(--flag-glow); display: inline-block; }

/* Zoom: ctrl+scroll/trackpad-pinch on desktop, real two-finger pinch on
   touch (see initWheelZoom()/initPinchZoom() in js/app.js) -- one shared
   gesture model on every device, always anchored on *today's* column so
   it stays fixed on screen while everything scales around it. No button
   UI for this. */

.timeline-scroller {
  overflow-x: auto; overflow-y: hidden; padding: var(--sp-2) 0 var(--sp-4);
  /* Touch drag and the click-drag-pan handler (initDragPan(), js/app.js)
     both set scrollLeft directly, tick by tick -- scroll-behavior:smooth
     here would queue a little animation on top of every one of those and
     make the whole thing feel laggy instead of 1:1 with the gesture.
     "Jump to now" opts into smooth scrolling itself, explicitly, via
     scrollIntoView({behavior:'smooth'}). */
  touch-action: pan-x;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  cursor: grab;
  /* The native scrollbar is redundant chrome, not a control anyone needs
     here -- panning is already fully covered by drag (mouse), touch-
     scroll, and pinch-zoom, plus "Jump to now" and each column's own
     date header for orientation. A default OS scrollbar under a custom,
     gesture-driven canvas like this reads as unfinished, not helpful. */
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* legacy Edge/IE */
}
.timeline-scroller::-webkit-scrollbar { display: none; } /* Chrome/Safari/Edge */
.timeline-scroller.grabbing { cursor: grabbing; user-select: none; }

/* One day = one fixed-width column, laid out left-to-right by plain CSS
   flex -- no manually-computed x positions, no lane-overlap packing.
   Zooming just changes --day-w and the browser reflows every column for
   free; re-rendering only happens when the actual game data changes. */
.timeline-track {
  display: flex; align-items: flex-start; gap: var(--sp-1);
  /* top: 22px, not --sp-5's 20px -- specifically measured clearance for
     the .is-today "NOW" label sitting above its column (see below); off
     the grid on purpose, not an oversight. */
  padding: 22px var(--sp-5) var(--sp-8);
}
/* min-width: 0 overrides flexbox's automatic content-based minimum size --
   .timeline-track is a row-direction flex container, so without this a
   column can never actually shrink below whatever its widest card's own
   content wants, regardless of --day-w or flex-shrink: 0 (neither of
   those override the automatic minimum, only min-width does). At
   zoomMin's default this genuinely bit -- every card stayed stuck around
   ~70-90px wide no matter how low --day-w went, and the @container tier
   meant to shrink that same content never got a chance to fire, because
   the column (and so the card) never actually got small enough to
   trigger it in the first place. */
.day-column {
  flex: 0 0 var(--day-w, 134px); min-width: 0; display: flex; flex-direction: column; gap: var(--sp-2);
  container-type: inline-size; container-name: daycol;
}
/* Without this, cards inside a column stack with zero gap between them --
   .day-column's own gap only separates the header from the card stack as
   a whole, not the cards from *each other*. */
.day-column-cards { display: flex; flex-direction: column; gap: var(--sp-2); }
.day-column-header {
  /* Scales with zoom instead of staying fixed -- see the width cap on
     .game-card above for why: past a point, zooming in was only ever
     making the card bigger, so this is where that "in" motion goes
     once the card's already maxed out. Written in px throughout, not
     mixed with rem -- (var(--day-w) - 16px) is already a px length, so
     multiplying it by a bare unitless factor to get a rem-scale result
     doesn't work (it stays a tiny px value); starting from a px base and
     a px-scale factor keeps the arithmetic honest. 16px/380px are
     zoomMin/zoomMax from data/config.js -- keep the three in sync if
     any changes. */
  font-size: clamp(9px, calc(9px + (var(--day-w, 16px) - 16px) * 0.0577), 30px);
  font-weight: 800; color: var(--text-dim); text-transform: uppercase;
  letter-spacing: 0.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  padding-bottom: 6px; border-bottom: 1.5px solid var(--border);
}
/* "THU, AUG 27" vs "SAT / 8/29" -- see fmtDay()/fmtDayShort()/
   fmtWeekdayShort() in js/app.js, all always rendered, CSS picks which
   is visible. .day-column is the query container here (it's already
   exactly --day-w wide, no padding of its own to account for), not
   .game-card's "gamecard" container -- the header sits beside the
   cards, not inside one. The short form stacks the weekday above the
   date rather than losing it entirely the way a single-line "8/29"
   would -- two short lines fit the same tight columns a one-line
   "THU, AUG 27" never could. */
.day-label-short { display: none; flex-direction: column; line-height: 1.15; }
.day-label-weekday { font-size: 0.85em; opacity: 0.75; }
@container daycol (max-width: 100px) {
  .day-label-full { display: none; }
  .day-label-short { display: flex; }
}
/* "Now" lives here instead of a separately-positioned line -- there's no
   more hour-precision axis to place a line on, so the day itself is the
   marker: highlight today's whole column. Simpler, and nothing to ever
   get mis-anchored. */
.day-column.is-today .day-column-header { color: var(--flag); border-color: var(--flag); }
.day-column.is-today { position: relative; }
.day-column.is-today::before {
  content: 'NOW'; position: absolute; top: -19px; left: 0; font-size: 0.74rem;
  font-weight: 800; color: var(--flag); letter-spacing: 1px;
}

.game-card {
  /* Capped well below zoomMax's 380px column width on purpose -- letting
     the card keep growing with the column made zooming in just an
     ever-bigger card and nothing else. Past this cap, the *column*
     still keeps growing with zoom (see .day-column), but that extra
     room now goes to the date header instead (see .day-column-header's
     own zoom-scaled font-size) -- more room to breathe on the thing
     that actually orients you in time, not a bigger tile for a game
     whose content already fit fine. 210px, not lower -- the @container
     tier below it (178px of *content*-box width -- see that rule for why
     this isn't the same number as the border-box width here) is the
     verified threshold below which a 3-digit score forces the team
     abbreviation to be dropped to avoid clipping; capping under that
     would make the card's own floor permanently hide it, at every zoom
     level, not just the compact end. */
  width: min(100%, 210px);
  aspect-ratio: 1 / 1;
  min-height: 0; /* flex items default to a content-based auto min-height that otherwise overrides aspect-ratio, same class of bug as .day-column's min-width earlier */
  display: flex; flex-direction: column; justify-content: space-between;
  background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 7px 10px; cursor: pointer; transition: transform 0.15s, border-color 0.15s;
  /* Lets the card itself respond to *its own* rendered width via the
     @container rules below -- zooming out shrinks --day-w, and content
     progressively simplifies to match, rather than tiny illegible text. */
  container-type: inline-size; container-name: gamecard;
}
.game-card:hover { transform: translateY(-3px); border-color: var(--accent-dim); }
/* The "jumps out at you" hover/tap pop -- see initCardInflate() in
   js/app.js for the position:fixed promotion this rides on (needed so it
   can grow to 2x without .timeline-scroller's overflow-y:hidden, there
   for the pan gesture, clipping it top/bottom the instant it does).
   Deliberately relies on the *base* .game-card transition (150ms) for the
   snap back rather than declaring its own -- that's the "deflates fast"
   half of the effect; this rule only owns the slower, springy grow. */
.game-card.inflated {
  transform: scale(2);
  transition: transform 0.32s cubic-bezier(.34, 1.56, .64, 1);
  box-shadow: 0 25px 60px rgba(0,0,0,0.55), 0 0 0 1.5px var(--team-glow-hover, var(--team-glow-default, var(--accent)));
  cursor: pointer;
}
.inflate-placeholder { flex-shrink: 0; }
/* Every *other* game the spotlighted team also plays -- past or future,
   anywhere in the timeline -- pulses in that same color while a card (or
   half of one, see initTeamHighlight() in js/app.js) has it spotlighted.
   A literal connecting line to each one would mean drawing across
   columns that are frequently scrolled out of view or not yet in the
   DOM at all -- this is the same "these are tied together" read without
   depending on anything being on-screen to draw a line to. */
.game-card.team-echo {
  border-color: var(--team-glow-hover, var(--accent));
  animation: teamEcho 1.1s ease-in-out infinite;
}
@keyframes teamEcho {
  0%, 100% { box-shadow: 0 0 0 1px var(--team-glow-hover, var(--accent)), 0 0 7px rgba(var(--team-glow-hover-rgb, 242,177,52), 0.35); }
  50% { box-shadow: 0 0 0 1.5px var(--team-glow-hover, var(--accent)), 0 0 16px rgba(var(--team-glow-hover-rgb, 242,177,52), 0.7); }
}
/* The literal bolt connecting the spotlighted card to each on-screen echo
   -- see tickLightning() in js/app.js, which redraws these paths every
   frame (a fresh jitter seed each time), so the flicker comes from the
   path itself changing, not just an opacity animation on top of a static
   one. Fixed, full-viewport, and non-interactive -- purely decorative,
   never allowed to intercept a click meant for the card underneath it. */
/* Below 45 (.game-card:has(.logo-amplify), see below), not above it --
   every bolt's start/end point IS an amplified logo (see tickLightning()
   in js/app.js, anchored to each one's own rect), so a higher z-index
   here drew the bolt cutting visibly across the crest's face at both
   ends instead of passing behind it like a cable plugged into the back
   of it. Still above plain, non-amplified cards (no z-index of their own,
   so any positive value here already wins) -- only the amplified ones it
   actually touches need to sit in front of it. */
#lightning-layer { position: fixed; inset: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 40; overflow: visible; }
#lightning-layer .bolt-core, #lightning-layer .bolt-glow, #lightning-layer .bolt-spark {
  animation: boltFlicker 0.4s ease-in-out infinite alternate;
}
#lightning-layer .bolt-glow { filter: blur(3px); }
#lightning-layer .bolt-spark { filter: blur(1px) drop-shadow(0 0 4px currentColor); }
@keyframes boltFlicker { 0% { opacity: 0.55; } 100% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  #lightning-layer { display: none; }
}
.game-card.is-live {
  border-color: var(--flag);
  animation: liveGlow 2.2s ease-in-out infinite;
}
@keyframes liveGlow {
  0%, 100% { box-shadow: 0 0 0 1px var(--flag-glow), 0 0 10px rgba(224,67,43,0.25); }
  50% { box-shadow: 0 0 0 1px var(--flag-glow), 0 0 22px rgba(224,67,43,0.55); }
}
.game-card .status-badge {
  font-size: 0.66rem; font-weight: 700; color: var(--text-dim); text-transform: uppercase;
  letter-spacing: 0.5px; line-height: 1; margin-bottom: 3px; display: flex; align-items: center; gap: 4px;
}
.game-card.is-live .status-badge { color: var(--flag); }
.game-card.is-live .status-badge::before {
  content: ''; width: 5px; height: 5px; border-radius: 50%; background: var(--flag);
  animation: pulse 1.2s infinite;
}
@keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:0.3;} }

/* heartbeat flash when a live flag lands on this card */
.game-card.pulse { animation: cardPulse 0.7s ease-out; }
@keyframes cardPulse {
  0% { box-shadow: 0 0 0 0 var(--flag-glow); border-color: var(--flag); }
  100% { box-shadow: 0 0 0 10px rgba(224,67,43,0); border-color: var(--border); }
}

/* Away, then home, stacked -- not side by side. That used to be reversed
   (one line saved a whole row of *height*, the main lever when the card
   was a short rectangle with no height to spare) -- now that the card is
   a fixed square (see .game-card's aspect-ratio), a shared row instead
   costs *width*: two logos competing with each other and the abbr/score
   text for the same row means none of it can ever get very big without
   overflowing. Stacked, each team gets the row's *entire* width to
   itself, which is what actually lets the logo grow large enough to
   fill a big square instead of floating small in the middle of one --
   see the @container gamecard tiers below for how big, at every size.
   Only the smallest tier (bare logo circles, no text at all left to
   share a row with) reverts to side by side, where two tiny circles fit
   more naturally than one stacked awkwardly above the other. */
.matchup-line { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; margin-bottom: 4px; }
/* flex-shrink: 0 on the side itself, not just its children -- without it
   the flex algorithm compresses .team-side as a whole to fit its parent
   row, and since its children can't shrink further, the overflow gets
   silently clipped by this overflow: hidden instead of the row just
   widening or wrapping. Verified across the whole zoom range, not
   assumed -- see the swept-width overflow check this was caught by. */
.team-side { display: flex; align-items: center; gap: 4px; min-width: 0; overflow: hidden; flex-shrink: 0; }
/* Shimmer mask for team logos -- a diagonal highlight that sweeps across
   the wrap's own box. Not a true alpha-mask of the crest itself (that
   would need a per-team CSS mask-image wired to each logo's own URL) --
   a rounded clip over the logo's bounding box reads as "shiny badge"
   without that complexity, and works identically for every team's
   artwork. Static everywhere by default (card tiles keep just the ring
   glow, permanently -- a looping shine on a whole grid of them at once
   was too much) -- .shimmer-once (see openGameModal() in js/app.js)
   plays it a single pass instead, only on the side panel/modal's own
   logos, only when that panel actually opens. */
.team-logo-wrap {
  position: relative; display: inline-flex; flex-shrink: 0; overflow: hidden; border-radius: 50%;
  box-shadow: 0 0 0 2px rgba(255,255,255,0.12), 0 3px 10px rgba(0,0,0,0.4);
}
.team-logo-wrap::after {
  content: ''; position: absolute; inset: -40%;
  background: linear-gradient(115deg, transparent 35%, rgba(255,255,255,0.55) 47%, rgba(255,255,255,1) 50%, rgba(255,255,255,0.55) 53%, transparent 65%);
  background-size: 250% 250%; background-position: -150% -150%;
  mix-blend-mode: screen; pointer-events: none;
}
.team-logo-wrap.shimmer-once::after { animation: logoShimmer 1.1s ease-out 1; }
/* The shimmer's resting position sits well outside its own box normally
   (see the comment above .team-logo-wrap for why), which relied on
   .team-logo-wrap's overflow: hidden to actually clip it there --
   .logo-amplify turns that off (needed so the fluorescent plate's own
   glow isn't clipped, see below), which let this ride along unclipped
   too and show up as a stray diagonal streak at 4x size. Not needed on
   an amplified logo either way -- shimmer-once is a modal-only, click-
   triggered flourish, never applied to a card-tile logo in the first
   place. */
.team-logo-wrap.logo-amplify::after { display: none; }
@keyframes logoShimmer {
  0%, 12% { background-position: -150% -150%; }
  70%, 100% { background-position: 150% 150%; }
}
/* The spotlighted team's own crest, specifically -- not its opponent's --
   blown up on every card it's on (see setTeamHighlight() in js/app.js),
   so it visibly outweighs the other logo on the same tile instead of
   just matching it in a different color. :has() lets the *ancestors*
   that would otherwise clip it (team-side for layout, game-card for
   stacking) react without JS having to reach up and toggle them itself. */
/* Centered on *both* axes, not just left-right -- a plain 4x scale grows
   outward from wherever the small original logo happened to sit in its
   own row (top row for the away team, bottom row for home), which reads
   as the whole blow-up skewed toward one edge of the card instead of
   sitting in its middle. Pulling it out of the stacked-row flow entirely
   (position: absolute, centered on .game-card -- see :has(.logo-amplify)
   below, which is what actually makes the card a positioning root) and
   re-centering it there fixes both axes the same way regardless of
   which row it started in. */
.team-logo-wrap.logo-amplify {
  position: absolute; top: 50%; left: 50%; z-index: 3; overflow: visible;
  transform: translate(-50%, -50%) scale(4);
  transition: transform 0.28s cubic-bezier(.34, 1.56, .64, 1);
}
/* Most crests are a transparent-background PNG -- at 4x they'd otherwise
   just be a shape floating over the card's own dark background. A solid
   plate in that *specific* logo's own color (--amp-color, set per
   element in setTeamHighlight() -- an echo card's is its opponent's
   color, not the spotlighted team's, see the comment there) sits behind
   it so the crest actually pops instead of blending in, and flickers
   hard rather than smoothly pulsing -- "frantic," not a gentle breathe. */
.team-logo-wrap.logo-amplify::before {
  content: ''; position: absolute; inset: 0; border-radius: 50%;
  background: var(--amp-color, var(--accent));
  animation: franticGlow 0.22s steps(2) infinite;
}
@keyframes franticGlow {
  0%, 100% { filter: brightness(1) saturate(1.3); box-shadow: 0 0 10px 3px rgba(var(--amp-color-rgb, 242,177,52), 0.85); }
  50% { filter: brightness(1.45) saturate(1.7); box-shadow: 0 0 18px 7px rgba(var(--amp-color-rgb, 242,177,52), 1); }
}
.team-side:has(.logo-amplify) { overflow: visible; }
.game-card:has(.logo-amplify) { z-index: 45; position: relative; }
/* Deliberately oversized relative to the row that holds them -- see
   .flag-btn's comment above this block for how tight that row's own
   budget already is; the 244px @container tier below is what keeps a
   logo this size from crowding the abbreviation text out at low zoom.
   cqw-scaled, not a flat number -- every size tier below gives this its
   own clamp() instead of a fixed px, so the logo actually grows and
   shrinks continuously with the card's own real size within a tier
   instead of jumping between a handful of fixed sizes and sitting flat
   (and small) across whatever range it lands in. */
.team-side .team-logo-wrap { width: clamp(19px, 30cqw, 60px); height: clamp(19px, 30cqw, 60px); }
/* position: relative (with no offsets, so it doesn't actually move) is
   the fix, not decoration -- a static-positioned img and an absolutely-
   positioned ::before are in different stacking layers regardless of DOM
   order, and the positioned one always wins. Without this the .logo-
   amplify plate (an absolutely-positioned ::before, see below) painted
   *over* the crest instead of behind it, no matter which came first in
   the markup. */
.team-side .team-logo-wrap img { width: 100%; height: 100%; object-fit: contain; flex-shrink: 0; position: relative; }
.team-abbr { font-size: 0.8rem; font-weight: 700; white-space: nowrap; flex-shrink: 0; }
/* Back in the row, next to the logo -- not overlapping it (an absolute
   corner/side badge was tried and dropped: making it small enough to
   never spill past the card's own edge at the tightest zoom tiers also
   made it too small to actually read at the *default* landing zoom,
   which defeats the point of showing a score at all). A small pill, not
   plain text, still gives it a clear boundary so it doesn't read as part
   of the logo -- legible over "perfectly optically centered" is the
   right trade here, a real gap next to a big logo reads fine, an
   unreadable badge crammed into a corner doesn't. */
.team-score {
  font-variant-numeric: tabular-nums; font-weight: 800; color: #fff;
  font-size: clamp(8px, 10cqw, 15px);
  white-space: nowrap; line-height: 1.3; flex-shrink: 0;
  background: rgba(10, 14, 20, 0.82); border: 1px solid rgba(255,255,255,0.14);
  border-radius: 5px; padding: 0 3px;
}
.matchup-at { color: var(--text-dim); font-size: 0.66rem; font-weight: 700; flex-shrink: 0; }

/* Venue and broadcasts only appear in the modal (see openGameModal() in
   js/app.js) -- .game-meta/.broadcast-row/.badge as *card* elements are
   gone; the classes stay for the modal's own use. */
/* One line: the LIVE pill (live games only, see openGameModal() in
   js/app.js) followed by the kickoff clock/venue text -- both are
   "what's happening with this game right now" status, so they share
   the line instead of the pill getting a whole row of its own. */
.game-meta { display: flex; align-items: center; gap: var(--sp-2); font-size: clamp(0.78rem, 3.4cqw, 0.95rem); color: var(--text-dim); margin: var(--sp-2) 0; }
.broadcast-row { display: flex; gap: var(--sp-1); flex-wrap: nowrap; overflow: hidden; margin: var(--sp-1) 0; }
.badge { white-space: nowrap; flex-shrink: 0; }
.badge { font-size: clamp(0.78rem, 3cqw, 0.9rem); font-weight: 700; padding: 3px var(--sp-2); border-radius: 10px; background: var(--bg-raised); color: var(--text-dim); border: 1px solid var(--border); }

/* Betting odds (see appendOddsSection() in js/app.js, fed by
   normalizeOdds() in server/espn.js). Two presentations of the same
   data, toggled by peek vs. the full dialog exactly like every other
   piece of modal content -- .odds-line is the compact "who's favored,
   what's the total" summary the peek panel has room for, .odds-table is
   the fuller away/home x spread/total/moneyline breakdown only the full
   dialog gets. Hidden by default, not built differently -- see the
   .modal.peek-mode overrides below for which one actually shows. */
.odds-line { display: none; font-size: 0.8rem; font-weight: 700; color: var(--text); margin: 0 0 2px; }
.modal.peek-mode .odds-line { display: block; }
.odds-table { width: 100%; border-collapse: collapse; margin: var(--sp-2) 0; font-size: 0.82rem; }
.odds-table th, .odds-table td { padding: 4px var(--sp-2); text-align: right; font-weight: 700; }
.odds-table th:first-child, .odds-table td:first-child { text-align: left; }
.odds-table thead th { color: var(--text-dim); font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 700; padding-bottom: 2px; }
.odds-table tbody tr { border-top: 1px solid var(--border); }
/* The favored side's own abbreviation, not the whole row -- picking out
   just the one fact ("this team is favored") reads clearly without
   tinting every number in the row, which just looked noisy. */
.odds-table tbody tr.odds-fav th { color: var(--accent); }
.odds-price { display: block; font-size: 0.66rem; font-weight: 600; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.odds-provider { font-size: 0.66rem; color: var(--text-dim); margin: 0; text-align: right; }
.modal.peek-mode .odds-table,
.modal.peek-mode .odds-provider {
  display: none;
}

/* Zoom-driven content density: at low zoom (--day-w shrinking toward its
   125px floor as more days come into view) the full card stops fitting --
   rather than letting text truncate into mush, each card drops the least
   essential content (team abbreviations, the "@") based on *its own*
   rendered width, via a container query, not the viewport's. Re-verified
   (not just scaled by eye) against the smaller card overhaul below --
   worst-case 3-letter abbreviations next to a 3-digit score need 174px of
   *content*-box width at this sizing. A @container max-width is measured
   against the container's content box, not its border box -- .game-card's
   20px horizontal padding + 2px border are NOT part of what this number
   compares against, so this threshold is ~22px lower than the equivalent
   border-box card width would be (that mismatch is exactly what made an
   earlier version of this number never actually reach the "show" tier,
   even at the 210px cap -- verified against the real computed display,
   not just assumed from the numbers on paper). */
/* The other direction -- a square this big, still holding the same
   fixed-size text and 18px logos as a much smaller card, reads as mostly
   empty instead of spacious. Scale the actual matchup content up so a
   large square feels intentional, not sparse. */
/* min-width: 178px, not the 195px an earlier version of this used --
   .game-card's own max border-box width is capped at 210px (see its own
   rule above), and this query -- like the max-width one below it -- is
   measured against *content*-box width, ~22px less than that border-box
   number. 195px of content-box width needs a ~217px border-box card to
   ever produce, which the 210px cap makes flatly unreachable -- that
   tier was silent dead code the whole time, the same mistake the
   .game-card .flag-btn cascade bug (see the flag-btn rules below) made
   in a different way. 178px content-box is the real ceiling-adjacent
   range this card actually reaches. */
@container gamecard (min-width: 178px) {
  .status-badge { font-size: 0.78rem; }
  .team-side .team-logo-wrap { width: clamp(30px, 24cqw, 55px); height: clamp(30px, 24cqw, 55px); }
}
@container gamecard (max-width: 178px) {
  .team-abbr { display: none; }
  .matchup-at { display: none; }
}
/* The deepest tier -- default zoom now lives here (see zoomMin in
   data/config.js). Logo+score side-by-side stops fitting a *second* team
   next to it in a card this narrow no matter how small the parts get, so
   the two team-sides stack instead of sitting on one row; the status
   text goes entirely (FINAL/LIVE/kickoff time -- the day column's own
   header already carries the date, and hovering or tapping still opens
   the real detail view with all of it). Verified the same way as the
   178px tier above: real scrollWidth vs clientWidth, worst-case 3-digit
   content, across the whole zoom range down to zoomMin. */
/* .game-card's own padding can't reliably be set from inside a query on
   "gamecard" (.game-card's own container) -- that's a genuine circular
   dependency (the container's padding affects its own content-box size,
   which is exactly what the query measures), and browsers quietly don't
   apply it rather than looping. Querying "daycol" (.day-column, the
   parent -- see css/styles.css's .day-column rule) sidesteps that: its
   width doesn't depend on the card's padding at all, just --day-w
   directly, so setting the *card's* padding from a query on its
   *parent's* size is safe. The threshold numbers are reused as-is from
   the equivalent gamecard tier below -- .day-column has no padding/
   border of its own, so its content width already equals --day-w, close
   enough to gamecard's own (day-w minus the card's padding/border) that
   the two tiers still change together for all practical purposes. */
@container daycol (max-width: 120px) {
  .game-card { padding: 3px 4px; }
}
@container gamecard (max-width: 120px) {
  .game-card .status-badge { display: none; }
  /* align-items: center, not flex-start -- a left-aligned row's own logo
     sits well left of the *card's* true center (bolts/glow/spark are all
     anchored to the card's own bounding-rect center, see tickLightning()
     in js/app.js), so a .logo-amplify blow-up -- which grows outward
     from wherever the small original logo actually is, not from the
     card's center -- read as skewed left of its own glow/bolt every
     time. Centering the row puts the logo close enough to the card's
     real center that the two line up. */
  .matchup-line { flex-direction: column; align-items: center; gap: 2px; margin-bottom: 2px; }
  .team-side { gap: 3px; }
  /* The default landing zoom (58px, see pxPerDay in data/config.js) lives
     in this tier -- this is the one most people see most of the time, so
     it's the one that most needs to not read as a small logo lost in a
     mostly-empty square. Stacked, one team per row (see .matchup-line
     above), so there's no second team sharing the row's width the way
     the >120px tier's logo has to budget for -- room enough to actually
     let it be the dominant thing on the card. */
  .team-side .team-logo-wrap { width: clamp(15px, 42cqw, 46px); height: clamp(15px, 42cqw, 46px); }
  /* A faint pill, not the full bordered-pill weight of the ordinary
     button -- but *some* outline, always, not "none." A plain icon+count
     sitting directly on the card background read as inert decoration,
     not a button, especially once it's disabled (a disabled button with
     zero chrome is indistinguishable from a label that just happens to
     look like one) -- this keeps a visible boundary in every state, dim
     enough not to compete with the logos above it, still light enough to
     shrink to. Still the same clickable .flag-btn either way (toggleFlag()
     doesn't care how it's styled); hovering/tapping still pops the card
     to 2x (see initCardInflate() in js/app.js) where the ordinary,
     comfortably-sized button takes back over. */
  .game-card .flag-btn {
    min-height: 0; width: auto;
    border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.05);
    border-radius: 7px; padding: 1px 5px; gap: 2px;
    justify-content: flex-start; color: var(--flag);
    /* .game-card is a column flex container now (see its aspect-ratio
       square rule), where a cross-axis width of auto means "stretch to
       fill" by default, not "shrink to content" the way a block-flow
       button's width: auto would -- align-self opts this one item back
       out of that stretch, centered under the stacked team rows above it
       rather than pinned edge-to-edge. */
    align-self: center;
  }
  .game-card .flag-icon svg { width: 8px; height: 8px; }
  .game-card .flag-btn .flag-count {
    font-size: 0.6rem; padding: 0; background: none; color: inherit; min-width: 0;
  }
}
/* Deeper still -- "see this team's whole season lit up at once on a real
   monitor" needs a lot more than 120px's own floor allows; the score
   number is the one thing left to give up for it (the flag count stays --
   plenty of real tests, and real users, click it right at this zoom).
   Nothing here needs its own overflow margin the way text did: a bare
   logo circle can't overflow. */
@container daycol (max-width: 40px) {
  .game-card { padding: 2px 3px; }
}
@container gamecard (max-width: 40px) {
  .team-score { display: none; }
  .team-side { gap: 0; }
  .team-side .team-logo-wrap { width: clamp(10px, 32cqw, 15px); height: clamp(10px, 32cqw, 15px); }
  .matchup-line { gap: 2px; }
  /* Back to side-by-side, undoing the 120px tier's stack -- that stack
     bought vertical room for a still-visible score under a rectangle's
     free-to-grow height; a square has no such room to spare at this end
     of the zoom range (its height is capped at its own width same as
     everything else here), and two bare logo circles side by side are
     narrow enough not to need the extra row anyway. */
  .matchup-line { flex-direction: row; }
}
@container gamecard (max-width: 30px) {
  .matchup-line { gap: 1px; }
  .team-side .team-logo-wrap { width: clamp(6px, 30cqw, 11px); height: clamp(6px, 30cqw, 11px); }
}
@container gamecard (max-width: 19px) {
  .team-side .team-logo-wrap { width: clamp(4px, 28cqw, 7px); height: clamp(4px, 28cqw, 7px); }
}
/* The last thing left to give up, past even that: the flag button/count
   itself. This tier is well below where the app ever lands by default
   (58px, see pxPerDay in data/config.js) -- it only exists at the far
   end of a deliberate zoom-out, "the whole loaded season as a grid of
   team-logo pairs" territory, where a tap target this size wasn't
   usable to aim at anyway. Hovering/tapping still pops the card to 2x
   (see initCardInflate() in js/app.js) with the real, properly-sized
   button, same as ever -- this only ever hides the *resting* button. */
@container daycol (max-width: 22px) {
  .game-card { padding: 2px; }
}
@container gamecard (max-width: 22px) {
  .game-card .flag-btn { display: none; }
}

/* The bad-call flag button -- the whole focal point of the app. On the
   timeline card specifically, just the drawn flag glyph (see
   FLAG_ICON_SVG in js/app.js, not an emoji) and the live count, no text
   label -- an icon button reads fine on its own there and it's the
   single biggest lever left on how tight the card can get at low zoom.
   The modal's bigger button (.big-flag-btn) isn't space-constrained the
   same way, so it keeps its label -- see the .flag-label override below,
   scoped to .game-card only. */
.flag-btn {
  width: 100%; min-height: var(--tap); border: 1.5px solid var(--flag);
  background: linear-gradient(180deg, rgba(224,67,43,0.16), rgba(224,67,43,0.04));
  color: var(--flag);
  border-radius: 12px; padding: 10px 14px; font-weight: 800; font-size: 0.82rem;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  letter-spacing: 0.8px; text-transform: uppercase;
  transition: background 0.15s, transform 0.1s, border-color 0.15s;
}
/* var(--tap) (48px) is the general touch-target floor, but a card this
   compact can't afford it without ballooning height back up -- one card
   style now has to work for a mouse *and* a thumb (the layout's unified
   across devices), so this splits the difference: noticeably smaller
   than a full touch target, but not the razor-thin 20px a mouse-only
   design could get away with. 26px, not lower -- the same floor the
   145px @container tier below already uses, so shrinking the card
   doesn't quietly shrink the tap target past what was already the
   established compact-mode minimum. */
/* Gated to min-width, not left unconditional -- .game-card .flag-btn and
   the @container gamecard (max-width: 120px) tier's own shrink rules
   above share this exact selector, so an unconditional version here would
   always win the cascade by source order (same specificity, later wins)
   regardless of which container size actually matched, silently making
   that whole shrink tier dead code. Verified by the same worst-case
   overflow sweep as everywhere else in this file -- with the tier
   actually winning below 120px, nothing clips down to zoomMin. */
@container gamecard (min-width: 121px) {
  .game-card .flag-btn { min-height: 26px; padding: 3px 8px; gap: 5px; }
  .game-card .flag-icon svg { width: 11px; height: 11px; }
  .game-card .flag-btn .flag-count { font-size: 0.68rem; padding: 1px 6px; }
}
/* A card with 178px+ of content-box room has plenty of height a flat
   26px button leaves untouched -- growing it along with the logos keeps
   the whole square reading as one intentionally-composed card instead of
   a couple of small fixed-size pieces adrift in a big empty box. */
@container gamecard (min-width: 178px) {
  .game-card .flag-btn { min-height: 34px; padding: 5px 10px; gap: 7px; }
  .game-card .flag-icon svg { width: 15px; height: 15px; }
  .game-card .flag-btn .flag-count { font-size: 0.78rem; padding: 2px 8px; }
}
.game-card .flag-left { gap: 5px; }
.flag-left { display: flex; align-items: center; gap: 9px; min-width: 0; }
.flag-icon { display: flex; flex-shrink: 0; }
.flag-icon svg { display: block; }
.flag-label { line-height: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.game-card .flag-label { display: none; }
.flag-btn:active { transform: translateY(1px); }
.flag-btn:not(:disabled):not(.not-yet):hover {
  background: linear-gradient(180deg, rgba(224,67,43,0.24), rgba(224,67,43,0.08));
  border-color: #ff6a4a;
}
.flag-btn .flag-count {
  background: var(--flag); color: #fff; border-radius: 8px; padding: 1px 8px; line-height: 1;
  font-size: 0.85rem; font-weight: 800; letter-spacing: 0; font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.flag-btn.voted { background: var(--flag); color: #fff; }
.flag-btn.voted .flag-icon svg { fill: currentColor; }
.flag-btn.voted .flag-count { background: rgba(255,255,255,0.22); color: #fff; }
.flag-btn:disabled, .flag-btn.not-yet {
  border-color: var(--border); background: transparent; color: var(--text-dim); cursor: default;
}
.flag-btn:disabled .flag-count, .flag-btn.not-yet .flag-count { background: var(--border); color: var(--text-dim); }
.vote-hint { color: var(--text-dim); font-size: clamp(0.75rem, 3cqw, 0.88rem); margin: var(--sp-2) 0 0; text-align: center; }

/* a genuinely failed vote (network hiccup, unexpected server error) --
   never silent, even briefly */
.flag-btn.denied { animation: voteDenied 0.4s ease-in-out; }
@keyframes voteDenied {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-5px); border-color: #ff6a4a; }
  40% { transform: translateX(5px); }
  60% { transform: translateX(-4px); }
  80% { transform: translateX(4px); }
}

.status-line { padding: 0 var(--sp-5); color: var(--text-dim); font-size: clamp(0.85rem, 1vw, 1rem); }

/* ---------- Referee table ---------- */
#ref-table-wrap { padding: 0 var(--sp-5); overflow-x: auto; }
.ref-table { width: 100%; border-collapse: collapse; min-width: 480px; }
.ref-table th, .ref-table td { text-align: left; padding: var(--sp-4) var(--sp-3); border-bottom: 1px solid var(--border); font-size: clamp(0.95rem, 1.1vw, 1.15rem); }
.ref-table th { color: var(--text-dim); font-weight: 800; font-size: clamp(0.78rem, 0.85vw, 0.9rem); text-transform: uppercase; letter-spacing: 0.5px; cursor: pointer; user-select: none; }
.ref-table th:hover { color: var(--text); }
.ref-table th.sorted { color: var(--accent); }
.ref-table th.sorted::after { content: ' \25BC'; font-size: 0.7rem; }
.ref-table th.sorted.sorted-asc::after { content: ' \25B2'; font-size: 0.7rem; }
.ref-table tr { cursor: pointer; }
.ref-table tr:hover td { background: var(--bg-raised); }
.ref-table td.flags-cell { color: var(--flag); font-weight: 800; }

.back-link { display: inline-block; padding: 0 var(--sp-5); margin-bottom: var(--sp-3); text-decoration: none; color: var(--text-dim); font-size: clamp(0.85rem, 0.95vw, 1rem); }
#ref-detail-games { padding: 0 var(--sp-5); }
.ref-game-row {
  display: flex; justify-content: space-between; align-items: center;
  padding: var(--sp-3) var(--sp-4); background: var(--bg-card); border: 1px solid var(--border);
  border-radius: 8px; margin-bottom: var(--sp-2); font-size: clamp(0.85rem, 1vw, 1.05rem);
}

/* ---------- Modal ---------- */
.modal { position: fixed; inset: 0; z-index: 90; display: flex; align-items: center; justify-content: center; padding: var(--sp-5); }
.modal-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.6); }
.modal-card {
  position: relative; z-index: 1; width: 100%; max-width: 460px; max-height: 86vh; overflow-y: auto;
  background: var(--bg-card); border: 1px solid var(--border); border-radius: 16px; padding: var(--sp-6);
  /* Sized relative to its own box (cqw), not the viewport -- the docked
     peek panel is a fixed 380px regardless of monitor width, so vw units
     would run away on a wide desktop; container units track the panel's
     actual width on every device instead. */
  container-type: inline-size; container-name: modalcard;
}
/* Close sits with whatever other icon control the header has (the
   listen-live button) in one small cluster, not scattered -- see
   #modal-controls in index.html and its build-out in openGameModal()
   (js/app.js). Both buttons share the same circular treatment so they
   read as one deliberate pair, not two unrelated floating icons. */
.modal-controls { position: absolute; top: var(--sp-3); right: var(--sp-3); z-index: 2; display: flex; align-items: center; gap: var(--sp-2); }
.modal-close {
  background: var(--bg-raised); border: 1px solid var(--border); border-radius: 50%;
  width: 40px; height: 40px; color: var(--text-dim); font-size: 1.3rem; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.modal-close:hover { color: var(--text); border-color: var(--accent-dim); background: var(--bg-card); }
/* Hover-triggered "peek" (desktop only -- see initCardPeek() in
   js/app.js): the exact same modal-card content, docked to the bottom
   edge of the screen and sliding up, instead of the centered dialog with
   a backdrop a real click still gets. A peek floating *next to* whichever
   card you're pointing at (an earlier version of this) inevitably ended
   up sitting on top of other cards nearby -- there's nowhere free to put
   a panel next to a tile in a dense grid without covering something.
   Docking it to a fixed spot every time means it never overlaps the grid
   at all, at the cost of the panel no longer being physically next to its
   card -- .peek-active (see setPeekAnchor() in js/app.js) makes up for
   that by outlining the card the open panel actually describes. Docked
   to the *bottom*, not the side, specifically because the whole point of
   this timeline is horizontal density -- a grid built to fit as many day
   columns across the screen as possible shouldn't lose a chunk of that
   same width to every hover, in either dimension: full width ("hotdog",
   not "hamburger") and deliberately thin, not a tall block that just
   happens to also be centered -- it trims a fixed, shallow strip off the
   bottom of the viewport rather than eating into a card column's worth
   of real estate on either axis. Whatever doesn't fit in that strip
   scrolls inside it (.modal-card's own overflow-y, unchanged from the
   centered dialog) rather than growing the strip taller. */
.modal.peek-mode { background: none; pointer-events: none; padding: 0; align-items: flex-end; justify-content: stretch; }
.modal.peek-mode .modal-backdrop { display: none; }
.modal.peek-mode .modal-card {
  /* 12.5vh -- half of the previous 25vh, exactly, per the ask -- with a
     floor and ceiling in real px so "half" doesn't mean "unreadably
     short" on a small laptop screen or "barely thinner than before" on
     a huge external monitor. It's still just a hover preview: what
     doesn't fit scrolls (see the comment above), and a click always
     opens the real, comfortably-sized dialog. */
  pointer-events: auto; width: 100%; max-width: 100%;
  height: clamp(90px, 12.5vh, 160px); max-height: clamp(90px, 12.5vh, 160px);
  border-radius: 0;
  transform: translateY(100%);
  transition: transform 0.24s cubic-bezier(.2,.8,.2,1);
  /* Still scrolls (same overflow-y as the centered dialog) if content
     genuinely doesn't fit this short a strip -- just without a visible
     scrollbar claiming its own sliver of an already-thin panel. Same
     hide-the-chrome-keep-the-gesture approach as .timeline-scroller. */
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* Translucent, not the modal's normal solid --bg-card -- low enough
     alpha that the game grid genuinely reads through behind the panel,
     not just a faint edge tint. The heavier blur+saturate boost is what
     keeps the card's own text crisp against whatever busy tile colors
     are moving underneath it at that opacity. */
  background: rgba(23, 32, 43, 0.68);
  backdrop-filter: blur(24px) saturate(1.5);
  -webkit-backdrop-filter: blur(24px) saturate(1.5);
  box-shadow: 0 -12px 40px rgba(0,0,0,0.5);
}
.modal.peek-mode .modal-card.docked-in { transform: translateY(0); }
.modal.peek-mode .modal-card::-webkit-scrollbar { display: none; }
/* A hover preview, not the full dialog crammed into a strip -- the
   referee assignment and impact-wave sections are exactly the kind of
   deeper detail a click through to the real dialog is *for*, and the
   broadcast row/vote-hint text are nice-to-know rather than essential at
   a glance. Dropping them (rather than shrinking everything to fit) is
   what actually gets the rest of the content -- who's playing, the
   score, the flag button -- to fit in the panel's own height without
   scrolling, which a hover preview should never need. */
.modal.peek-mode .broadcast-row,
.modal.peek-mode .vote-hint,
.modal.peek-mode .modal-section {
  display: none;
}
.modal.peek-mode .modal-card {
  padding: 1px var(--sp-6);
  display: flex; flex-direction: column; justify-content: center;
}
.modal.peek-mode .modal-title-row { margin-bottom: 2px; font-size: 1rem; gap: var(--sp-1) var(--sp-2); }
.modal.peek-mode .modal-title-row .team-logo-wrap { width: 28px; height: 28px; }
.modal.peek-mode .game-meta { margin: 0 0 1px; font-size: 0.72rem; }
.modal.peek-mode .odds-line { margin: 0 0 2px; font-size: 0.72rem; }
/* The full dialog's flag button is deliberately oversized -- room to
   comfortably tap, breathing room around the label -- none of which a
   ~130px-tall strip has to spare. Same button, same click handler
   (toggleFlag() doesn't care how it's styled), just sized down to what
   this tier of density actually needs, the same way the timeline card's
   own compact flag-btn tiers already do at low zoom. */
.modal.peek-mode .big-flag-btn {
  padding: 1px var(--sp-3); margin-top: 0; font-size: 0.76rem; min-height: 0;
}
.modal.peek-mode .big-flag-btn .flag-icon svg { width: 13px; height: 13px; }
.modal.peek-mode .big-flag-btn .flag-count { font-size: 0.74rem; padding: 1px var(--sp-2); }
/* The card this open peek is describing -- see setPeekAnchor() in
   js/app.js -- since the panel itself no longer sits next to it. */
.game-card.peek-active {
  border-color: var(--team-glow-hover, var(--team-glow-default, var(--accent)));
  box-shadow: 0 0 0 2px rgba(var(--team-glow-hover-rgb, var(--team-glow-default-rgb, 242,177,52)), 0.35);
}
/* padding-right clears #modal-controls (two 40px circles + gap) parked
   in the corner. Just the matchup here now -- no status pill competing
   for the same line (see .game-meta) -- so this reliably stays one
   line; flex-wrap is only a safety net for an unusually long team name
   at the panel's narrowest. */
.modal-title-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-2); padding-right: 84px;
  /* Capped tighter than the other cqw-scaled modal text, and the gap and
     logo size below trimmed to match -- at the docked panel's fixed
     380px this is the one line that has to hold two team abbreviations
     *and* leave room for #modal-controls in the corner, so there's much
     less slack here than in the rest of the card for text to grow into. */
  font-size: clamp(1.15rem, 5.4cqw, 1.55rem); font-weight: 800;
}
.modal-title-row .team-logo-wrap { width: 52px; height: 52px; }
.modal-title-row .team-logo-wrap img { width: 100%; height: 100%; object-fit: contain; }
/* The 52px logo size above is tuned for the roomier desktop docked panel
   and centered dialog; on a narrow phone (the centered dialog goes full
   viewport-width below 720px, see the media query at the bottom of this
   file) that same size -- plus #modal-controls' reserved corner -- was
   tight enough to push a plain two-abbreviation title back onto a second
   line for ordinary 3-letter team codes, not just unusually long ones.
   Scaled down at the container's own narrow end rather than picking one
   compromise size for every width. Excludes .peek-mode on purpose: the
   docked panel is a fixed 380px on every desktop screen regardless of
   window width (it's not a real mobile context, just numerically similar)
   and was already confirmed to fit at full size -- shrinking it here too
   would just be a quieter version of the same panel for no reason. */
@container modalcard (max-width: 400px) {
  .modal:not(.peek-mode) .modal-title-row { font-size: clamp(0.92rem, 5.4cqw, 1.3rem); padding-right: 68px; gap: var(--sp-1) var(--sp-2); }
  .modal:not(.peek-mode) .modal-title-row .team-logo-wrap { width: 30px; height: 30px; }
  .modal:not(.peek-mode) .modal-controls { gap: var(--sp-1); }
  .modal:not(.peek-mode) .modal-close, .modal:not(.peek-mode) .radio-toggle { width: 30px; height: 30px; font-size: 1.05rem; }
}
.modal-vs { color: var(--text-dim); font-weight: 700; }
.modal-score { font-variant-numeric: tabular-nums; color: var(--text-dim); margin-left: 0.3em; }
.modal-section { margin-top: var(--sp-5); }
.modal-section h3 { font-size: clamp(0.75rem, 3.2cqw, 0.9rem); text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-dim); margin-bottom: var(--sp-2); }
/* Grid, not flex-wrap -- see openGameModal() in js/app.js for why: a full
   9-person crew's labels range from "#4 Wrolstad" to "Replay Assistant
   Kris Raper", and letting them wrap freely packed two short ones on one
   line, then stranded every longer one alone on the next, a visibly
   ragged list. Even columns read as one deliberate list regardless of
   label length; each chip's own text still truncates if a column ever
   ends up too narrow for it rather than forcing the grid wider. */
.ref-chip-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: var(--sp-2); }
/* Two skinny, heavily-truncated columns of "Umpire #22 Brandon Cru…" reads
   worse than one full-width column that actually shows the name -- below
   this the panel isn't wide enough for two 150px+ columns to hold a real
   label anyway. */
@container modalcard (max-width: 400px) {
  .ref-chip-grid { grid-template-columns: 1fr; }
}
.ref-chip {
  display: flex; align-items: center; justify-content: flex-start; gap: var(--sp-1);
  background: var(--bg-raised); border: 1px solid var(--border); border-radius: 20px;
  padding: var(--sp-1) var(--sp-3); font-size: clamp(0.82rem, 3.2cqw, 0.95rem);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
.wave-big { width: 100%; height: 90px; display: block; margin-top: var(--sp-2); }
.big-flag-btn { width: 100%; padding: var(--sp-4); font-size: clamp(1rem, 4.2cqw, 1.15rem); margin-top: var(--sp-2); justify-content: space-between; }
.big-flag-btn .flag-icon svg { width: 22px; height: 22px; }
.big-flag-btn .flag-count { font-size: clamp(0.95rem, 4cqw, 1.1rem); padding: var(--sp-1) var(--sp-3); }

/* ---------- Live modal: the "Hollywood" mode ---------- */
.modal-card.is-live {
  border-color: var(--flag);
  background-image:
    repeating-linear-gradient(0deg, rgba(224,67,43,0.035) 0px, rgba(224,67,43,0.035) 1px, transparent 1px, transparent 3px),
    linear-gradient(var(--bg-card), var(--bg-card));
  animation: modalGlow 2.4s ease-in-out infinite;
}
@keyframes modalGlow {
  0%, 100% { box-shadow: 0 20px 60px rgba(0,0,0,0.5), 0 0 0 1px var(--flag-glow); }
  50% { box-shadow: 0 20px 60px rgba(0,0,0,0.5), 0 0 26px rgba(224,67,43,0.5), 0 0 0 1px var(--flag-glow); }
}
.modal-card.pulse { animation: modalGlow 2.4s ease-in-out infinite, modalFlash 0.6s ease-out; }
@keyframes modalFlash {
  0% { box-shadow: 0 20px 60px rgba(0,0,0,0.5), 0 0 0 3px rgba(255,106,74,0.9), 0 0 60px rgba(255,106,74,0.7); }
  100% { box-shadow: 0 20px 60px rgba(0,0,0,0.5), 0 0 0 1px var(--flag-glow); }
}
.modal-card.is-live .modal-section,
.modal-card.is-live .modal-title-row,
.modal-card.is-live .game-meta,
.modal-card.is-live .big-flag-btn {
  animation: sectionIn 0.45s cubic-bezier(.2,.8,.2,1) backwards;
}
.modal-card.is-live .modal-title-row { animation-delay: 0.02s; }
.modal-card.is-live .game-meta { animation-delay: 0.05s; }
.modal-card.is-live .big-flag-btn { animation-delay: 0.08s; }
.modal-card.is-live .modal-section:nth-of-type(1) { animation-delay: 0.14s; }
.modal-card.is-live .modal-section:nth-of-type(2) { animation-delay: 0.2s; }
@keyframes sectionIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }

.live-chip {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  background: rgba(224,67,43,0.15); color: var(--flag); border: 1px solid var(--flag);
  border-radius: 20px; padding: 3px var(--sp-2) 3px var(--sp-1); font-size: clamp(0.76rem, 2.8cqw, 0.88rem); font-weight: 800; letter-spacing: 1px;
}
.sonar { position: relative; width: 8px; height: 8px; flex-shrink: 0; }
.sonar-ring {
  position: absolute; inset: 0; border-radius: 50%; background: var(--flag);
  animation: sonarPing 1.8s cubic-bezier(0,.5,.5,1) infinite;
}
.sonar-ring:nth-child(2) { animation-delay: 0.5s; }
.sonar-ring:nth-child(3) { animation-delay: 1s; }
@keyframes sonarPing {
  0% { transform: scale(1); opacity: 0.9; }
  100% { transform: scale(3.4); opacity: 0; }
}

/* Opens the game's home-market iHeartRadio station in a new tab (see
   openGameModal() in js/app.js) -- only rendered at all live or on game
   day for a team with a known station, so there's no disabled/broken
   state to style, just present or absent. Sized to match .modal-close
   exactly -- they sit together in #modal-controls as one matched pair. */
.radio-toggle {
  background: var(--bg-raised); border: 1px solid var(--border); border-radius: 50%;
  width: 40px; height: 40px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--text-dim); text-decoration: none;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.radio-toggle:hover { color: var(--text); border-color: var(--accent-dim); background: var(--bg-card); }
.radio-icon { display: flex; }
.radio-icon svg { display: block; }

.count-wrap { position: relative; display: inline-block; }
.shock-ring {
  position: absolute; inset: -6px; border: 2px solid #fff; border-radius: 20px;
  pointer-events: none; animation: shockOut 0.6s ease-out forwards;
}
@keyframes shockOut {
  0% { opacity: 0.9; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.9); }
}

/* Odometer: each digit is its own scrolling strip of 0-9 */
.odometer { display: inline-flex; overflow: hidden; font-variant-numeric: tabular-nums; }
.odo-digit { display: inline-block; height: 1em; width: 0.62em; overflow: hidden; vertical-align: bottom; }
.odo-strip { display: flex; flex-direction: column; transition: transform 0.5s cubic-bezier(.2,.8,.2,1); }
.odo-row { height: 1em; line-height: 1em; display: flex; align-items: center; justify-content: center; }

.pulse-monitor h3 {
  color: var(--flag); font-family: ui-monospace, 'SF Mono', Consolas, 'Courier New', monospace;
  letter-spacing: 1px;
}

/* ---------- Flag button hold charge-up ---------- */
/* Press and hold the real "Bad call" button and it visibly charges:
   amber -> flag red -> white-hot, purely as feel for the gesture. Doesn't
   change what a click does -- see initFlagHoldEffects() in js/app.js. */
.flag-btn.charging {
  border-color: var(--accent);
  animation: flagCharging 0.5s ease-in-out infinite;
}
@keyframes flagCharging {
  0%, 100% { transform: scale(1); box-shadow: 0 0 14px 2px rgba(242,177,52,0.5); }
  50% { transform: scale(1.035); box-shadow: 0 0 22px 6px rgba(242,177,52,0.75); }
}
.flag-btn.hot {
  border-color: var(--flag);
  animation: flagHot 0.3s ease-in-out infinite;
}
@keyframes flagHot {
  0%, 100% { transform: scale(1.03); box-shadow: 0 0 20px 5px rgba(224,67,43,0.65); }
  50% { transform: scale(1.07); box-shadow: 0 0 30px 10px rgba(224,67,43,0.9); }
}
.flag-btn.inferno {
  border-color: #ff6a4a;
  animation: flagInferno 0.15s ease-in-out infinite;
}
@keyframes flagInferno {
  0%, 100% { transform: scale(1.08); box-shadow: 0 0 26px 8px rgba(255,106,74,0.85); }
  50% { transform: scale(1.14); box-shadow: 0 0 42px 16px rgba(255,255,255,0.55); }
}

.hold-fx-overlay { position: fixed; pointer-events: none; z-index: 200; }

@media (prefers-reduced-motion: reduce) {
  .game-card.is-live, .modal-card.is-live, .modal-card.pulse, .sonar-ring,
  .status-badge::before, .modal-card.is-live .modal-section,
  .modal-card.is-live .modal-title-row, .modal-card.is-live .modal-live-row,
  .modal-card.is-live .big-flag-btn,
  .flag-btn.charging, .flag-btn.hot, .flag-btn.inferno, .flag-btn.denied,
  .game-card.team-echo, .team-logo-wrap.logo-amplify::before {
    animation: none !important;
  }
  .modal.peek-mode .modal-card { transition: none !important; }
  .game-card.inflated { transition: none !important; }
}

/* Narrower-viewport follow-through -- the layout itself (day-column strip,
   pinch-zoom, drag/touch-pan) is the same on every device now, this is
   just spacing/sizing for the smaller chrome. */
@media (max-width: 720px) {
  .topbar { padding: var(--sp-2) var(--sp-4); gap: var(--sp-2); }
  .brand { font-size: 1.2rem; }
  .topnav { gap: 2px; }
  .topnav a { padding: var(--sp-2); font-size: 0.92rem; }
  .clock { display: none; } /* reclaim space -- not critical info on a small screen */
  .view h1 { font-size: 1.35rem; }
  .timeline-toolbar, .status-line, .subhead, .view h1,
  #ref-table-wrap, .back-link, #ref-detail-games {
    padding-left: var(--sp-4); padding-right: var(--sp-4);
  }
  .timeline-track { padding-left: var(--sp-4); padding-right: var(--sp-4); }
  /* Bottom sheet instead of a centered dialog -- easier to reach one-handed */
  .modal { padding: 0; align-items: flex-end; }
  .modal-card { max-width: 100%; max-height: 92vh; border-radius: 18px 18px 0 0; }
}
