
:root {
  --bg-page:              #f5f5f2;
  --bg-card:              #ffffff;
  --bg-fab:               #ffffff;
  --bg-out-dot:           #e0e0e0;
  --color--favorite-track: transparent;

  --border-card:          #e5e5e5;
  --border-card-hover:    #ccc;
  --border-out-dot:       #cccccc;
  --border-filter-btn:    #444444;
  --border-bug:           #fff;

  --text-primary:         var(--color--snug-black);
  --text-secondary:       #444444;
  --text-muted:           #888888;
  --text-dim:             #aaaaaa;

  --accent:               #e8c020;
  --out-dot-on-bg:        #c0392b;
  --out-dot-on-border:    #922b21;

  --font--base: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font--jaro: 'Jaro', sans-serif;
  --font--league-gothic: 'Teko', sans-serif;
  --font-size--subheading: 28px;

  --border-radius--card: 10px;
  --border-radius--rounded: 12px;
  --border-radius--circular: 99rem;

  --height--header: 74px;
  --height--header-title-track: 46px;
  --height--header-total: calc(var(--height--header) + env(safe-area-inset-top));

  --rotate--fav-logo: -15deg;

  --duration--spring-bounce: 0.25s;
  --ease--spring-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
  /* A plain decelerating ease (no overshoot/bounce) — the board pill's own
     momentum-flick glide coasting to a stop (see _scrubGlideTo in
     global.js), as opposed to the bounce used when it's confirming it
     landed exactly on the live edge. */
  --ease--momentum: cubic-bezier(0.17, 0.84, 0.44, 1);

  --duration--live-pulse: 1.3s;

  /* MLB replay (see _mlbReplayMatchupHtml/js/mlb.js's MLB_BALL_FLYBY_MS,
     which has to be kept in step with this by hand — JS can't read a CSS
     var without extra plumbing): how long the pitch itself takes to cross
     the diamond, and how long each reveal's own fade-in runs once it lands. */
  --duration--mlb-ball-flyby: 0.35s;
  --duration--mlb-reveal-fade: 0.3s;
  /* A little breathing room between the ball actually landing and the
     call/result text fading in over it — reads as a beat of its own
     rather than text popping in the instant the ball stops. */
  --duration--mlb-text-delay: calc(var(--duration--mlb-ball-flyby) + 0.4s);

  --padding--app-sides: 4vw;
  --padding--left-right-compact-tile: 8px;

  --color--wick: rgba(51, 51, 51, 0.85);
  /* Same hue as --color--wick, fully opaque — the track line reads this
     dark because it sits directly on the page background; a translucent
     border on the pill sits on top of the pill's own glass background
     instead, which washes it out lighter at the same alpha. Used wherever
     the pill's border needs to match the track's visual darkness. */
  --color--wick-solid: rgb(51, 51, 51);
  --color--upcoming-time-text: var(--color--snug-black);
  /* Scoreboard navy — the SNUG logo's own ink color (index.html/appstart.html),
     reused here for every spot that would otherwise be a hard #000. */
  --color--snug-black: #13233f;

  /* Shared "glass pill" look — the date pill (.header-title--date) and
     non-final bugs (.compact-upcoming/.compact-live) both use these. */
  --color--glass-bg: color-mix(in srgb, var(--bg-page) 90%, transparent);
  --color--glass-highlight: linear-gradient(to bottom, rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0) 55%);
  --shadow--glass:
    inset 0 1px 1px rgba(255, 255, 255, 0.7),
    inset 0 -2px 3px rgba(0, 0, 0, 0.08),
    0 3px 8px rgba(0, 0, 0, 0.15);

  --width--game-score-box: 166px;
  --height--game-score-box: 46px;
  --width--upcoming-time: 68px;

  --filter--newspaper: sepia(0.8) grayscale(0.8);
  --filter--newspaper-slight: sepia(0.5) grayscale(0.5) opacity(0.85);
  /* Grayscale mode's actual look: an old Game & Watch LCD — mostly
     grayscale, just a light sepia wash nudged toward green (a small
     hue-rotate, not a strong one) rather than a flat gray. Sepia stays low
     on purpose — any higher and it tans white all the way to a dingy
     cream instead of a clean, pale screen. */
  --filter--lcd-greenscale: grayscale(0.5) sepia(0.08) hue-rotate(15deg) saturate(0.15);

  --padding-top--teko-font: 3px;
  /* The bug's score digits (and the muted blur that stands in for them) want
     1px more than the rest of the app's Teko text. */
  --padding-top--teko-font-bug: 4px;

  /* Divider under the finished games (or under the "N completed games" button
     while they're folded away) — see .finals-toggle and .last-final. */
  --image--wavy-line: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='4'%3E%3Cpath d='M0 2 Q5 0 10 2 Q15 4 20 2' fill='none' stroke='black' stroke-width='1'/%3E%3C/svg%3E");

  --bg-image--nfl:
    linear-gradient(90deg, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.3) 100%),
    linear-gradient(90deg, transparent 10%, rgba(255,255,255,0.3) 10%, rgba(255,255,255,0.3) 30%, transparent 30%),
    linear-gradient(90deg, transparent 10%, rgba(255,255,255,0.3) 10%, rgba(255,255,255,0.3) 30%, transparent 30%),
    linear-gradient(90deg, transparent 10%, rgba(255,255,255,0.3) 10%, rgba(255,255,255,0.3) 30%, transparent 30%),
    linear-gradient(90deg, transparent 10%, rgba(255,255,255,0.3) 10%, rgba(255,255,255,0.3) 30%, transparent 30%),
    linear-gradient(90deg, transparent 10%, rgba(255,255,255,0.3) 10%, rgba(255,255,255,0.3) 30%, transparent 30%),
    linear-gradient(130deg, #2a6b17 0%, #3d8f22 40%, #2e7519 70%, #265e14 100%);
  --bg-size--nfl:     100% 3px, 100% 3px, 100% 3px, 100% 3px, 100% 3px, 100% 3px, 100% 100%;
  --bg-position--nfl: 0 5%, 0 22%, 0 39%, 0 56%, 0 73%, 0 90%, 0 0;
}

::-webkit-scrollbar { display: none; }

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  -webkit-user-drag: none;
  user-drag: none;
  font-variant-numeric: tabular-nums;
  -webkit-tap-highlight-color: transparent;
  scrollbar-width: none;
}

.hidden {
  display: none !important;
}

/* Native <button> defaults to cursor: default in most browsers — force
   pointer everywhere so hovering any button on a laptop/desktop reads
   as clickable, same as a link. */
button {
  cursor: pointer;
}

html {
  overflow-x: hidden;
  overscroll-behavior-x: none;
}

body {
  font-family: var(--font--base);
  background: #ebf2fa;
  color: var(--text-primary);
  min-height: 100vh;
  overflow-anchor: none;
  -webkit-font-smoothing: antialiased;
}

body.light-overscroll {
  background: var(--bg-page);
}

/* Grayscale mode, in two parts that together cover every colored pixel:

   1. .gray-root (in app.html) marks the top-level pieces — header, nav
      buttons, sky/stars backdrop, board, footer, settings, the game detail
      modal — that each get one filter over their whole subtree, which is
      what catches colors that aren't images (the sky gradient, star field,
      team-colored tile backgrounds, the red out dots). These can't be
      replaced by filtering one shared ancestor: any `filter` makes that
      element the containing block for position:fixed descendants, which
      would pull the fixed header/footer/nav buttons/modal out of the
      viewport.

   2. Every <img> and <svg> that ISN'T inside a .gray-root is grayscaled
      directly, so a new image or icon anywhere needs no opt-in at all —
      forgetting to mark a new top-level piece can only ever leave a
      background color behind, never a logo. The :not() keeps it off
      anything already covered by a root: the settings page alone has
      hundreds of team logos, and filtering each one on top of its
      container's filter would just multiply the compositing layers for
      no change in the result. */
body.grayscale-app .gray-root {
  filter: var(--filter--lcd-greenscale);
}

body.grayscale-app :is(img, svg):not(.gray-root *) {
  filter: var(--filter--lcd-greenscale);
}

body.grayscale-app #nav-sky-theme {
  .icon-sun {
    transform: translate(-50%, -50%) translateX(0);
    opacity: 1;
  }

  .icon-moon {
    transform: translate(-50%, -50%) translateX(100%);
    opacity: 0;
  }
}

.app {
  margin: 0 auto;
  margin-top: var(--height--header-total);
  padding: 0 var(--padding--app-sides);
}

#app-favorites {
  background: var(--bg-page);
  min-height: 100vh;
  margin-top: 0;
  padding-top: var(--height--header-total);
  padding-bottom: 20vh;
}

p.instructions {
  font-family: var(--font--base);
  font-size: 16px;
  line-height: 1.4;
  text-align: center;
  text-wrap: balance;

  strong {
    font-family: var(--font--jaro);
    text-transform: uppercase;
  }
}

#top-nav {
  view-transition-name: site-header;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 30;
  pointer-events: none;
  height: var(--height--header-total);
  padding-top: env(safe-area-inset-top);
}

/* Settings (My Teams) is the only view with a light page background behind
   the header — the all-view's header floats over the dark/sky board, which
   doesn't want this wash. */
body.light-overscroll #top-nav {
  background: linear-gradient(to bottom, var(--bg-page) 0%, var(--bg-page) 70%, transparent 100%);
}

/* Plain flex centering for the title row — no calc()/env() math needed to
   find its vertical or horizontal center, unlike the old absolute+transform
   approach. This row's own box excludes the safe-area padding (that lives
   on #top-nav above), so top:50%/left:50% below "just work" too. */
.header-title-row {
  position: relative;
  height: var(--height--header);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--padding--app-sides);
}

/* A dark, inset-shadowed track sitting behind the date title showing
   exactly the range it can be swiped across (the title's own fixed width
   plus the drag's maxTravel of 18px on each side — see
   _initHeaderDateSwipe/HEADER_DATE_DRAG_TRAVEL) — like a slot the pill
   slides within. Both widths are fixed in CSS (the title's text is always
   "Today" or a short weekday/date, so it never needs to grow/shrink to fit
   content) instead of measured — no JS, no math. */
.header-date-track {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: calc(var(--width--game-score-box) + 36px);
  height: var(--height--header-title-track);
  border-radius: var(--border-radius--circular);
  background: rgba(26, 26, 26, 0.1);
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4),
              inset 0 -1px 2px rgba(255, 255, 255, 0.1);
  pointer-events: none;
}

.header-title {
  display: flex;
  align-items: center;
  justify-content: center;
  height: calc(var(--height--header-title-track) + 2px);
  font-size: 28px;
  font-weight: 600;
  letter-spacing: -0.2px;
  font-family: var(--font--league-gothic);
  background-color: var(--color--glass-bg);
  background-image: var(--color--glass-highlight);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  border-radius: var(--border-radius--circular);
  padding: 0 20px;
  padding-top: var(--padding-top--teko-font);
  pointer-events: auto;
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
  white-space: nowrap;
}

.header-title--date {
  font-size: 24px;
  width: var(--width--game-score-box);
  /* Rest position is just the flex-centered default (no transform needed)
     — this only carries the live drag offset from _initHeaderDateSwipe,
     composed in via --drag-x. */
  transform: translate(var(--drag-x, 0px), 0);
  font-weight: 600;
  cursor: grab;
  border: 1px solid rgba(255, 255, 255, 0.85);
  transition: transform var(--duration--spring-bounce) var(--ease--spring-bounce);
  box-shadow: var(--shadow--glass);

  /* A tiny press-down, same spring as the rest of this pill's own
     movement — composed onto the drag offset (scale alone would reset it
     to 0,0) so a press mid-drag doesn't jump the pill back to center. */
  &:active {
    transform: translate(var(--drag-x, 0px), 0) scale(0.95);
  }
}

/* Stand-in for the date pill's own text before any JS has run (first
   paint only — _setHeadingLabel's textContent wipes this the instant
   goToIndex fires). Each dot gets the same pressed-in bevel as the
   track behind it, not just a flat circle. */
.header-title__dots {
  display: inline-flex;
  gap: 5px;
  align-items: center;

  span {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.9);
    box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.35),
                inset 0 -1px 1px rgba(255, 255, 255, 0.5);
    animation: anim-header-dot-pulse 1.1s ease-in-out infinite;

    &:nth-child(2) { animation-delay: 0.15s; }
    &:nth-child(3) { animation-delay: 0.3s; }
  }
}

@keyframes anim-header-dot-pulse {
  0%, 60%, 100% { opacity: 0.35; transform: scale(0.75); }
  30% { opacity: 1; transform: scale(1); }
}

.nav-fab {
  position: fixed;
  left: var(--padding--app-sides);
  top: calc(env(safe-area-inset-top) + var(--height--header) / 2);
  /* Individual `translate` property (not the transform shorthand) so it
     doesn't fight with .nav-btn.active's transform:scale(...) — the two
     compose instead of one overwriting the other. */
  translate: 0 -50%;
  z-index: 30;
}

.nav-fab-right {
  left: auto;
  right: var(--padding--app-sides);
}

/* #nav-settings (star) and #nav-back (chevron) sit in the same spot and
   both stay in the DOM — whichever is hiding slides out to the left,
   whichever is appearing slides in from the left (opposite direction
   from nav-sky-theme's sun/moon). Still using the standalone `translate`
   property (not `transform`) so this composes with .nav-btn.active's
   transform:scale(...) instead of fighting it. */
#nav-settings,
#nav-back {
  transition: translate 0.3s ease, opacity 0.3s ease;
}

#nav-back {
  translate: -100% -50%;
  opacity: 0;
  pointer-events: none;
}

body.light-overscroll #nav-settings {
  translate: -100% -50%;
  opacity: 0;
  pointer-events: none;
}

body.light-overscroll #nav-back {
  translate: 0 -50%;
  opacity: 1;
  pointer-events: auto;
}

#nav-back {
  scale: 1.25;
}

#bottom-nav {
  view-transition-name: site-footer;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 25;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 0 var(--padding--app-sides) calc(16px + env(safe-area-inset-bottom));
  pointer-events: none;
}

/* Same track "glass" groove look as the header date pill, always full
   width — #bottom-nav's own side padding (--padding--app-sides) is what
   gives it the app's usual side margins. Labels (.sport-track-item) center
   as a group within it (see .sport-track-labels) rather than the track
   itself shrinking to fit them. .sport-track-pill is a background
   highlight, positioned in JS (see _positionSportPill) to match wherever
   the active label actually rendered. That slot's own label is what
   visually "magnifies" by scaling up over it. */
/* Pops up with the same overshoot spring the pill snaps/bounces use
   elsewhere (--ease--spring-bounce) — played once via renderCfbSubLevers
   (global.js) only on the hidden -> shown transition, not on every routine
   re-render while it's already up. transform-origin: bottom so it reads as
   growing up out of the main track right below it. */
@keyframes cfb-sub-nav-pop {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
#cfb-sub-track.is-popping {
  animation: cfb-sub-nav-pop var(--duration--spring-bounce) var(--ease--spring-bounce) both;
}

.sport-track {
  position: relative;
  width: 100%;
  height: 44px;
  border-radius: var(--border-radius--circular);
  background: rgba(26, 26, 26, 0.1);
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4),
              inset 0 -1px 2px rgba(255, 255, 255, 0.1);
  overflow: visible;
  pointer-events: auto;
}

/* Slightly taller than .sport-track itself (44px), same way .header-title
   sits a bit bigger than .header-date-track — needs .sport-track's own
   overflow set to visible above so this isn't clipped back down. */
.sport-track-pill {
  position: absolute;
  top: 50%;
  height: 47px;
  transform: translateY(-50%);
  border-radius: var(--border-radius--circular);
  background-color: var(--color--glass-bg);
  background-image: var(--color--glass-highlight);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  box-shadow: var(--shadow--glass);
  pointer-events: none;
  z-index: 1;
  transition: left var(--duration--spring-bounce) var(--ease--spring-bounce),
              width var(--duration--spring-bounce) var(--ease--spring-bounce);
}

.sport-track-labels {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  height: 100%;
}

.sport-track-item {
  flex: 0 0 var(--sport-item-basis, 24px);
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  touch-action: pan-y;
  font-family: var(--font--league-gothic);
  font-size: 20px;
  padding-top: var(--padding-top--teko-font);
  text-transform: uppercase;
  color: var(--color--snug-black);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;

  &.on {
    font-weight: 600;
  }

  &:has(.sport-track-icon) {
    padding-top: 0;
  }

  .sport-track-icon {
    width: 18px;
    height: 18px;
  }

  /* Only the non-selected labels flip — the active one sits on the glass
     pill highlight (always light), so it never needs to. Sampled at the
     footer's own position (see _updateSportLabelContrast in global.js)
     rather than reusing nav-icons-light, since that's sampled up at the
     header and the sky color back there can differ from the footer's. */
  body.sport-labels-light &:not(.on) {
    color: #fff;
  }
}

.nav-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: none;
  background: none;
  color: var(--text-secondary);
  cursor: pointer;
  flex-shrink: 0;
  pointer-events: auto;

  svg { width: 20px; height: 20px; }

  &.active {
    transform: scale(1.25);
  }

  /* nav-sky-theme: moon over the normal colored app, sun once
     grayscale-app is on — mirrors the current look rather than what
     tapping it switches to. Both icons stay in the DOM, stacked in the
     same spot — whichever is hiding slides out to the right, whichever
     is appearing slides in from the right (see body.grayscale-app
     #nav-sky-theme below for the swapped state). */
  .icon-sun,
  .icon-moon {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) translateX(0);
    opacity: 1;
    transition: transform 0.3s ease, opacity 0.3s ease;
  }

  .icon-sun {
    transform: translate(-50%, -50%) translateX(100%);
    opacity: 0;
  }

  /* Flips to white once the sky scrolling behind the header gets dark
     enough — see _updateNavIconContrast in global.js, which toggles this
     on scroll (the fixed header sits over a different part of the
     scrolling sky gradient at any given moment). */
  body.nav-icons-light & {
    color: #fff;
  }
}

#all-view {
  position: relative;
  display: flex;
  flex-direction: column;
  margin-top: calc(-1 * var(--height--header-total));
  min-height: 100vh;
  padding-top: calc(var(--height--header-total) + 16px);
  /* #bottom-nav is fixed now — leave room so the last tile doesn't sit
     underneath it (and enough extra room below for the fade mask's own
     span to fully play out against — see _updateBoardFadeMask). */
  padding-bottom: 50vh;
}

#all-view-sky {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(-1 * var(--padding--app-sides));
  right: calc(-1 * var(--padding--app-sides));
  pointer-events: none;
}

#all-view-stars {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(-1 * var(--padding--app-sides));
  right: calc(-1 * var(--padding--app-sides));
  pointer-events: none;
  background-repeat: repeat;
  background-size: 180px 180px;
}

/* Masked (not an opaque overlay) so the sky gradient behind shows through
   exactly as-is rather than needing a color-matched fade layer on top.
   The actual gradient stops are viewport-relative, not element-relative
   (tiles need to fade right above the fixed footer wherever that lands on
   screen, not just at the very end of the whole scrolling list) — see
   _updateBoardFadeMask in global.js, which keeps this in sync with scroll.
   Left at "no mask" here so there's nothing to flash before JS sets it.

   A mask (like overflow) clips to its element's own border box regardless of
   what the mask image itself does — so with .app's padding creating the
   inset from OUTSIDE #all-board, this box's edge sat right at a tile's own
   edge, and dragging a bug pill even slightly past its tile's own padding
   (to mute a side) got clipped by this mask. Bled out to the true screen
   edges (same trick as #all-view-sky/#all-view-stars) and given that same
   padding back as its own, so the inset — and everything inside it — is
   unchanged, but a drag now has that whole padding's worth of room inside
   this box before it could ever reach the mask's real clip line. */
#all-board {
  width: calc(100% + 2 * var(--padding--app-sides));
  margin-left: calc(-1 * var(--padding--app-sides));
  margin-right: calc(-1 * var(--padding--app-sides));
  padding-left: var(--padding--app-sides);
  padding-right: var(--padding--app-sides);
  -webkit-mask-image: none;
  mask-image: none;
}

/* Shown (via .is-visible, see _showBoardLoading/_hideBoardLoading in
   global.js) on the very first load and while switching dates, waiting on
   games to load. */
.board-loading {
  display: none;
  position: sticky;
  top: calc(var(--height--header-total) + 24px);
  z-index: 5;
  justify-content: center;
  pointer-events: none;

  &.is-visible {
    display: flex;
  }
}

.board-loading-spinner {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 3px solid rgba(255, 255, 255, 0.3);
  border-top-color: #fff;
  animation: anim-board-loading-spin 0.8s linear infinite;
}

@keyframes anim-board-loading-spin {
  to { transform: rotate(360deg); }
}

.zero-state {
  width: 100%;
  padding-top: 24px;
  font-family: var(--font--league-gothic);
  text-transform: uppercase;
  font-size: 24px;
  text-align: center;
  color: #fff;

  /* No games anywhere today (as opposed to just filtered down to none by
     the sport levers) — a bigger, bolder, dead-center-of-viewport
     treatment instead of the small inline caption above, since there's
     nothing else on screen at all for it to sit politely near the top
     of. position:fixed so it stays centered on the actual viewport
     regardless of scroll position/page height. */
  &.zero-state--empty-day {
    position: fixed;
    inset: 0;
    /* Plain text, nothing to tap — stay out of the way of the header's
       own buttons (grayscale toggle, settings) sitting fixed above it,
       which this would otherwise cover end to end. */
    pointer-events: none;
    padding-top: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 57.6px;
    line-height: 0.85;
    letter-spacing: -0.02em;
    font-weight: 500;
    color: var(--text-primary);
  }
}

/* Tap a bug to open its detail (see openGameDetailModal/_swipeEnd in
   global.js — MLB only for now). Same glass-pill language as the bug and
   header-title-date: translucent blur background, inset-only highlight
   shadow (no outer drop shadow), light border. */
.game-detail-overlay {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  flex-direction: column; /* the modal (its only child now); see .game-detail-modal for the toggle */
  align-items: center;
  justify-content: flex-start;
  /* A third of the old 20vh top/bottom margin split, kept as the modal's
     own gap from the top of the screen — the rest of it is gone now that
     the modal runs all the way down to the safe area instead of floating
     with room to spare below it (see .game-detail-modal's own height). */
  padding-top: calc(20vh / 3);
  box-sizing: border-box;
  overscroll-behavior: none;
  background: rgba(0, 0, 0, 0.15);
  /* Full-screen blur, not just the patch directly behind the modal's own
     box (that was .game-detail-modal's own backdrop-filter before) — so
     everything behind the whole scrim softens, not just the modal's shape. */
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease;

  /* Loading: the backdrop and a spinner, no modal yet. Open: the modal slides in
     and the spinner goes. */
  &.loading, &.open {
    opacity: 1;
    pointer-events: auto;
  }

  &.loading .detail-loading { opacity: 1; }

  &.open .game-detail-modal {
    opacity: 1;
    transform: translateY(0);
  }
}

/* The spinner over the dark backdrop while a game's detail loads: centered,
   toward the top, and gone once the modal's in. */
.detail-loading {
  position: absolute;
  top: 22vh;
  left: 50%;
  transform: translateX(-50%);
  opacity: 0;
  transition: opacity 0.15s ease;
  pointer-events: none;
}

.game-detail-modal {
  width: 88vw; /* 6vw of backdrop each side */
  /* Rises 2vh into place (from below) as the overlay fades in (see .open below). */
  opacity: 0;
  transform: translateY(2vh);
  transition: transform 0.25s ease-out, opacity 0.25s ease-out;
  /* A fixed height — the header and the toggle at the bottom stay a constant
     size, and .detail-body (the middle) flexes to fill whatever's left,
     scrolling internally when its content doesn't fit. Same margin below
     (before the safe area) as the overlay's own padding-top above, so the
     modal sits evenly between the two rather than floating off-center or
     running flush to the bottom. */
  height: calc(100vh - 40vh / 3 - env(safe-area-inset-bottom));
  border-radius: 32px;
  /* Top only (the header's own side). 3px: matches the O-Only zones' own
     3px margin (see .detail-header-oonly-zone) and .detail-tabs' own 3px
     inset at the bottom, so the gap reads even all the way around rather
     than tighter on one edge than another. */
  padding-top: 3px;
  /* More transparent than the shared --color--glass-bg (90% opaque) — a
     local override rather than changing that shared token, since this is
     the only spot trying it out. The blur that keeps this reading as
     "glass" rather than just see-through now lives on .game-detail-overlay
     instead (full-screen, not just this box's own shape). */
  background-color: color-mix(in srgb, var(--bg-page) 70%, transparent);
  background-image: var(--color--glass-highlight);
  border: 1px solid rgba(255, 255, 255, 0.85);
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.7),
              inset 0 -2px 3px rgba(0, 0, 0, 0.08);
  overflow: hidden; /* clips to the rounded corners; .detail-body does the scrolling */

  /* Column so .detail-body can take the height left under the header (the
     last play centers itself in what's left below the situation). */
  display: flex;
  flex-direction: column;

  > * { flex-shrink: 0; }
  > .detail-body {
    position: relative; /* so .detail-muted-callout pins to its own box rather than scrolling off with the content */
    flex: 1 1 auto;
    min-height: 0; /* lets it shrink below its content's height, so it scrolls instead of growing the modal */
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    overscroll-behavior: contain; /* don't hand leftover scroll to the board */
  }

  @media (min-width: 700px) {
    width: 50vw;
  }
}

/* The only way to close the modal now — tapping the backdrop deliberately
   doesn't (see global.js's own pointerdown handler on .game-detail-overlay).
   Centered dead on the seam between the two tabs, the same way the
   header's own center badge sits over its two O-Only zones (see
   .detail-header-half) — same glass-pill material (--color--glass-bg/
   --color--glass-highlight/--shadow--glass) as the replay slider's own
   scrubber knob, a very slight bevel rather than a floating button. The
   modal's own bottom edge sits 20vh/3 above the safe area (the same gap
   as the overlay's own padding-top above it — see .game-detail-modal's
   own height, 100vh - 40vh/3 - the safe area, starting 20vh/3 down),
   and the tabs row sits 3px + half its own 68px height above THAT, so
   this is centered 37px above it. Fixed to the viewport rather than a
   child of .game-detail-modal so it stays put regardless of the modal's
   own height (.detail-body's content can vary a lot by sport/tab) and so
   .game-detail-modal's own overflow:hidden never clips it. Pops in a
   beat after the modal itself does (.open below) — not during .loading,
   so it reads as following the modal's own entrance rather than beating
   it there. */
.game-detail-dismiss {
  position: fixed;
  left: 50%;
  top: calc(100vh - 20vh / 3 - env(safe-area-inset-bottom, 0px) - 37px);
  /* translate (not transform) so it's applied after scale and doesn't
     get shrunk along with it — keeps this centered through the animation. */
  translate: -50% -50%;
  scale: 0.6;
  z-index: 41; /* above .game-detail-overlay */
  width: 58px;
  height: 58px;
  display: grid;
  place-items: center;
  border: 1px solid var(--border-out-dot);
  border-radius: 50%;
  background-color: var(--color--glass-bg);
  background-image: var(--color--glass-highlight);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  box-shadow: var(--shadow--glass);
  color: var(--text-primary);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease, scale 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* A separate, ~20%-smaller inner circle with its own top-down inner
   shadow — reads as a recessed knob face set into the outer glass disc. */
.game-detail-dismiss::before {
  content: '';
  position: absolute;
  inset: 10%;
  border-radius: 50%;
  box-shadow: inset 0 2px 9px rgba(0, 0, 0, 0.1);
  pointer-events: none;
}
.game-detail-overlay.open .game-detail-dismiss {
  opacity: 1;
  scale: 1;
  pointer-events: auto;
  transition-delay: 0.15s; /* the modal's own pop-in (.game-detail-modal) runs 0.25s */

  /* Same press-down feel as the board/header pills — instant shrink,
     eased release. */
  &:active {
    transition: scale 0.04s ease;
    scale: 0.95;
  }
}

/* Game detail modal header — its own component, not the compact tile's
   .bug (which carries a lot of tile-specific sizing/overrides that don't
   apply in a big centered modal). See detailHeaderHtml in global.js; each
   sport supplies its own .detail-header-center content (e.g.
   _mlbDetailCenterHtml). Anticipates a linescore, last play, and scoring
   log eventually sitting below this same header, once built. */
.detail-header {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 16px 12px 20px;
  position: relative;
}

/* Favorites this one game (joins the FAVS filter + the board's own
   favorite-game highlight), even when neither team in it is a followed
   favorite — see toggleGameFavorite/isFavoritedByTeam in global.js.
   Static markup (app.html), fixed to the viewport same as
   .game-detail-dismiss and for the same reason — a sibling of
   .game-detail-modal rather than a descendant, so its own
   overflow:hidden never clips this, and rebuilding .detail-head
   underneath (every poll/tab change) never moves or recreates it. `top`
   matches .game-detail-overlay's own padding-top exactly (both
   calc(20vh / 3)) — that's .game-detail-modal's own top edge, and
   centering this badge there (translate -50%) is what straddles it, half
   above/half over the header, with .game-detail-modal's own padding-top
   staying untouched (plain 3px). Same glass material + box-shadow as
   .detail-replay-progress-knob (not its .is-live glow, just
   var(--shadow--glass) — this has its own, simpler active look below). */
/* A team-favorited game hides this entirely instead of showing it locked
   (see _syncDetailFavoriteButton) — spelled out explicitly since the base
   rule's own display:grid below would otherwise out-specificity the
   [hidden] attribute's UA-stylesheet display:none. */
.detail-header-fav[hidden] {
  display: none;
}

.detail-header-fav {
  position: fixed;
  top: calc(20vh / 3);
  left: 50%;
  /* translate (not transform) so it's applied after scale and doesn't get
     shrunk along with it — keeps this centered through the pop-in below,
     same as .game-detail-dismiss. */
  translate: -50% -50%;
  scale: 0.6;
  z-index: 41; /* above .game-detail-overlay, same as .game-detail-dismiss */
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border: none;
  border-radius: 50%;
  background-color: var(--color--glass-bg);
  background-image: var(--color--glass-highlight);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  box-shadow: var(--shadow--glass);
  color: var(--text-primary);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease, scale 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), background-color 0.15s ease, color 0.15s ease;
}

/* Hidden until the overlay's own .loading, same gating as
   .game-detail-dismiss — there's no game to favorite yet while the
   backdrop+spinner are up with no modal in. Pops in a beat after the
   modal itself (.game-detail-modal runs its own 0.25s), same delay as
   the dismiss button, so the two read as arriving together. */
.game-detail-overlay.open .detail-header-fav {
  opacity: 1;
  scale: 1;
  pointer-events: auto;
  transition-delay: 0.15s;

  &:active {
    transition: scale 0.04s ease;
    scale: 0.92;
  }
}

/* Outline only by default — same look as the nav's own star (Settings
   button, goToFavorites), just unfilled until favorited. */
.detail-header-fav svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Favorited (toggled on here — a team-favorited game never reaches this
   state visibly, see .detail-header-fav[hidden] above). The star itself
   fills solid orange; the circle stays the same glass pill either way. */
.detail-header-fav.is-active svg {
  fill: #e8a020;
  stroke: #e8a020;
}

.detail-header-team {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 16%;
  position: relative;
}

.detail-header-logo {
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Nothing of its own to tap — lets a click on it fall through to whatever
     sits beneath (the O-Only zone — see .detail-header-oonly-zone). */
  pointer-events: none;

  img {
    width: 100%;
    height: 100%;
    object-fit: contain;
  }
}

/* In the modal header the logo hangs off the abbreviation's outer side (left of
   the away team, right of the home team), centered on it, out of the flow — so
   the abbreviation stays on the same horizontal axis as the score and badge.
   (The team block is only as tall as the abbreviation, so top:50% is its
   middle.) */
.detail-header-team .detail-header-logo {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 28px;
  height: 28px;
}

.detail-header-team-away .detail-header-logo { right: 100%; margin-right: 3px; }
.detail-header-team-home .detail-header-logo { left: 100%;  margin-left: 3px; }

/* Once a game's begun: the abbreviation sits above the score and the record below it
   (both out of the flow, hanging off the score), so the score is what's
   centered with the badge. The team blocks shrink to just their logo, which
   goes vertically centered out toward the edge. */
.detail-header-begun .detail-header-team { display: contents; }
.detail-header-begun .detail-header-team .detail-header-logo {
  top: calc(50% - 2px); /* the header's padding is 4px heavier below */
  width: 44px; /* same size as an upcoming game's */
  height: 44px;
  margin: 0;
}
.detail-header-begun .detail-header-team-away .detail-header-logo { left: 14px;  right: auto; }
.detail-header-begun .detail-header-team-home .detail-header-logo { right: 14px; left: auto; }

.detail-header-above {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-bottom: -11px;
  display: flex;
  align-items: center;
  gap: 3px;
  line-height: 1;
  white-space: nowrap;

  .detail-header-abbr { font-size: 16px; }

  /* The ranking sits beside the abbreviation, centered on it and out of the
     flow, so the abbreviation stays centered over the score whether or not
     there is one — to the right for home, the left for away (so it's never
     the one nearer the middle of the header for either side). */
  .detail-header-rank {
    position: absolute;
    left: 100%;
    top: 50%; 
    transform: translateY(-50%);
    margin-left: 3px;
    font-size: 13px;
    letter-spacing: -1px;
  }
}

.detail-header-score-away .detail-header-rank {
  left: auto;
  right: 100%;
  margin-left: 0;
  margin-right: 3px;
}

.detail-header-below {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-top: -9px;
  font-family: var(--font--base);
  font-size: 12px;
  line-height: 1;
  font-weight: 400;
  color: var(--text-secondary);
  white-space: nowrap;
}

/* Upcoming games have no scores, so the abbreviations sit tight around the
   start time and there's room to spare: the logos get bigger and go out toward
   the edges instead of hugging the abbreviations (the team block stops being
   their anchor, so they're placed off the header itself). */
.detail-header-upcoming .detail-header-team { position: static; }

/* Nothing but the abbreviations and the start time up top, so the abbreviations
   can be big (still clear of the logos out at the edges). */
.detail-header-upcoming .detail-header-abbr { font-size: 36px; font-weight: 500; }
.detail-header-upcoming .detail-header-team .detail-header-logo {
  width: 44px;
  height: 44px;
  margin: 0;
  top: calc(50% - 2px); /* the header's padding is 4px heavier below */
}
.detail-header-upcoming .detail-header-team-away .detail-header-logo { left: 14px;  right: auto; }
.detail-header-upcoming .detail-header-team-home .detail-header-logo { right: 14px; left: auto; }

.detail-header-abbr {
  font-family: var(--font--league-gothic);
  padding-top: var(--padding-top--teko-font);
  font-size: 24px;
  font-weight: 600;
  text-transform: uppercase;
  color: var(--text-primary);
  white-space: nowrap;
  position: relative;
}


.detail-header-record {
  /* Out of the flow, centered just under the abbreviation (it's a child of the
     abbreviation's row, so top:100% is that row's bottom). Pulled up tight
     against its line box the same way .detail-header-below pulls up against
     the (similarly-sized) score digits above it — same -9px, same 12px text. */
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-top: -9px;
  font-family: var(--font--base);
  font-size: 12px;
  color: var(--text-secondary);
  white-space: nowrap;
}

.detail-header-score {
  font-family: var(--font--league-gothic);
  font-size: 40px;
  font-weight: 600;
  padding-top: var(--padding-top--teko-font);
  color: var(--text-primary);
  /* A fixed slot, two digits wide, with the number centered in it: the
     abbreviation above and the record below center on the slot, so they stay put
     as the score changes width (9 -> 10, or counting up). */
  width: 44px;
  flex-shrink: 0;
  text-align: center;
}

.detail-header-score-value {
  position: relative;
  display: block; /* the whole slot, so what hangs off it centers on the slot */
  /* Nothing here (abbr, digits, record) has a tap of its own — lets a click
     fall through to the O-Only zone underneath (see .detail-header-oonly-zone). */
  pointer-events: none;
}

/* The state marker next to a team's logo, on its inner side and vertically
   centered like the logo: the football for the team with the ball. */
.detail-header-marker {
  position: absolute;
  top: calc(50% - 2px);
  transform: translateY(-50%);
  display: flex;
  color: var(--text-primary);
  line-height: 0;
  pointer-events: none; /* same reasoning as .detail-header-score-value above */

  svg { display: block; }
  .bug-football-icon { width: 20px; height: 13px; color: var(--text-secondary); }
  .bug-baseball-icon { width: 16px; height: 16px; color: var(--text-secondary); }
}

/* The winner's triangle, above the abbreviation and pointing down at it
   (see detailHeaderScoreHtml) — centered over the abbreviation's own box,
   not the wider row it shares with the ranking. */
.detail-header-winner {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-bottom: 3px;
  width: 10px;
  height: 8px;
  color: var(--text-primary);
}

.detail-header-begun .detail-header-team-away .detail-header-marker { left: 64px; }  /* 14 + 44 logo + 6 */
.detail-header-begun .detail-header-team-home .detail-header-marker { right: 64px; }

/* The football itself sits above the logo instead of beside it, centered on
   the same 44px box the logo occupies, and a bit bigger since there's room
   above. The winner's triangle is unaffected — it keeps the side placement. */
.detail-header-marker:has(.bug-football-icon) {
  top: calc(50% - 41px); /* logo's own top edge (50% - 2px - 22px) plus a 1px gap and this icon's height */
  transform: none;
  width: 44px;
  justify-content: center;

  .bug-football-icon { width: 24px; height: 16px; }
}

.detail-header-begun .detail-header-team-away .detail-header-marker:has(.bug-football-icon) { left: 14px; }
.detail-header-begun .detail-header-team-home .detail-header-marker:has(.bug-football-icon) { right: 14px; left: auto; }

/* Same "above the logo" treatment for the team at bat (MLB) — same 44px
   box, just its own top offset since the baseball's own box (14px) is a
   different height than the football's (16px). */
.detail-header-marker:has(.bug-baseball-icon) {
  top: calc(50% - 39px); /* logo's own top edge (50% - 2px - 22px) plus a 1px gap and this icon's height */
  transform: none;
  width: 44px;
  justify-content: center;

  .bug-baseball-icon { width: 14px; height: 14px; }
}

.detail-header-begun .detail-header-team-away .detail-header-marker:has(.bug-baseball-icon) { left: 14px; }
.detail-header-begun .detail-header-team-home .detail-header-marker:has(.bug-baseball-icon) { right: 14px; left: auto; }

/* In the red zone (the ball's own icon, on the bug and here): a slow double
   pulse — "buh-bump, buh-bump" — same shape as the splash logo's heartbeat. */
@keyframes anim-football-buhbump {
  0%   { transform: scale(1); }
  12%  { transform: scale(1.25); }
  24%  { transform: scale(1); }
  36%  { transform: scale(1.3); }
  52%  { transform: scale(1); }
  100% { transform: scale(1); }
}

.detail-header-marker.in-redzone .bug-football-icon { animation: anim-football-buhbump 1.6s ease-in-out infinite; }

/* A muted score (the viewer flicked it away on the board): the digits go
   transparent and a blurred 8 stands in for them, the way the box score's
   own cells do (not the scribble O-Only uses below — that's reserved for
   a deliberate "I've chosen not to look at this side" omission, not a
   plain not-yet-revealed mute). Shown for real again in the replay. */
.detail-header-score-digits { position: relative; display: inline-block; }

.detail-header-score.is-muted .detail-header-score-digits {
  color: transparent;

  &::before {
    content: '8';
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    color: var(--text-primary);
    filter: blur(4px);
    pointer-events: none;
  }
}

/* O-Only (see setOOnlySide/clearOOnly): the other side's score gets the
   same scribble treatment as a muted one (.is-muted above) — this only
   ever applies in REPLAY (detailMutedSides always reads both as false
   there, see its own comment), so without this the locked-out side would
   otherwise show nothing hidden at all. */
.detail-header-score.is-oonly-dimmed .detail-header-score-digits {
  color: transparent;

  &::before {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    width: 30px;
    height: 33px;
    transform: translate(-50%, -50%);
    background: var(--text-primary);
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 90 100'%3E%3Cpath d='M10,75 L24,25 L38,75 L52,25 L66,75 L80,25' transform='rotate(45 45 50)' fill='none' stroke='black' stroke-width='20' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 90 100'%3E%3Cpath d='M10,75 L24,25 L38,75 L52,25 L66,75 L80,25' transform='rotate(45 45 50)' fill='none' stroke='black' stroke-width='20' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: contain;
    mask-size: contain;
    pointer-events: none;
  }
}

/* O-Only's two tap zones (see _detailOOnlyZonesHtml) live inside this
   wrapper — a 3px inset all round (matching the 3px .game-detail-modal's
   own padding-top, so the gap reads even on every side rather than
   tighter on the sides than the top/bottom — same treatment as .detail-
   tabs/BOX | PLAY below) with the OUTER drop shadow on the wrapper itself
   rather than on each zone individually — two adjacent shadows would
   otherwise double up right at the seam between them, reading like one
   button sitting on top of the other instead of two sitting evenly side by
   side. The wrapper has no background of its own, so the shadow still only
   shows where it falls outside the zones themselves. Its own border-radius
   (rounded top corners only, same 29px nested-corner math as each zone's
   own below) keeps the shadow's shape matching theirs. */
.detail-header-oonly-zones {
  position: absolute;
  top: 0;
  bottom: 3px; /* so there's air before the linescore below too */
  left: 3px;
  right: 3px;
  border-radius: 29px 29px 0 0;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
}

/* Each its own rounded glass "button," same raised material/emboss as a
   replay pill (--color--glass-bg/--color--glass-highlight/--shadow--glass)
   — just the inset highlight/shadow, not an outer one (see the wrapper
   above). Click capture doesn't need a raised z-index here: the header's
   other content (logos, score text, the possession marker) all set
   pointer-events:none on themselves now (nothing there has a tap of its
   own), so a tap anywhere over them already falls through to whichever
   zone is underneath — which is what lets the zone keep painting behind
   that content instead of its own background hiding it. */
.detail-header-oonly-zone {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 50%; /* fills its half of the wrapper exactly; meets its twin at the center */
  background-color: var(--color--glass-bg);
  background-image: var(--color--glass-highlight);
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.7),
              inset 0 -2px 3px rgba(0, 0, 0, 0.08);
  border: 1px solid rgba(0, 0, 0, 0.15);
  padding: 0;
  margin: 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.15s ease;
}

/* Only the outer top corner rounds (top-left for away, top-right for
   home) — the other three stay square. 29px: nested-corner formula (inner
   radius = outer radius − gap) off the modal's own 32px corner (see
   .game-detail-modal) and the wrapper's 3px inset from it, so the two
   curves read as concentric/continuous rather than two unrelated radii.
   The away zone also drops its own right border — they meet with no gap
   at the center, so the home zone's own left border alone is the seam
   between them (running the full header height, in and out of view above
   and below the center badge); without this they'd double up to 2px
   there. */
.detail-header-oonly-zone-away { left: 0; border-radius: 29px 0 0 0; border-right: none; }
.detail-header-oonly-zone-home { right: 0; border-radius: 0 29px 0 0; }

/* The locked-in side — pressed-in rather than a new color, same "inset
   track" look as .header-date-track (the groove the header's own date
   pill slides within): a dark translucent fill and a deep inset shadow
   along the top edge with just a hint of highlight along the bottom,
   rather than a new color. No highlight gradient over it either, so it
   reads as physically pushed in rather than raised like the unselected
   one. */
.detail-header-oonly-zone.is-active {
  background-color: rgba(26, 26, 26, 0.05);
  background-image: none;
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4),
              inset 0 -1px 2px rgba(255, 255, 255, 0.1);
}

.detail-header-center {
  position: relative; /* so .detail-header-ring can overlay the badge inside it */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: auto;
  margin: 0 4px;
}

/* The badge's own progress ring (see detailHeaderProgressRingHtml) — sized
   and centered to land exactly on the 80px circle below, whichever of the
   three badge states is showing. Rotated -90deg so the circle's default
   start point (3 o'clock) lands at 12 instead, without changing the path
   math; stroke-dashoffset then reveals it clockwise from there as normal. */
.detail-header-ring {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 80px;
  height: 80px;
  transform: translate(-50%, -50%) rotate(-90deg);
  pointer-events: none;

  circle {
    fill: none;
    stroke-width: 3;
    stroke-linecap: round;
    stroke-dasharray: 100; /* the circle's own full length, via pathLength="100" */
  }
}

/* One 80px circle for every header badge state — in progress, final and
   upcoming all size the same now, so this is the only place that sets it.
   Flat here (no shadow/press feedback) — the box score tab's badge isn't
   actually tappable (no O-Only zones under it there, see below), so it
   shouldn't read as a button. */
.detail-header-half,
.detail-header-time,
.detail-header-status .final-main {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 80px;
  height: 80px;
  padding-top: var(--padding-top--teko-font);
  border-radius: var(--border-radius--circular);
  border: 1px solid var(--border-out-dot);
  font-family: var(--font--league-gothic);
  color: var(--text-primary);
}

/* Only while O-Only's two zones are present (replay, MLB/NFL/NCAAF — see
   _detailOOnlyZonesHtml) does the badge need the modal's own glass fill
   (it's transparent otherwise, by design above): the zones' own center gap
   runs right behind the badge, and without a fill of its own the badge
   would just let that gap show straight through the circle instead of
   looking like it sits on top of it. This is also the only context the
   badge is actually tappable in (see the click listener below), so the
   raised/popped-out shadow and press-down feedback (same feel as the
   board/header pills and the modal's own dismiss button) live here too,
   not on the unconditional rule above. Same shadow throughout, regardless
   of which side (if any) is locked in. */
.detail-header:has(.detail-header-oonly-zone) .detail-header-half,
.detail-header:has(.detail-header-oonly-zone) .detail-header-time,
.detail-header:has(.detail-header-oonly-zone) .detail-header-status .final-main {
  /* A true opaque color here, not var(--color--glass-bg)'s own alpha mix —
     this badge sits over the O-Only zones' own shared center border (see
     .detail-header-oonly-zone-home's left border), which would otherwise
     show faintly through any translucent fill. */
  background-color: rgb(222, 221, 219);
  background-image: var(--color--glass-highlight);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
  transition: scale 0.15s ease;

  &:active {
    transition: scale 0.04s ease;
    scale: 0.95;
  }
}

/* Neither side hidden (both scores available) — no zone carries .is-active
   in that state (see _detailOOnlyZonesHtml), so checking for the absence of
   one anywhere is how this reads "nothing hidden" in CSS alone. Same glass
   fill as the zone buttons' own unselected look (adapts with day/night/
   grayscale automatically) instead of the opaque gray above, same raised
   shadow. */
.detail-header:has(.detail-header-oonly-zone):not(:has(.detail-header-oonly-zone.is-active)) .detail-header-half,
.detail-header:has(.detail-header-oonly-zone):not(:has(.detail-header-oonly-zone.is-active)) .detail-header-time,
.detail-header:has(.detail-header-oonly-zone):not(:has(.detail-header-oonly-zone.is-active)) .detail-header-status .final-main {
  background-color: var(--color--glass-bg);
}

.detail-header-half {
  flex-direction: column;
  line-height: 0.85;
}

.detail-header-time {
  font-size: 20px;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
}

.detail-header-half-word {
  font-size: 13px;
  font-weight: 600;
}

.detail-header-half-inning {
  font-size: 24px;
  font-weight: 700;
}

.detail-header-status {
  font-family: var(--font--league-gothic);
  font-size: 20px;
  font-weight: 600;
  text-align: center;
  color: var(--text-primary);

  .final-main {
    font-size: 22px;
    font-weight: 700;
  }
}

/* Exploratory redesign (see _mlbDetailSituationHtml): a true 1:1 diamond,
   bounded by whichever of width/height is tighter rather than always
   claiming the full width — on a short modal it gives up width to stay
   inside the height it's actually got, rather than spilling past it.
   .detail-matchup (below) is a container-query context sized to whatever
   .detail-body actually has left after its other children (the linescore,
   last play, etc. — see the flex-shrink:0 guards on those, scoped to MLB),
   so 100cqw/100cqh here read that real, current space; width: min(...)
   takes the smaller of the two, and aspect-ratio keeps height matching it.
   overflow: visible since the avatars/names at home and center are
   sized/placed off this box's own percentages but are free to spill past
   its edges — into .detail-matchup's own padding-bottom below, not into
   whatever follows it (see .detail-matchup). */
.detail-diamond {
  position: relative;
  width: min(100cqw, 100cqh);
  aspect-ratio: 1 / 1;
  overflow: visible;
}

.mlb-diamond-field {
  /* Without this, a plain static element always paints BELOW any positioned
     sibling (.detail-mound/.detail-batting-circle, both position:absolute)
     no matter which one is later in the DOM — home plate (drawn in here —
     see _mlbSvgDiamondFieldHtml) needs to sit on top of the batting circle
     centered on that same spot, not buried under it. */
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  /* The infield arc (see _mlbSvgDiamondFieldHtml) is sized to a real
     field's own proportions, which puts most of it above the viewBox
     entirely (its own top sits well past 2nd base) — without this, the
     SVG's default clip-to-viewBox would hide all of it outright instead of
     just the part that's actually meant to run off past the diamond's own
     edges (and, above that, the linescore or the modal's own top). */
  overflow: visible;
}

/* The bases/home-plate SVG (_mlbSvgDiamondMarkersHtml) — position:absolute,
   not :relative like .mlb-diamond-field above: that one can stay in normal
   flow since it's the only thing establishing .detail-diamond's own
   content size, but a SECOND in-flow element here would just stack below
   the first (relative doesn't take an element out of flow the way absolute
   does) rather than overlapping it — which is exactly what happened before
   this existed (the bases floated below the whole diamond instead of
   sitting on it). absolute+inset:0 overlays it exactly instead. */
.mlb-diamond-markers {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* A base runner's own marker — the same avatar/name pairing as the
   pitcher's/batter's (.detail-matchup-player, _mlbMatchupPlayerHtml), just
   with its own extra class (see .detail-base-runner below) rather than one
   of theirs, since its `left`/`top` are never fixed — _mlbRunnerMarkersHtml
   sets them inline, either a plain style (resting, unmoved this pitch) or
   an animation stepping through the same waypoints a traveling base path
   itself would. */
.detail-base-runner {
  flex-direction: column-reverse; /* name above the avatar, like the pitcher's own — see .detail-matchup-player-pitcher */
  transform: translate(-50%, -50%);

  .detail-matchup-avatar {
    border: 1.5px solid #1f4d2e;
  }

  /* Same little pill as the pitcher's/batter's own name. */
  .detail-matchup-name {
    background: var(--color--glass-bg);
    padding-left: 4px;
    padding-right: 4px;
    padding-bottom: 1px;
    border-radius: 4px;
  }
}
.detail-diamond.is-night .detail-base-runner .detail-matchup-avatar {
  border-color: var(--color--snug-black);
}


/* A small scoreboard-style B/S/O: a label row, then a bigger number under
   each, outlined and colored to match the linescore's own board
   (.detail-linescore-wrap) — ballpark-seat green normally, or its night
   version (see .is-night below) for a night game. A sibling of
   .detail-diamond rather than nested inside it (see _mlbDetailSituationHtml),
   so it's pinned to .detail-matchup's own bottom-left corner — the full
   flexible row, not just the (often narrower, centered) diamond square. */
.detail-count-board {
  position: absolute;
  bottom: 0;
  left: 16px;
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  padding: 3px 6px;
  border-radius: 8px;
  border: 1px solid #1f4d2e;
  /* Opaque, matching the page showing through the modal's own glass
     background — the diamond's arc (see .mlb-diamond-field's overflow:
     visible) runs right under this corner otherwise. */
  background: var(--bg-page);
}

.detail-count-board.is-night { border-color: var(--color--snug-black); }

.detail-count-board-row {
  display: flex;
  gap: 6px;
}

.detail-count-label,
.detail-count-value {
  width: 18px;
  font-family: var(--font--league-gothic);
  padding-top: var(--padding-top--teko-font);
  font-weight: 600;
  line-height: 1;
  color: #1f4d2e;
  text-align: center;

  .is-night & { color: var(--color--snug-black); }
}

.detail-count-label { font-size: 14px; }
.detail-count-value { font-size: 24px; }

/* The old and new number occupy the same cell during a reveal (see the
   `value` helper in _mlbDetailSituationHtml) so one can crossfade into the
   other instead of the new one just fading in over nothing. */
.detail-count-value-stack {
  position: relative;
  width: 18px;
  display: grid;

  > * { grid-area: 1 / 1; }
}

/* A replay reveal: the prior number (is-leaving) crossfades to the new one
   (is-entering) — see .detail-count-value-stack — at the same mark as
   the rest of the reveal (anim-ball-flyby, --duration--mlb-ball-flyby). */
.detail-count-value.is-entering { animation: anim-finals-fade-in var(--duration--mlb-reveal-fade) ease-out var(--duration--mlb-ball-flyby) both; }
.detail-count-value.is-leaving  { animation: anim-count-fade-out var(--duration--mlb-reveal-fade) ease-out var(--duration--mlb-ball-flyby) both; }
@keyframes anim-count-fade-out { from { opacity: 1; } to { opacity: 0; } }

/* Upcoming MLB games: the two probable starters under the (empty) linescore. */
.detail-pregame-pitchers {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: 4px;
  padding: 12px 16px 20px;
}

.detail-pregame-pitcher {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}

.detail-pregame-pitcher-avatar {
  width: 76px;
  height: 76px;
  border-radius: var(--border-radius--circular);
  object-fit: cover;
  background: var(--bg-out-dot);
}

.detail-pregame-pitcher-name {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font--base);
  font-size: 14px;
  font-weight: 600;
  line-height: 1;
  color: var(--text-primary);
}

.detail-pregame-vs {
  align-self: center;
  flex-shrink: 0;
  font-family: var(--font--league-gothic);
  padding-top: var(--padding-top--teko-font);
  font-size: 20px;
  color: var(--text-primary);
}

/* In-progress and finished games only (see _mlbLoadDetailBody) — the
   native sans-serif rather than Teko, since this is dense tabular data,
   not a display number. Always 1-9, plus at most one extra-inning column
   (see _mlbLinescoreInningNumbers). */
.detail-linescore-wrap {
  /* Ballpark-seat green, full modal width — only the table is inset. */
  background: #1f4d2e;
  padding: 6px 8px;
  margin-bottom: 8px;
  /* MLB's diamond (a later sibling via .detail-matchup) bleeds its own
     infield arc/foul lines upward past its own box on purpose (see
     .mlb-diamond-field's overflow: visible) — this keeps it painting
     behind this table rather than on top of it where the two overlap. */
  position: relative;
  z-index: 1;

  /* Just a 2px seam before the field/rink/court (each now flush, with no
     margin-top of its own) rather than this element's own usual 8px gap,
     which read as inconsistent against the court's and rink's own
     (different) spacing before this was unified. */
  &:has(+ .detail-field), &:has(+ .detail-rink), &:has(+ .detail-court) {
    margin-bottom: 2px;
  }
}

/* Night games: a black scoreboard with warm, slightly glowing off-white
   text (bulbs), the current inning's score still a clear yellow. */
.detail-linescore-wrap.is-night {
  background: var(--color--snug-black);

  .detail-linescore,
  .detail-linescore th {
    color: #f3ead2;
    text-shadow: 0 0 5px rgba(255, 236, 190, 0.45);
  }

  .detail-linescore td.is-current {
    color: #ffd23f;
    text-shadow: 0 0 8px rgba(255, 200, 40, 0.9);
  }

  /* Muted cells stay muted on the dark board: no yellow current-inning colour and
     no glow (a text-shadow still draws on transparent text) showing the number. */
  .detail-linescore td.is-muted {
    color: transparent;
    text-shadow: none;

    &::before { color: #f3ead2; }
  }

  .detail-linescore td.is-current.is-muted::before { color: #ffd23f; }

  /* Same warm-white tint as the muted scribble above, instead of the
     plain #fff the day board's own uses. */
  .detail-linescore td.is-oonly-dimmed::before { background: #f3ead2; }
}

.detail-linescore {
  width: 100%;
  table-layout: fixed;
  border-collapse: collapse;
  font-family: var(--font--base);
  font-size: 12px;
  color: #fff;

  /* No width here on purpose: under table-layout:fixed, every column
     without one splits the leftover space evenly, so the inning columns
     (and R/H/E) all come out identical whether or not there's a 2-digit
     extra-inning column in the mix. Only the team column is pinned. */
  th, td {
    padding: 1px 2px;
    text-align: center;
  }

  /* A little air either side of the divider before R (fixed layout, so this
     just nudges the centered text apart). Whichever column actually sits
     right before the totals — the 9th normally, but an extra inning once
     the game's gone long — gets the nudge, so extra innings stay evenly
     padded with the rest rather than the 9th keeping air it no longer
     needs next to it. */
  th:has(+ .detail-linescore-total), td:has(+ .detail-linescore-total) { padding-right: 6px; }
  /* All of R/H/E take the same nudge so they stay evenly spaced — MLB only:
     football/NHL have just the one trailing TOTAL column (the rule right
     above already nudges whatever sits before it, OT included), so this
     "last 3" would otherwise also catch a real period column — quarter 4,
     say, whenever an OT column's also in play — and shove it away from its
     neighbor for no reason. */
  &:not(.detail-linescore-football):not(.detail-linescore-nhl) {
    th:nth-last-child(-n+3), td:nth-last-child(-n+3) { padding-left: 6px; }
  }

  /* The inning / quarter numbers (and R H E / T): Teko, a size up from the
     sans they were. */
  th {
    font-family: var(--font--league-gothic);
    font-size: 14px;
    font-weight: 600;
    color: #fff;
  }

  /* Body rows sit flush against each other. */
  td { padding-top: 0; padding-bottom: 0; }

  /* A muted side's runs / points: blurred stand-ins, like the header's. */
  td.is-muted {
    position: relative;
    color: transparent;

    &::before {
      content: '8';
      position: absolute;
      inset: 0;
      /* Matches the cell's own padding (border-box so it doesn't just add to
         inset:0's own full padding-box size) rather than centering across
         the whole thing uniformly — R/H/E and the inning right before them
         carry a lopsided 6px/2px nudge (see above), and without this the "8"
         centered itself a couple px off from where the real (transparent)
         digit actually sits. */
      box-sizing: border-box;
      padding: inherit;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      filter: blur(3px);
      pointer-events: none;
    }
  }

  /* O-Only (see setOOnlySide/clearOOnly): the other team's cell gets the
     same scribble treatment as a muted one, not the "8" stand-in (see
     .detail-box-team's own tbody td:not(.detail-box-name) in styles.css
     for the shared mask) — same reasoning as the header's own
     .is-oonly-dimmed fix. */
  td.is-oonly-dimmed {
    position: relative;
    color: transparent;

    &::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 8px;
      height: 9px;
      transform: translate(-50%, -50%);
      background: #fff;
      -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 90 100'%3E%3Cpath d='M10,75 L24,25 L38,75 L52,25 L66,75 L80,25' transform='rotate(45 45 50)' fill='none' stroke='black' stroke-width='20' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
      mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 90 100'%3E%3Cpath d='M10,75 L24,25 L38,75 L52,25 L66,75 L80,25' transform='rotate(45 45 50)' fill='none' stroke='black' stroke-width='20' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
      -webkit-mask-repeat: no-repeat;
      mask-repeat: no-repeat;
      -webkit-mask-size: contain;
      mask-size: contain;
      pointer-events: none;
    }
  }

  /* The score for the inning being played. */
  td.is-current { color: #ffd23f; }

  /* ...and when that side's muted, the yellow score is blurred too — its
     stand-in stays yellow so the current inning still reads. */
  td.is-current.is-muted {
    color: transparent;
    text-shadow: none;

    &::before { color: #ffd23f; }
  }

  /* Needs to be on the header's first cell too — fixed layout reads its
     column widths off the first row, so putting this only on the body
     rows left the column auto-sized and the abbreviation crammed up
     against the 1st inning. */
  .detail-linescore-team {
    width: 46px;
    text-align: left;
    font-weight: 800;
    font-size: 16px;
    font-family: var(--font--league-gothic);
    line-height: 1.1; /* Teko's default is tall — this is what sets row height */
    padding-top: var(--padding-top--teko-font);
  }

  /* Same divider before R and before an extra-inning column, so extras read
     as a separate block after the 9th. */
  .detail-linescore-total,
  .detail-linescore-extra {
    border-left: 1px solid rgba(255, 255, 255, 0.35);
  }

  .detail-linescore-total { font-weight: 700; }
}

/* Football has only four (or five) periods, so at the full modal width the
   columns end up far apart: a shorter table, centered in the green. */
.detail-linescore.detail-linescore-football {
  width: min(100%, 240px);
  margin-inline: auto;
}

/* Hockey has only three (or four/five with OT/SO) periods — same deal, an
   even shorter table than football's. */
.detail-linescore.detail-linescore-nhl {
  width: min(100%, 220px);
  margin-inline: auto;
}

/* Basketball has the same four (or five with OT) periods as football —
   same fix, same width. */
.detail-linescore.detail-linescore-nba {
  width: min(100%, 240px);
  margin-inline: auto;
}

/* Finished MLB games: batting box score, away left / home right. */
.detail-box {
  display: flex;
  gap: 8px;
  padding: 0 8px 12px;
}

.detail-box-team {
  flex: 1;
  min-width: 0;
}

/* A muted team's box score — the column labels and the team's own name
   above stay put (those live in the table's <thead> — MLB's team name
   included, see its own th.detail-box-abbr — or in .detail-box-abbr-line
   above the table, never in <tbody>, so scoping to that is enough with no
   per-sport cases). Within <tbody>, names and stats are both just blurred
   in place (the real text/digits, out of focus) — deliberately NOT the
   scribble the header/board/linescore use elsewhere: this is a whole grid
   of numbers at once, and a scribble on every single cell reads as messy
   clutter rather than one clean "something's hidden here" mark. A plain
   blur alone would still leak shape on the stat cells though (a
   double-digit stat blurs into a wider smear than a single-digit one), so
   a fixed '8' stands in first, same trick the linescore's own td.is-muted
   uses. */
.detail-box-team.is-muted tbody td.detail-box-name {
  filter: blur(4px);
}

.detail-box-team.is-muted tbody td:not(.detail-box-name) {
  position: relative;
  color: transparent;

  &::before {
    content: '8';
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-primary);
    filter: blur(3px);
    pointer-events: none;
  }
}

.detail-box-table {
  width: 100%;
  table-layout: fixed;
  border-collapse: collapse;
  font-family: var(--font--base);
  font-size: 11px;
  color: var(--text-primary);

  th, td {
    padding: 1px 0;
    text-align: center;
    width: 17px; /* the four stat columns; the name column takes the rest */
  }

  /* Column labels (AB R H RBI, IP ER K, Passing...) in Teko — its narrow letters
     suit the narrow stat columns — a size up from the sans they were. */
  th {
    font-family: var(--font--league-gothic);
    padding-top: var(--padding-top--teko-font);
    font-size: 13px;
    font-weight: 600;
    color: var(--text-secondary);
  }

  /* The section titles in the name column (Passing, Rushing, Receiving, Kicking,
     Pitchers) share the stat labels' Teko, just uppercase. */
  th.detail-box-name:not(.detail-box-abbr) { text-transform: uppercase; }

  th.detail-box-abbr {
    font-family: var(--font--league-gothic);
    padding-top: var(--padding-top--teko-font);
    font-size: 14px;
    text-transform: uppercase;
    color: var(--text-primary);
  }

  .detail-box-name {
    width: auto;
    text-align: left;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  tr.is-sub .detail-box-name { padding-left: 6px; }

  /* The batter currently up, while the game's live. */
  tr.is-batting td { background: rgba(232, 192, 32, 0.18); }
  tr.is-batting .detail-box-name { color: var(--text-primary); font-weight: 700; }

  /* Pitcher row: name (which gives way with an ellipsis) then the W/L
     decision and new record, which never gets cut. */
  .detail-box-namewrap { display: flex; gap: 3px; min-width: 0; }
  .detail-box-pname {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .detail-box-decision {
    flex-shrink: 0;
    font-size: 10px;
    font-weight: 700;
    color: var(--text-secondary);
  }

  /* The pitchers sit under the hitters with a little air between. */
  &.detail-box-pitching { margin-top: 8px; }

  .detail-box-dash { color: var(--text-muted); margin-right: 2px; }
  .detail-box-pos  { font-size: 9px; color: var(--text-muted); }

  /* Pregame lineup rows: position gets its own narrow column ahead of the
     name, Teko rather than the row's own sans (see _mlbPregameLineupHtml),
     right-aligned toward the name rather than centered in its own column. */
  &.detail-lineup-table {
    .detail-lineup-pos {
      width: 20px;
      text-align: right;
      padding-right: 3px;
      font-family: var(--font--league-gothic);
      padding-top: var(--padding-top--teko-font);
      font-size: 12px;
      font-weight: 600;
      color: var(--text-secondary);
    }

    /* AVG/HR/RBI: more room than the 4-stat tables' 17px, right-aligned
       (numbers of differing width read better lined up on the ones place
       than centered) with tighter-set digits to make the most of it. HR/RBI
       run shorter (1-3 digits, never AVG's ".277") so they need less of it. */
    .detail-lineup-stat {
      width: 26px;
      text-align: right;
      padding-right: 2px;
      letter-spacing: -0.5px;
    }
    .detail-lineup-stat-narrow { width: 22px; }
  }
}

/* ---- NFL detail modal ---- */

/* Live: "2nd" over the clock — the quarter big, the clock lighter. */
.detail-header-half-clock {
  .detail-header-half-word { font-size: 20px; font-weight: 600; }
  .detail-header-half-inning { font-size: 18px; font-weight: 400; }
}

/* The abbreviation's own row: the anchor the logo, record and the rank/timeouts
   row hang from. */
.detail-header-abbr-row {
  position: relative;
  display: flex;
  justify-content: center;
}

/* Above the abbreviation (upcoming games), centered on it and out of the flow:
   the college ranking — same side for both teams, so it doesn't need to flip.
   Pulled up tight against the 36px abbreviation's own line box (its leading
   leaves far more room than the visible glyphs do) — same -11px pull
   .detail-header-above uses to hug the similarly-sized score digits. */
.detail-header-outer {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-bottom: -11px;
  display: flex;
  align-items: center;
  gap: 4px;
}

.detail-header-rank {
  font-family: var(--font--base);
  font-size: 14px;
  font-weight: 600;
  line-height: 1;
  color: var(--text-secondary);
}

/* Timeouts left, in the linescore: three small circles hanging off the left of
   the team's name (out of the flow, into the room the shorter football table
   leaves beside it), in the same colour as the letters — filled while
   available, hollow once used. */
.detail-linescore-team { position: relative; }

.detail-timeouts {
  position: absolute;
  right: 100%;
  top: 50%;
  transform: translateY(-50%);
  margin-right: 5px;
  display: flex;
  gap: 2px;

  i {
    box-sizing: border-box;
    width: 5px;
    height: 5px;
    border-radius: var(--border-radius--circular);
    border: 1px solid currentColor;
    background: currentColor;

    &:not(.on) {
      background: transparent;
      opacity: 0.5;
    }
  }
}

/* A simple field, edge to edge: end zones (team colour, nickname running up
   the left / down the right like painted on a real one) around a 100-yard
   field with a line every 10, and the ball at its spot. */
.detail-field {
  display: flex;
  /* A real NFL field is 120 yards end to end (the 100-yard field plus a
     10-yard end zone either side — the same 10:100:10 split .detail-field-ez/
     .detail-field-play's own flex ratios already use below), scaled
     narrower than its real 53.33-yard width so it reads as a long strip. */
  width: 100%;
  aspect-ratio: 120 / 40;
  margin: 0 0 4px;
}

.detail-field-ez {
  flex: 10;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ez-color, #444); /* --ez-color is set inline to the team's colour */
  color: #fff;
  font-family: var(--font--league-gothic);
  font-size: 18px;
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-shadow: 0 0 3px rgba(0, 0, 0, 0.55);

  span {
    writing-mode: vertical-rl;
    /* Teko's top-of-letter padding, turned sideways: the letters' tops face
       right here (and left once the away side is flipped), so it's a side
       padding — in this box's own frame it's the same one for both. */
    padding-right: var(--padding-top--teko-font);
    white-space: nowrap;
    max-height: 100%; /* college nicknames can be long */
    overflow: hidden;
    text-overflow: ellipsis;
    /* letter-spacing trails the last letter too, which nudges the word off
       centre along its length (up-down here) — take that back so there's the
       same space above and below. */
    margin-inline-end: -0.04em;
  }

  &.detail-field-ez-away span { transform: rotate(180deg); }
}

/* A score in this end zone: the team colour and white trade places once per
   point scored (--blinks, set from JS), starting once the ball has got there
   (--blink-delay). Hard flips, ending on the colour. */
.detail-field-ez.is-scoring {
  animation: anim-ez-blink 0.3s step-end var(--blinks, 3) var(--blink-delay, 0s) both;
}

@keyframes anim-ez-blink {
  0% { background: var(--ez-color, #444); color: #fff; }
  50% { background: #fff; color: var(--ez-color, #444); text-shadow: none; }
}

.detail-field-play {
  flex: 100;
  position: relative;
  background-color: #2e7d3f;
  background-image: repeating-linear-gradient(90deg,
    rgba(255, 255, 255, 0.3) 0, rgba(255, 255, 255, 0.3) 1px,
    transparent 1px, transparent 10%);
  border-inline: 1px solid #fff; /* goal lines */

  &::before { /* the 50 */
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: calc(50% - 0.5px);
    width: 1px;
    background: rgba(255, 255, 255, 0.55);
  }
}

/* The red zone, tinted while the ball's in it: the last 20 yards before the goal
   line the offense is attacking. Under the logo, numbers, lines and ball. */
.detail-field-redzone {
  position: absolute;
  top: 0;
  bottom: 0;
  background: rgba(220, 38, 38, 0.3);
  pointer-events: none;
}

/* The home team's logo at midfield, like it's painted on the turf. */
.detail-field-logo {
  position: absolute;
  top: 50%;
  left: 50%;
  height: 40%;
  width: auto;
  transform: translate(-50%, -50%);
  opacity: 0.7;
  pointer-events: none;
}

/* Yard numbers along the bottom, centered on their 10-yard lines. */
.detail-field-yd {
  position: absolute;
  bottom: 3px;
  transform: translateX(-50%);
  font-family: var(--font--league-gothic);
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  color: rgba(255, 255, 255, 0.55);
  pointer-events: none;
}

/* The same numbers along the top edge, upside-down. */
.detail-field-yd.detail-field-yd-top {
  bottom: auto;
  top: 3px;
  transform: translateX(-50%) rotate(180deg);
}

/* Line of scrimmage: where the ball is spotted, a translucent black line. */
.detail-field-los {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  margin-left: -1px;
  background: rgba(0, 0, 0, 0.5);
}

/* The yellow first-down line, drawn where the offense has to get to. */
.detail-field-firstdown {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  margin-left: -0.5px;
  background: #ffd400;
  box-shadow: 0 0 4px rgba(255, 212, 0, 0.7);
}

.detail-field-ball {
  position: absolute;
  top: 50%;
  width: 18px;
  height: 11px;
  border-radius: 50%;
  background: #8a552c;
  border: 1px solid #3d2410;
  transform: translate(calc(-50% + var(--tip, 0px)), -50%);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
  z-index: 1;

  &::after { /* the laces */
    content: '';
    position: absolute;
    top: 50%;
    left: 22%;
    right: 22%;
    height: 1px;
    background: #fff;
  }
}

/* Where the previous play left the ball: a plain solid white outline,
   and a semi-opaque line from it to the ball. Both sit
   under the live ball. */
.detail-field-ball-ghost {
  position: absolute;
  top: 50%;
  width: 18px;
  height: 11px;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, 0.5); /* same color as .detail-field-los */
  transform: translate(calc(-50% + var(--tip, 0px)), -50%);
}

.detail-field-trace {
  position: absolute;
  top: 50%;
  height: 1px;
  margin-top: -0.5px;
  margin-left: var(--tip, 0px); /* ends at the balls' centers */
  /* No visible line — the element (and its width/position animation, which
     the ghost's fade timing keys off) stays, it's just invisible now. */
  background: transparent;
}

.detail-field-ball.is-moving,
.detail-field-trace.is-moving { transition: left 0.9s ease-in-out, transform 0.9s ease-in-out; }
.detail-field-los.is-moving,
.detail-field-firstdown.is-moving { transition: left 0.5s ease-in-out; }

/* The outline and line fade away once the graphics are done (JS: GHOST_FADE_MS). */
.detail-field-ball-ghost.is-fading,
.detail-field-trace.is-fading { opacity: 0; transition: opacity 0.4s ease-out; }

/* NHL's rink (see _nhlRinkHtml in nhl.js): same footprint as football's
   field, rounded into a rink instead — a goal line near each end, a blue
   line at center ice, the home logo faint at midice, and each team's own
   running shots-on-goal tally sitting in its own zone. The puck (and the
   shooter/goalie avatars for whichever shot is showing) sit on top. */
.detail-rink {
  position: relative;
  /* A real NHL rink is 200x85ft — keeping that same ratio (rather than a
     fixed height) is what keeps the rink reading like a rink instead of a
     squashed oval. Overflow stays visible here (see .detail-rink-surface
     below) so a shot taken from deep in the corner doesn't get its own
     avatar clipped by the boards. */
  width: 100%;
  aspect-ratio: 200 / 85;
  margin: 0 0 4px;
}

.detail-rink-surface {
  position: absolute;
  inset: 0;
  border-radius: 20px;
  overflow: hidden;
  background: radial-gradient(ellipse at 50% 30%, #e8f4fb 0%, #d3e9f7 55%, #bfdcef 100%);
  border: 1px solid #eef6fb;
  box-shadow: inset 0 0 0 1px #a9c6dc;
}

/* The goal area (net/crease) tucked behind each goal line, into the rink's
   own rounded end — only its outer corners (against that end) are rounded,
   tighter than the rink's own radius; the inner corners (against the goal
   line) stay square. Its inner edge sits right at the goal line
   (NHL_AWAY_GOAL_PCT/NHL_HOME_GOAL_PCT in nhl.js — a scoring puck slides
   all the way to its outer edge, see NHL_GOAL_AREA_INSET_PX there), and
   its outer edge stops a few px short of the rink's own wall rather than
   touching it. Tall enough for the goalie's avatar plus some breathing room. */
.detail-rink-goal-area {
  position: absolute;
  top: 50%;
  height: 38px; /* .detail-rink-avatar's 26px + padding */
  transform: translateY(-50%);
  background: rgba(209, 60, 60, 0.12);
  pointer-events: none;
}
.detail-rink-goal-area-away { left: 4px; right: 92%; border-radius: 8px 0 0 8px; } /* keep the 4px in step with NHL_GOAL_AREA_INSET_PX */
.detail-rink-goal-area-home { left: 92%; right: 4px; border-radius: 0 8px 8px 0; }

.detail-rink-line {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  margin-left: -1px;
}

.detail-rink-line-goal { background: #d13c3c; }
.detail-rink-line-goal-away { left: 8%; }
.detail-rink-line-goal-home { left: 92%; }
.detail-rink-line-center { left: 50%; background: #d13c3c; } /* the real center-ice line is red, not blue */
/* The two blue lines: 25ft off center ice on a real 200ft rink — using the
   same scale _nhlShotPct maps shots onto (center 50%, each goal line at
   NHL_AWAY_GOAL_PCT/NHL_HOME_GOAL_PCT, 100ft of ice between them), so
   50 ± (25/100)*42 = 39.5% / 60.5%. */
.detail-rink-line-blue { background: rgba(47, 111, 209, 0.35); width: 2px; margin-left: -1px; }
.detail-rink-line-blue-away { left: 39.5%; }
.detail-rink-line-blue-home { left: 60.5%; }

/* Faceoff circles/dots — fixed px size (rather than computed from real
   feet through the same stretched scale the lines use) so they read as
   true circles regardless of the rink's own non-square aspect; red in
   each zone, blue at center ice, same as a real rink. Positions below are
   the real spots (20ft from the goal line, 22ft off the centerline for
   the zone circles; 25ft off center for the blue lines, already placed
   above) run through the same per-axis scale _nhlShotPct uses. */
.detail-rink-faceoff-circle {
  position: absolute;
  width: 15%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  border: 2px solid rgba(209, 60, 60, 0.15);
  pointer-events: none;
}
.detail-rink-faceoff-circle.is-center { left: 50%; top: 50%; border-color: rgba(47, 111, 209, 0.15); }
.detail-rink-faceoff-away-top     { left: 16.4%; top: 24.12%; }
.detail-rink-faceoff-away-bottom  { left: 16.4%; top: 75.88%; }
.detail-rink-faceoff-home-top     { left: 83.6%; top: 24.12%; }
.detail-rink-faceoff-home-bottom  { left: 83.6%; top: 75.88%; }

.detail-rink-faceoff-dot {
  position: absolute;
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  border-radius: 50%;
  background: rgba(209, 60, 60, 0.25);
  pointer-events: none;
}
.detail-rink-faceoff-dot.is-center { left: 50%; top: 50%; background: rgba(47, 111, 209, 0.25); }

/* The home team's logo at center ice, like it's painted on the surface —
   same idea as football's .detail-field-logo. */
.detail-rink-logo {
  position: absolute;
  top: 50%;
  left: 50%;
  height: 33%;
  width: auto;
  transform: translate(-50%, -50%);
  opacity: 0.35;
  pointer-events: none;
}

/* The "SOG" label, fixed dead center at center ice — unlike the tallies
   beside it, its own position never depends on how many digits either one
   is showing. A small white plaque behind just the label (not the counts)
   keeps it readable against the ice/logo underneath. Reused as-is, text
   swapped for "Shootout", once the game's down to one (see
   _nhlShootoutLabelHtml) — same exact look either way. */
.detail-rink-shots-label {
  position: absolute;
  top: 10px; /* 2px above .detail-rink-sog's own vertical middle */
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--font--league-gothic);
  padding: var(--padding-top--teko-font) 5px 1px;
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(20, 55, 115, 0.55); /* a dark blue, in the ice's own family, rather than a plain dark gray */
  background: #fff;
  border-radius: 5px;
  pointer-events: none;
}

/* Each tally is anchored by its near edge (the one facing the label), a
   fixed distance out from center — so it grows a second digit away from
   center instead of shifting the label, and away's own width can never
   move home's position (or the label's) since each is anchored
   independently rather than as a group. */
.detail-rink-sog {
  position: absolute;
  top: 5px;
  font-family: var(--font--league-gothic);
  padding-top: var(--padding-top--teko-font);
  font-size: 26px;
  font-weight: 600;
  line-height: 1;
  color: rgba(20, 55, 115, 0.55); /* a dark blue, in the ice's own family, rather than a plain dark gray */
  pointer-events: none;
}
.detail-rink-sog-away { right: 57%; text-align: right; }
.detail-rink-sog-home { left: 57%; text-align: left; }

/* Shootout bulbs (see _nhlShootoutBulbsHtml) — share the same line as the
   "Shootout" label now sitting at center ice (see .detail-rink-shots-label),
   flanking it to either side rather than competing with the shooter/goalie/
   puck below (vertical middle). Empty by default (unlit ice), lit green for
   a make or red for a miss once that round's actually happened. */
.detail-rink-shootout-bulb {
  position: absolute;
  top: 18px; /* lines up with .detail-rink-shots-label's own vertical middle (its "Shootout" text, here) */
  width: 10px;
  height: 10px;
  transform: translate(-50%, -50%);
  border-radius: var(--border-radius--circular);
  background: rgba(255, 255, 255, 0.5);
  border: 1px solid rgba(20, 55, 115, 0.3);
  pointer-events: none;
}

.detail-rink-shootout-bulb.is-made {
  background: #2ecc71;
  border-color: #1e9e57;
  box-shadow: 0 0 4px rgba(46, 204, 113, 0.8);
}

.detail-rink-shootout-bulb.is-missed {
  background: #e74c3c;
  border-color: #c0392b;
  box-shadow: 0 0 4px rgba(231, 76, 60, 0.8);
}

/* Behind the shooter and goalie avatars (z-index below .detail-rink-player)
   so it slides out from under one and settles in under the other. */
.detail-rink-puck {
  position: absolute;
  top: 50%;
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  background: #1a1a1a;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
  z-index: 0;
}

.detail-rink-puck.is-moving { transition: left 0.3s linear, top 0.3s linear; } /* keep in sync with NHL_PUCK_SLIDE_MS — top converges on the goalie's own fixed center from wherever the real shot started */

/* A save's rebound off the goalie: fast and linear (a real deflection isn't
   easing in or out), left/top carrying it through each bounce leg, opacity
   fading it out on the last one. Each leg's own duration is set inline by
   _nhlAnimatePuck (nhl.js), since a board bounce or two can come before the
   final fade — no fixed duration here to keep in sync with. */
.detail-rink-puck.is-rebounding { transition-property: left, top, opacity; transition-timing-function: linear; }

/* A miss that's wide of the net outright (not off a post/crossbar, which
   keeps the deflection above) never reaches the goalie — one straight
   diagonal flight (left and top together) past the goal line, then a quick
   fade, rather than a bounce off anything. Keep the left/top duration here
   in sync with NHL_PUCK_SLIDE_MS in nhl.js (same as .is-moving), so it
   still lands exactly when the SAVE!/SCORE!/MISS! text reveals. */
.detail-rink-puck.is-wide { transition: left 0.3s linear, top 0.3s linear, opacity 0.2s linear 0.3s; }

/* The shooter (always center ice) and goalie (always their own team's
   fixed goal line — see NHL_AWAY_GOAL_PCT/NHL_HOME_GOAL_PCT in nhl.js) for
   whichever shot is showing, vertically centered on the puck's own travel
   line so it passes behind each avatar at either end of its slide. */
.detail-rink-player {
  position: absolute;
  top: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  z-index: 1;
}

/* The shooter skating in from deeper in their own zone to center ice,
   before the puck leaves — see NHL_SHOOTER_SKATE_PCT/_MS in nhl.js. */
.detail-rink-player-shooter.is-skating { transition: left 120ms linear, top 120ms linear; } /* keep in sync with NHL_SHOOTER_SKATE_MS */

.detail-rink-avatar {
  width: 26px;
  height: 26px;
  margin-top: -13px; /* centers the avatar itself on top:50%, not the name below it */
  border-radius: var(--border-radius--circular);
  object-fit: cover;
  background: var(--bg-out-dot);
  border: 1px solid rgba(255, 255, 255, 0.85);
}

.detail-rink-name {
  font-family: var(--font--base);
  font-size: 9px;
  font-weight: 600;
  color: var(--color--snug-black);
  background: rgba(255, 255, 255, 0.8);
  padding: 0 4px;
  border-radius: 3px;
  white-space: nowrap;
  max-width: 68px;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* NBA's court (see _nbaCourtHtml in nba.js): a hardwood floor — one flat
   warm wood tone with a faint plank-seam texture running the court's own
   length — under a half-court line, each end's free-throw lane/circle and
   three-point arc, and a basket near each end. Same footprint and fixed
   "away always shoots left, home always right" convention as the rink. */
.detail-court {
  position: relative;
  /* A real NBA court is 94x50ft — keeping that same ratio (rather than a
     fixed height, like the rink's) is what keeps the free-throw/center
     circles actually round instead of squashed into ellipses. Overflow
     stays visible here (unlike the rink) so a shooter near the baseline
     or sideline can stand with their avatar partly off the hardwood
     instead of getting clipped — see .detail-court-surface below, which
     is what actually clips the floor itself to its own rectangle. */
  width: 100%;
  aspect-ratio: 94 / 50;
  margin: 0 0 4px;
}

.detail-court-surface {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background-color: #c48b4b;
  background-image:
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.07) 0, rgba(255, 255, 255, 0.07) 1px, transparent 1px, transparent 7px);
  border: 1px solid #9c6b34;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}

.detail-court-lines {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.detail-court-lines path,
.detail-court-lines circle,
.detail-court-lines line,
.detail-court-lines rect,
.detail-court-lines polyline {
  fill: none;
  stroke: rgba(255, 255, 255, 0.8);
  stroke-width: 2.5;
  vector-effect: non-scaling-stroke;
}

.detail-court-key { fill: rgba(20, 15, 10, 0.1) !important; }
.detail-court-rim { fill: none; }

/* The home team's logo at center court, like it's painted on the floor —
   same idea as the rink's own .detail-rink-logo. */
.detail-court-logo {
  position: absolute;
  top: 50%;
  left: 50%;
  height: 30%;
  width: auto;
  transform: translate(-50%, -50%);
  opacity: 0.55;
  pointer-events: none;
}

/* The FG tally at center court — same plaque look as the rink's SOG. */
.detail-court-shots-label {
  position: absolute;
  top: 10px;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--font--league-gothic);
  padding: var(--padding-top--teko-font) 5px 1px;
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(110, 62, 18, 0.65);
  background: #fff;
  border-radius: 5px;
  pointer-events: none;
}

.detail-court-fg {
  position: absolute;
  top: 5px;
  font-family: var(--font--league-gothic);
  padding-top: var(--padding-top--teko-font);
  font-size: 22px;
  font-weight: 600;
  line-height: 1;
  color: rgba(110, 62, 18, 0.7);
  pointer-events: none;
}
.detail-court-fg-away { right: 57%; text-align: right; }
.detail-court-fg-home { left: 57%; text-align: left; }

.detail-court-player {
  position: absolute;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  z-index: 1;
}

.detail-court-avatar {
  width: 26px;
  height: 26px;
  border-radius: var(--border-radius--circular);
  object-fit: cover;
  background: var(--bg-out-dot);
  border: 1px solid rgba(255, 255, 255, 0.85);
}

/* The defender in a standalone foul step (see _nbaFoulCourtHtml) — a
   darker ring so the two players read as shooter vs. defender at a
   glance, not just two identical avatars. */
.detail-court-player-defender .detail-court-avatar {
  border-color: rgba(0, 0, 0, 0.55);
}

.detail-court-name {
  font-family: var(--font--base);
  font-size: 9px;
  font-weight: 600;
  color: var(--color--snug-black);
  background: rgba(255, 255, 255, 0.8);
  padding: 0 4px;
  border-radius: 3px;
  white-space: nowrap;
  max-width: 68px;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The ball — a shot's own flight, eased by hand in nba.js (_nbaAnimateBall)
   rather than a single CSS transition, since it arcs (a lifted midpoint)
   instead of sliding in a straight line the way the puck does. */
.detail-court-ball {
  position: absolute;
  width: 13px;
  height: 13px;
  margin: -6.5px 0 0 -6.5px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #f3883a, #c9611a);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
  z-index: 2;
  opacity: 0;
  transition: left 0.18s linear, top 0.18s linear, opacity 0.2s linear;
}

.detail-court-ball::before {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid rgba(0, 0, 0, 0.45);
  border-radius: 50%;
}

/* Replay (finished NFL): the slider under the modal's content. The header date
   pill's language: a glass pill in an inset dark track, dragged left / right
   (--drag-x, set while dragging) and springing back. One font, weight, size.
   A thin progress bar spans the full width to mark off the whole area, its
   green fill showing how far into the play list the current step is, a white
   knob riding at the fill's edge that can be dragged anywhere along the bar
   by hand (see _detailInitReplaySliderKnob) — letting go snaps to the
   nearest play; then the controls themselves, the pills side by side:
   [ play-by-play ] [ score-by-score ] (NHL's two; MLB and football each add
   a third grain of their own between them — see .detail-replay-row-3). */
.detail-replay-row {
  /* The pill tracks and the 8px gaps between them (.detail-replay-controls)
     — kept as variables so .detail-replay-header below can size itself to
     exactly the same total width without duplicating the arithmetic, and so
     everything stays in step if any of these change. --replay-slack is how
     much wider each track is than the pill riding inside it (see
     .detail-replay-pill) — half of it is what _initHeaderDateSwipe's own
     maxTravel drags that pill across, so a full drag lands its edge right
     at the track's own edge instead of stopping short of it. --replay-pills
     is how many of these a row holds, for .detail-replay-header's width. */
  --replay-pill-w: 160px;
  --replay-slack: 48px;
  --replay-gap: 8px;
  --replay-pills: 2;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: center; /* the controls stay centered at their own (narrower) width */
  gap: 8px;
  margin: 20px 0 10px;

  &[hidden] { display: none; }
}

/* The three-pill variant (MLB's Score / At-Bat / Pitch, see
   _mlbEnsureReplaySlider; football's Score / Drive / Play, see
   _footballEnsureReplaySlider) — each pill scaled down a third from the
   other sports' two so all three still fit one row at a phone's width,
   rather than growing the row itself past it. */
.detail-replay-row-3 {
  --replay-pill-w: 104px;
  --replay-slack: 32px;
  --replay-pills: 3;
}

/* Sized to exactly match .detail-replay-controls's own rendered width (see
   .detail-replay-row's variables) — not stretched to the row's full width —
   so the progress bar it holds lines up with the outer edges of the two
   pills below it, instead of some unrelated fraction of the modal. */
.detail-replay-header {
  width: calc(var(--replay-pill-w) * var(--replay-pills) + var(--replay-gap) * (var(--replay-pills) - 1));
  height: 28px; /* tall enough for the knob (below) to clear the thin track without clipping */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Very dark and inset like the other tracks in this row (.detail-replay), just
   thin and inset instead of a discrete pill's slot. Inset 8px from each edge
   of the header (same gap as between the pills below — see --replay-gap) so
   the knob's own travel range (see .detail-replay-progress-knob and
   _detailInitReplaySliderKnob's paint()) stops short of the pills' outer
   edges by that same 8px, rather than overhanging past them. */
.detail-replay-progress {
  position: absolute;
  left: 8px;
  width: calc(100% - 16px);
  height: 5px;
  border-radius: var(--border-radius--circular);
  background: rgba(0, 0, 0, 0.55);
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4),
              inset 0 -1px 2px rgba(255, 255, 255, 0.1);
  overflow: hidden; /* clips the fill's own square end to the track's rounded one */
}

.detail-replay-progress-fill {
  height: 100%;
  width: 0%;
  border-radius: var(--border-radius--circular);
  background: #2e9e4f; /* same green as the ball count's "on" dots */
  transition: width var(--duration--spring-bounce) var(--ease--spring-bounce);
}

/* The slider's own handle, riding at the fill's right edge — a sibling of
   .detail-replay-progress (not a child of it, which clips to the thin
   track's own height) so its full circle can clear the bar above and below.
   `left` (via the -14px margin below, it's the knob's own CENTER) is set in
   JS (_detailInitReplaySliderKnob's paint()) as
   calc(22px + pct * (100% - 44px)): the track's own 8px inset plus the
   knob's 14px radius, so at pct 0/1 it's the knob's own LEFT/RIGHT edge —
   not its center — that lands on the track's two ends, rather than the
   circle overhanging past them. */
.detail-replay-progress-knob {
  position: absolute;
  top: 50%;
  left: 0%;
  width: 28px;
  height: 28px;
  margin: -14px 0 0 -14px;
  border-radius: 50%;
  /* Same glass material as the pills below it (.detail-replay-pill) —
     color, highlight and inset shadow all shared, rather than a plain
     white dot with its own separate drop shadow. */
  background-color: var(--color--glass-bg);
  background-image: var(--color--glass-highlight);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  box-shadow: var(--shadow--glass);
  transition: left var(--duration--spring-bounce) var(--ease--spring-bounce);
  cursor: grab;
  touch-action: none; /* dragging it shouldn't also scroll the modal */
  -webkit-tap-highlight-color: transparent;
}
.detail-replay-progress-knob:active { cursor: grabbing; }

/* LIVE/auto-follow's only visible sign (see _detailSyncReplayLiveGlow) — a
   pulsing glow behind the knob in the same orange as an occupied base (see
   _mlbSvgDiamondFieldHtml's base()), once scrubbing lands on the very last
   play; any other move drops the class, no separate on/off control. */
.detail-replay-progress-knob.is-live {
  animation: detail-replay-knob-glow 1.8s ease-in-out infinite;
}
@keyframes detail-replay-knob-glow {
  0%, 100% { box-shadow: var(--shadow--glass), 0 0 0 0 rgba(232, 160, 32, 0.9), 0 0 8px 2px rgba(232, 160, 32, 0.85); }
  50%      { box-shadow: var(--shadow--glass), 0 0 0 5px rgba(232, 160, 32, 0.25), 0 0 18px 8px rgba(232, 160, 32, 0.55); }
}

/* 16px narrower than .detail-replay-header's own width (same pill-w/gap/
   pills formula, just -16px) — centered the same way under .detail-replay-
   row, this lands its outer edges exactly 8px in from the header's, i.e.
   right where the (also 8px-inset) progress bar's own ends are. The two (or
   three) .detail-replay tracks absorb that 16px via their default
   flex-shrink, a few px each — .detail-replay-pill's own width stays keyed
   to the unshrunk --replay-pill-w, so the pill itself never loses size. */
.detail-replay-controls {
  display: flex;
  align-items: center;
  gap: var(--replay-gap);
  width: calc(var(--replay-pill-w) * var(--replay-pills) + var(--replay-gap) * (var(--replay-pills) - 1) - 16px);
}

.detail-replay {
  position: relative;
  /* See --replay-slack (.detail-replay-row) — this track is that much wider
     than the pill inside it. */
  width: var(--replay-pill-w);
  height: 40px;
  border-radius: var(--border-radius--circular);
  background: rgba(26, 26, 26, 0.1);
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4),
              inset 0 -1px 2px rgba(255, 255, 255, 0.1);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Both the play-by-play and the score-by-score pill — "‹ Play ›" / "‹ Score ›",
   the chevrons hinting at the drag either way. Sized off the track's own
   width minus --replay-slack, rather than a fixed px, so the smaller
   three-pill row (.detail-replay-row-3) scales this down to match without
   a separate rule. */
.detail-replay-pill {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: calc(var(--replay-pill-w) - var(--replay-slack));
  height: 42px;
  border-radius: var(--border-radius--circular);
  background-color: var(--color--glass-bg);
  background-image: var(--color--glass-highlight);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  box-shadow: var(--shadow--glass);
  font-family: var(--font--league-gothic);
  font-size: 16px;
  font-weight: 600;
  text-transform: uppercase;
  padding-top: var(--padding-top--teko-font);
  color: var(--text-primary);
  cursor: grab;
  touch-action: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  transform: translateX(var(--drag-x, 0px));
  transition: transform var(--duration--spring-bounce) var(--ease--spring-bounce);
}

/* Bigger and lighter than the label, so they read as a subtle hint rather
   than competing with it — same font (see the earlier mixed-font centering
   issue) so the pill's one padding-top correction still centers both. */
.detail-replay-arrow {
  font-family: var(--font--league-gothic);
  font-size: 22px;
  font-weight: 600;
  color: var(--text-secondary);
  opacity: 0.6;
}

/* The three-pill row's narrower pills (.detail-replay-row-3) need smaller
   text and a tighter label/chevron gap to still read as "‹ At-Bat ›" rather
   than spilling past their own narrower width. */
.detail-replay-row-3 {
  .detail-replay-pill { gap: 3px; font-size: 13px; }
  .detail-replay-arrow { font-size: 17px; }
}

.detail-status-line {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 6px 16px 0;
}

.detail-status-line.is-entering { animation: anim-last-play-in 0.35s ease-out both; }

.detail-status-text {
  font-family: var(--font--league-gothic);
  padding-top: var(--padding-top--teko-font);
  font-size: 32px;
  font-weight: 600;
  line-height: 1;
  color: var(--text-primary);
}


/* A scoring play, in place of the down and distance (see _footballStatusLineHtml) —
   two lines ("TOUCHDOWN" / team), tighter than the single-line default. */
.detail-status-score {
  text-transform: uppercase;
  line-height: 0.85;
  text-align: center;
}

/* The team name comes in a beat after the score label above it — same slide
   as .detail-status-line's own entrance (anim-last-play-in), just delayed. */
.detail-status-score-team {
  display: inline-block;
  animation: anim-last-play-in 0.3s ease-out 0.2s both;
}

.detail-status-where {
  font-family: var(--font--league-gothic);
  padding-top: 1px;
  font-size: 18px;
  font-weight: 600;
  line-height: 1.1;
  color: var(--text-primary);
}

/* NBA: a floor shot that drew a foul (see _nbaStatusLineHtml) — the foul
   call's own wording, under the SCORE!/MISS! label, smaller and secondary
   so it reads as context rather than competing with it. */
.detail-status-foul {
  font-family: var(--font--league-gothic);
  padding-top: 1px;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  line-height: 1.1;
  color: var(--text-secondary);
}

/* Finished: leaders under each team (shares .detail-box with MLB). */
.detail-box-abbr-line {
  font-family: var(--font--league-gothic);
  padding-top: var(--padding-top--teko-font);
  font-size: 16px;
  font-weight: 600;
  color: var(--text-primary);
  text-transform: uppercase;
  margin-bottom: 2px;
}

.detail-box-table.detail-box-nfl {
  th, td { width: 22px; }
  .detail-box-name { width: auto; }
  & + .detail-box-nfl { margin-top: 8px; }

  /* C/ATT is the wide one. */
  &.detail-box-passing th:nth-child(2),
  &.detail-box-passing td:nth-child(2) { width: 34px; }
}

.detail-box-table.detail-box-nhl {
  & + .detail-box-nhl { margin-top: 8px; }

  /* Only 4 stat columns (SA/GA/SV/SV%) — same reasoning as .detail-box-nba
     below: the base 17px default (sized for the denser 6-column skater
     table) left the goalie labels too cramped. */
  &.detail-box-goalies th:not(.detail-box-name),
  &.detail-box-goalies td:not(.detail-box-name) { width: 24px; }

  /* SV% ( ".909") is the wide one. */
  &.detail-box-goalies th:last-child,
  &.detail-box-goalies td:last-child { width: 30px; }
}

/* Only 4 stat columns now (MIN/PTS/REB/AST) — the base 17px default
   (sized for a denser table) left no room between the header labels,
   which ran into each other with no gap at all. */
.detail-box-table.detail-box-nba {
  th:not(.detail-box-name), td:not(.detail-box-name) { width: 24px; }
}

/* MLB's replay: who's up, who's pitching, the count, and how the last
   plate appearance ended (see _mlbReplayMatchupHtml). The flexible item in
   .detail-body's own column (flex-basis 0, so it only ever takes space
   none of its siblings need — see the flex-shrink:0 guards scoped to MLB
   below), with min-height:0 so it can actually shrink down to whatever's
   left rather than holding out for its own natural size. container-type
   gives .detail-diamond's cqw/cqh something real to read off that
   resolved, bounded box. Centers the square (however big it ends up) in
   whatever space that leaves; the 24px bottom padding is reserved for the
   batter's own avatar (see .detail-matchup-player-batter), which sits
   right at the square's own bottom edge and needs room past it regardless
   of how much slack the centering leaves. */
.detail-matchup {
  position: relative;
  /* Below .detail-linescore-wrap's own z-index: 1 — see that rule — so the
     diamond's bled-out arc/foul lines (overflow: visible) paint behind it
     rather than on top where they overlap. */
  z-index: 0;
  container-type: size;
  flex: 1 1 0%;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 12px 16px 24px;
}

/* Pitcher and batter sit inside the diamond itself (center and home — see
   .detail-matchup-player-pitcher/-batter below), each just its own
   position; both share the same look — avatar, then its role/name/stat
   stack (.detail-matchup-player-info) below it. Now that .detail-diamond
   is bounded to the space it's actually got (see .detail-matchup), the
   batter's own stack has room to sit under its avatar without needing to
   spill past the square. */
.detail-matchup-player {
  position: absolute;
  width: 84px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.detail-matchup-player-info {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

/* Unlike the batter below, the name sits above the avatar here (shared
   markup with the batter — see _mlbMatchupPlayerHtml — this is a visual
   reorder, not a DOM change) — so centering this box as a whole (the
   batter's approach) would leave the avatar itself sitting off-center,
   pushed down by the name/gap above it. Each child is positioned
   independently instead: the avatar's own center is pinned to the mound
   (left/top: 50%, translate(-50%,-50%) same as the batter uses on its
   whole box), and the name sits directly above it (bottom anchored at
   the mound plus the avatar's own half-height, 20px — see
   .detail-matchup-avatar's fixed 40px size — plus the 2px gap the column
   layout used to provide). */
.detail-matchup-player-pitcher {
  left: 50%;
  /* 44%, not the diamond's true geometric center (50%) — same y the base
     markers themselves sit at (see _mlbSvgDiamondFieldHtml's `inset`:
     50-6=44), so the pitcher reads level with 1st/3rd instead of lower. */
  top: 44%;
  transform: translate(-50%, -50%);
  width: 84px;

  .detail-matchup-avatar {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    border: 1px solid #1f4d2e;
  }

  .detail-matchup-player-info {
    position: absolute;
    left: 50%;
    bottom: calc(50% + 20px + 2px);
    transform: translateX(-50%);
  }

  /* Same little pill as the batter's own name (.detail-matchup-player-batter)
     and a base runner's (.detail-base-runner) — it sits over the mound
     (.detail-mound) here, same reason those need a background: legible
     over whatever's behind it rather than relying on contrast that isn't
     guaranteed. */
  .detail-matchup-name {
    background: var(--color--glass-bg);
    padding-left: 4px;
    padding-right: 4px;
    padding-bottom: 1px;
    border-radius: 4px;
  }
}

/* Avatar and info positioned independently, same reasoning as the pitcher's
   own block above — so the avatar's own center always lands exactly at
   left:50%/top:86% (what .detail-batting-circle below is centered on too),
   regardless of the name's length, rather than the whole column's center
   (avatar + gap + name) landing there instead. Name stays below the
   avatar here (unlike the pitcher's, which sits above its own). */
.detail-matchup-player-batter {
  left: 50%;
  /* 8px above the vertex itself — matches the runner-marker overlay that
     takes over this exact spot the moment the batter takes off running
     (.detail-base-runner, a plain flex column rather than this one's own
     decoupled avatar/info — so its avatar centers a bit off the vertex
     instead of exactly on it). Nudged to meet that rather than the other
     way around, so the handoff between the two doesn't visibly jump. */
  top: calc(86% - 8px);
  transform: translate(-50%, -50%);
  width: 84px;

  /* Same stroke as the base paths (see _mlbSvgDiamondFieldHtml) so the
     batter reads as part of that design — standing at home — rather than
     a plain avatar dropped on top of it. */
  .detail-matchup-avatar {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    border: 1.5px solid #1f4d2e;
  }

  .detail-matchup-player-info {
    position: absolute;
    left: 50%;
    top: calc(50% + 20px + 2px);
    transform: translateX(-50%);
  }

  /* Same little pill as a base runner's own name (.detail-base-runner) — it
     sits over the batting circle (.detail-batting-circle, below), same
     reason that one needs a background: legible over whatever's behind it
     rather than relying on contrast that isn't guaranteed. */
  .detail-matchup-name {
    background: var(--color--glass-bg);
    padding-left: 4px;
    padding-right: 4px;
    padding-bottom: 1px;
    border-radius: 4px;
  }
}

/* The batter's left the batter's box the moment this lands — see
   _mlbDetailSituationHtml's own batterRunning check — so their own avatar/
   name fade out right here rather than sticking around at home alongside
   their own runner marker (_mlbRunnerMarkersHtml), which already covers
   the same spot from here on. */
.detail-matchup-player-batter.is-running {
  animation: mlb-run-fade-out 0.3s ease-out var(--duration--mlb-ball-flyby) forwards;
}

/* The mound/batting-circle, same sand color as the base paths (see
   _mlbSvgDiamondFieldHtml's sandFill) — plain siblings in .detail-diamond
   (see _mlbDetailSituationHtml) rather than a pseudo-element on the
   pitcher's/batter's own wrapper, so they can sit in the SAME stacking
   context as the ball (.detail-replay-ball-fly) and the avatars instead of
   being bundled inside the avatar's own local one: that's what lets the
   ball paint above these two but still under the avatars, by DOM order
   alone, with no z-index juggling needed anywhere. Centered on the same
   spot CSS already centers the pitcher/batter on (50%/44%, 50%/86%). */
.detail-mound {
  position: absolute;
  left: 50%;
  top: 44%; /* matches .detail-matchup-player-pitcher's own (see its comment) */
  width: 60px;
  height: 60px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: #C68B4A;
}

.detail-batting-circle {
  position: absolute;
  left: 50%;
  top: calc(86% - 8px); /* matches .detail-matchup-player-batter's own nudge — see its comment */
  width: 60px;
  height: 60px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: #C68B4A;
}

/* Home plate — sized as a % of .detail-diamond itself (same as the bases,
   drawn in the viewBox SVG above) so it scales with the field, but POSITIONED
   with the exact same `calc(86% - 8px)` as .detail-batting-circle rather than
   a second viewBox-unit guess at that nudge — see _mlbHomePlateHtml's own
   comment for why the two drifted apart before. */
.detail-home-plate {
  position: absolute;
  left: 50%;
  top: calc(86% - 8px);
  width: 6%;
  height: 5.5%;
  transform: translate(-50%, -50%);
  background: #fff;
  clip-path: polygon(0% 0%, 100% 0%, 100% 54.55%, 50% 100%, 0% 54.55%);
}

/* The pitcher's rubber — centered on .detail-mound's own spot (same
   left:50%/top:44%), sized off its 60px diameter: 30px wide (50% of the
   mound), 6px tall (that same width at a 5:1 ratio). Same plane as home
   plate above (a plain div, not part of the bases' shared SVG) so it
   paints in that same layer — above the mound's own dirt, below the
   pitcher's avatar. */
.detail-pitchers-rubber {
  position: absolute;
  left: 50%;
  top: 44%;
  width: 24px;
  height: 6px;
  transform: translate(-50%, -50%);
  background: #fff;
}

.detail-diamond.is-night .detail-matchup-player-pitcher .detail-matchup-avatar,
.detail-diamond.is-night .detail-matchup-player-batter .detail-matchup-avatar {
  border-color: var(--color--snug-black);
}

/* The pitch's own flight (.detail-replay-ball-fly, above) is drawn with a
   plain black stroke and no fill — invisible against the same dark mound
   at night. */
.detail-diamond.is-night .detail-replay-ball-fly svg circle,
.detail-diamond.is-night .detail-replay-ball-fly svg path {
  stroke: #fff;
}

.detail-matchup-avatar {
  width: 40px;
  height: 40px;
  border-radius: var(--border-radius--circular);
  object-fit: cover;
  background: var(--bg-out-dot);
}

.detail-last-play-role {
  font-family: var(--font--base);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

.detail-matchup-name {
  font-family: var(--font--base);
  font-size: 12px;
  font-weight: 500;
  color: var(--text-primary);
  letter-spacing: -0.2px;
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

.detail-matchup-stat,
.detail-pregame-pitcher-stat { /* one style for every under-the-name stat line */
  font-family: var(--font--base);
  font-size: 10px;
  color: var(--text-secondary);
  text-align: center;
}

/* Pregame: the record/ERA hugs the starter's name (the column's gap is 6px). */
.detail-pregame-pitcher .detail-pregame-pitcher-stat { margin-top: -4px; }

/* BOX | PLAY — the modal's own bottom row: since the modal is a fixed
   height and only .detail-body scrolls, this row (a flex-shrink:0 sibling
   of it) is always visible without needing to pin it itself. Each tab
   fills its own column (see .detail-tab), 3px in on every side from the
   modal's own edges — same inset as the O-Only zones above (see
   .detail-header-oonly-zones) and the modal's own padding-top, so the gap
   reads even all the way around the modal rather than tighter on one edge
   than another. Equal-width columns (not justify-content: space-evenly, which spaces by
   the *gaps* between labels), so the two tabs split the row evenly
   regardless of how much wider one label is than the other. Each tab's
   column is a fixed 1fr share of the row rather than sized to its own
   content, so the selected label can carry more font weight (below)
   without rendering wider and shifting the other one. */
.detail-tabs {
  position: relative;
  flex-shrink: 0;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  margin-left: 3px;
  margin-right: 3px;
  margin-bottom: 3px; /* same 3px inset as the sides (and the O-Only zones/modal's own padding-top) */
  height: 68px;
  margin-top: 12px;
  overflow-y: hidden;

  &[hidden] { display: none; }
}

/* Same raised glass material as the replay pills (highlight along the top
   edge, shadow along the bottom), squared off rather than a floating pill
   — except for the two outer bottom corners (see :first-child/:last-child
   below). The outer drop shadow lives on each tab individually here, not
   shared on the row (like the O-Only zones' own wrapper) — safe to do
   since exactly one tab is always selected, so there's never two adjacent
   *unselected* tabs' shadows to double up at the seam between them (see
   .on below, which drops this tab's own outer shadow — a pressed-in button
   doesn't cast one). A dark gray border frames each button all the way
   round, which also happens to supply the hairline between the two — they
   sit edge to edge, so the second one's own left border is that seam, with
   no extra rule needed for it. */
.detail-tab {
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(0, 0, 0, 0.15);
  padding-top: 12px;
  padding-bottom: 6px;
  font-family: var(--font--league-gothic);
  font-size: 22px;
  font-weight: 400;
  text-transform: uppercase;
  color: var(--text-primary);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  background-color: var(--color--glass-bg);
  background-image: var(--color--glass-highlight);
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.7),
              inset 0 -2px 3px rgba(0, 0, 0, 0.08),
              0 2px 4px rgba(0, 0, 0, 0.4);

  /* 29px: nested-corner formula (inner radius = outer radius − gap) off the
     modal's own 32px corner (see .game-detail-modal) and this row's 3px
     inset from it, so the two curves read as concentric/continuous. */
  &:first-child { border-bottom-left-radius: 29px; border-right: none; } /* the next tab's own left border is the seam between them — without this they'd double up to 2px there */
  &:last-child { border-bottom-right-radius: 29px; }

  /* Selected: pressed-in rather than a new color — same "inset track" look
     as .header-date-track (the groove the header's own date pill slides
     within): a dark translucent fill, a deep inset shadow along the top
     edge, just a hint of highlight along the bottom, no highlight
     gradient, and no outer drop shadow at all — a pressed-in button reads
     as recessed, not floating. Text weight stays the same as unselected —
     no bolding. */
  &.on {
    background-color: rgba(26, 26, 26, 0.05);
    background-image: none;
    box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4),
                inset 0 -1px 2px rgba(255, 255, 255, 0.1);
  }

  /* Unused while every sport's disabledTabs is always empty (see
     detailComposeBody) — kept for a tab that's on offer but not tappable. */
  &.is-disabled {
    opacity: 0.35;
    cursor: default;
  }
}

.detail-box-pending {
  padding: 24px 16px;
  text-align: center;
  font-family: var(--font--base);
  font-size: 12px;
  color: var(--text-secondary);
}

/* The box score's own muted callout (see detailMutedBoxCalloutHtml) —
   pinned dead center over .detail-body's own box (not the scrolling
   content, which it sits outside of in document order — see .detail-body's
   own position:relative), so it stays put as the faked/blurred box score
   underneath scrolls. A dimming scrim behind the card itself, same idea as
   .game-detail-overlay's own backdrop, so the (still-visible) fake stats
   read as secondary to it rather than competing for attention. */
.detail-muted-callout {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.55);
  z-index: 2;
}

/* Wraps the linescore + box content sitting underneath the callout (see
   detailComposeBody's own boxUnderlay) — the callout sits outside this
   wrapper in the markup, so it stays in full color while everything
   behind it desaturates. */
.detail-box-grayscale {
  filter: grayscale(1);
}

/* The header (team logos, the progress ring's green arc) lives in its own
   sibling box above .detail-body, shared across every tab — so it isn't
   inside .detail-box-grayscale above. :has() reaches across to it instead
   of threading a muted/box flag into detailHeaderHtml, and only matches
   while the box tab's own callout is actually in the DOM, so REPLAY (which
   never renders it) keeps its header in full color. */
.game-detail-modal:has(.detail-muted-callout) .detail-header {
  filter: grayscale(1);
}

/* Same glass-pill material as the rest of the app's floating cards
   (--color--glass-bg/--color--glass-highlight), rather than MLB's own
   plain white .detail-play-result card — this one floats over a whole
   tab, not a single diamond mid-replay. Lighter than --shadow--glass's
   own outer shadow (just the inset highlight/shade, no 0 3px 8px drop) —
   it's already sitting on the dimming scrim behind it, so a second heavy
   shadow on top of that read as too much. A light gray border carries
   most of the card's definition instead, since the shadow's been cut
   back. overflow:hidden so the unmute row below can be a flush
   full-width footer (border-radius lives here once, not duplicated on
   it) rather than a traditional pill button. Wider than a typical
   callout card (320px) so the body copy below the title doesn't wrap
   too tightly. */
.detail-muted-callout-card {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  width: min(320px, 100%);
  background-color: var(--color--glass-bg);
  background-image: var(--color--glass-highlight);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.7),
              inset 0 -1px 2px rgba(0, 0, 0, 0.05),
              0 2px 5px rgba(0, 0, 0, 0.12);
  border: 1px solid rgba(0, 0, 0, 0.12);
  border-radius: 18px;
  overflow: hidden;
}

/* Same Teko/uppercase language as every other callout title in the detail
   view (.detail-pitch-call, .detail-play-result-status) — tighter
   letter-spacing and a touch lighter weight than those, since this title
   now shares the card with a full sentence of body copy below it rather
   than standing alone. */
.detail-muted-callout-title {
  font-family: var(--font--league-gothic);
  padding: calc(20px + var(--padding-top--teko-font)) 36px 6px;
  font-size: 28px;
  font-weight: 500;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  color: var(--text-primary);
  text-align: center;
}

/* The plain-language explanation under the title — sans-serif/mixed-case
   like the rest of the app's prose (not Teko, which is reserved for
   display labels), so it reads as an explanatory sentence rather than
   another header. */
.detail-muted-callout-body {
  padding: 0 28px 20px;
  font-family: var(--font--base);
  font-size: 15px;
  line-height: 1.4;
  text-align: center;
  color: var(--text-secondary);
}

/* The one option under the title — a flush footer row (like a third
   .detail-tab) rather than a floating pill button, so it reads as the
   bottom half of the same card instead of a separate control dropped
   onto it. The hairline above is the same seam .detail-tab's own borders
   draw between tabs. Red fill since this is the one spot in the app that
   actually reverses a mute rather than just flicking a bug blind. */
.detail-muted-callout-unmute {
  width: 100%;
  border: none;
  border-top: 1px solid rgba(0, 0, 0, 0.15);
  padding: calc(10px + var(--padding-top--teko-font)) 28px 10px;
  font-family: var(--font--league-gothic);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: #fff;
  background-color: var(--out-dot-on-bg);
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.25),
              inset 0 -2px 3px rgba(0, 0, 0, 0.15);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.detail-last-play {
  /* Fills what's left under the matchup and centers the text in it — so it
     sits midway between the outs and the bottom of the modal. */
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 10px 20px;
  text-align: center;
  font-family: var(--font--base);
  font-size: 13px;
  line-height: 1.35;
  color: var(--text-secondary);
}

/* Football, MLB and NHL: the play's wording leads, with no "Last play"
   label (football's down and distance / NHL's SAVE!/SCORE! sits under it —
   see _footballHeldStatusLine / _nhlHeldStatusLine, which place it after). */
.game-detail-modal[data-sport="nfl"],
.game-detail-modal[data-sport="ncaaf"],
.game-detail-modal[data-sport="mlb"],
.game-detail-modal[data-sport="nhl"],
.game-detail-modal[data-sport="nba"] {
  .detail-last-play-role { display: none; }
  .detail-last-play {
    flex: 0 0 auto;
    padding: 12px 20px 0;
  }
}

/* .detail-matchup (above) is the one flexible, shrinkable child of
   .detail-body for MLB's replay tab — its only sibling there right now
   (the last-play sentence, result and score/half-end lines are all paused —
   see _mlbReplayMatchupHtml/_mlbParts), shared with other sports' own
   detail views (so scoped here rather than touched globally) and already
   sized to its own content; pinned so it never gives up any of that size
   to the diamond, which already has its own min-height:0 to do the
   shrinking instead. */
.game-detail-modal[data-sport="mlb"] {
  .detail-linescore-wrap {
    flex-shrink: 0;
  }
}

.game-detail-modal[data-sport="nfl"],
.game-detail-modal[data-sport="ncaaf"] {
  .detail-status-line { padding-top: 8px; }
}

.game-detail-modal[data-sport="nhl"],
.game-detail-modal[data-sport="nba"] {
  .detail-status-line { padding-top: 8px; }
}

.detail-last-play-stack {
  display: grid;
  justify-items: center;

  > * { grid-area: 1 / 1; }
}

.detail-last-play-text {
  font-family: var(--font--base);
  font-size: 15px; /* bigger and heavier than the 13px / regular text around it */
  font-weight: 700;
  line-height: 1.3;
  text-wrap: pretty;
  color: var(--text-primary);
}

/* A new last play slides in from the left while the old one fades out to the
   right (JS: _mlbSwapLastPlay; keep the 350ms in step with LAST_PLAY_SWAP_MS). */
.detail-last-play-text.is-entering { animation: anim-last-play-in 0.35s ease-out both; }
.detail-last-play-text.is-leaving  { animation: anim-last-play-out 0.35s ease-in both; }

/* MLB replay: the pitch that decided the at-bat carries its own result
   folded into the same step (see _mlbReplayStates) — a baseball flies from
   the pitcher's own spot (dead center) to the batter's (home), right there
   in .detail-diamond (see _mlbDetailSituationHtml/_mlbReplayMatchupHtml).
   Replaces the old row-based version (pitcher/batter side by side, with an
   is-rtl mirror for whichever team was batting) — redesigned from scratch
   for the new geometry, which is actually simpler: pitcher's always center
   and batter's always home regardless of which team's up, so the flight is
   always the same straight vertical line, no mirroring needed. Outcomes
   are being reintroduced one at a time (see the "big change" this came
   from) — foul balls first (.is-foul below); plain pitches just fade out
   on arrival for now. */
.detail-replay-ball-fly {
  position: absolute;
  left: 50%;
  top: 44%;
  width: 16px;
  margin-left: -8px;
  /* Pitcher's at 44% of the field (matching .detail-matchup-player-pitcher
     — see its own comment), home's at 86% (see .detail-diamond) — this box
     spans exactly that gap, so percentages inside it (the ball's own `top`
     keyframes below) land exactly on the pitcher/home regardless of how
     big the diamond actually renders. */
  height: 42%;
  pointer-events: none;
}
.detail-replay-ball-fly svg {
  position: absolute;
  left: 0;
  width: 16px;
  height: 16px;
  animation: anim-ball-flyby var(--duration--mlb-ball-flyby) linear forwards;
}
@keyframes anim-ball-flyby {
  from { top: 0%; transform: rotate(0deg); }
  85%  { opacity: 1; }
  to   { top: calc(100% - 16px); transform: rotate(360deg); opacity: 0; }
}

/* Foul ball: the same flight, but it lands fully opaque (no fade at 85%,
   unlike the plain pitch above) since there's more to come — see
   anim-foul-fly just below, which takes over right where this leaves off
   (--duration--mlb-ball-flyby in) and owns the fade-out itself instead. */
@keyframes anim-ball-flyby-land {
  from { top: 0%; transform: rotate(0deg); }
  to   { top: calc(100% - 16px); transform: rotate(360deg); }
}
.detail-replay-ball-fly.is-foul svg {
  animation: anim-ball-flyby-land var(--duration--mlb-ball-flyby) linear forwards, anim-foul-fly 0.45s ease-out var(--duration--mlb-ball-flyby) forwards;
}
/* Deflects off at a random angle (--foul-dx/--foul-dy, set inline per
   pitch — see _mlbReplayMatchupHtml) clear of the diamond's own foul
   lines, tumbling an extra half-turn as it fades. `transform` here fully
   replaces the flight's own (rather than building on it) since this
   animation owns the property outright from that point on — translate/rotate
   are relative to the ball's landed position/rotation, not a continuation
   of the flight's path. */
@keyframes anim-foul-fly {
  from { transform: translate(0, 0) rotate(360deg); opacity: 1; }
  to   { transform: translate(var(--foul-dx, 80px), var(--foul-dy, -60px)) rotate(540deg); opacity: 0; }
}

/* A fielded out: --hit-dx/--hit-dy (set inline per pitch — see
   _mlbHitDeflection) carry it to its own real, tracked location rather
   than a fixed or random one, same "lands opaque, then this takes over"
   relay as the foul above. Which of the four below applies comes straight
   off the feed's own trajectory code (G/L/F/P) — translate always moves
   straight there at a constant rate (no detour, no direction change —
   see the straight-liner note on .is-liner/_mlbReplayMatchupHtml's own
   isHit check); scale is what actually reads as "ground ball" vs. "fly
   ball" here — little hops for a grounder, a real rise-then-recede for
   anything hit in the air, steeper and closer for a pop up than a fly
   ball. is-inplay itself carries no look of its own (always paired with
   one of the four below — see _mlbReplayMatchupHtml). */

/* Ground ball: a few small scale dips along the way out — bounces,
   without ever leaving its own straight line to the fielder. */
.detail-replay-ball-fly.is-grounder svg {
  animation: anim-ball-flyby-land var(--duration--mlb-ball-flyby) linear forwards, anim-hit-grounder 0.5s linear var(--duration--mlb-ball-flyby) forwards;
}
@keyframes anim-hit-grounder {
  0%   { transform: translate(0, 0) scale(1) rotate(360deg); opacity: 1; }
  16%  { transform: translate(calc(var(--hit-dx) * 0.25), calc(var(--hit-dy) * 0.25)) scale(0.75) rotate(460deg); opacity: 1; }
  33%  { transform: translate(calc(var(--hit-dx) * 0.5), calc(var(--hit-dy) * 0.5)) scale(1) rotate(520deg); opacity: 1; }
  50%  { transform: translate(calc(var(--hit-dx) * 0.62), calc(var(--hit-dy) * 0.62)) scale(0.8) rotate(560deg); opacity: 1; }
  66%  { transform: translate(calc(var(--hit-dx) * 0.8), calc(var(--hit-dy) * 0.8)) scale(1) rotate(620deg); opacity: 1; }
  83%  { transform: translate(calc(var(--hit-dx) * 0.92), calc(var(--hit-dy) * 0.92)) scale(0.85) rotate(670deg); opacity: 1; }
  100% { transform: translate(var(--hit-dx), var(--hit-dy)) scale(1) rotate(720deg); opacity: 0; }
}

/* Line drive: fast and flat, no scale at all — just a straight, quick
   shot out (same treatment every actual hit uses — see
   _mlbReplayMatchupHtml's isHit check). Its own 0.35s here is a fixed
   duration, not tied to --duration--mlb-ball-flyby — just a coincidence
   that they currently match. */
.detail-replay-ball-fly.is-liner svg {
  animation: anim-ball-flyby-land var(--duration--mlb-ball-flyby) linear forwards, anim-hit-liner 0.35s linear var(--duration--mlb-ball-flyby) forwards;
}
@keyframes anim-hit-liner {
  from { transform: translate(0, 0) rotate(360deg); opacity: 1; }
  to   { transform: translate(var(--hit-dx), var(--hit-dy)) rotate(650deg); opacity: 0; }
}

/* Fly ball: scales up as it "rises" (reads as coming up toward the
   viewer), then back down past its own normal size as it recedes out to
   the fielder — hangs in the air the longest of the four. */
.detail-replay-ball-fly.is-flyball svg {
  animation: anim-ball-flyby-land var(--duration--mlb-ball-flyby) linear forwards, anim-hit-flyball 0.6s linear var(--duration--mlb-ball-flyby) forwards;
}
@keyframes anim-hit-flyball {
  0%   { transform: translate(0, 0) scale(1) rotate(360deg); opacity: 1; }
  45%  { transform: translate(calc(var(--hit-dx) * 0.45), calc(var(--hit-dy) * 0.45)) scale(1.9) rotate(630deg); opacity: 1; }
  100% { transform: translate(var(--hit-dx), var(--hit-dy)) scale(0.55) rotate(900deg); opacity: 0; }
}

/* Pop up: the same rise-then-recede as the fly ball, just a bigger scale
   swing (it's reading as closer/more straight-up) over less distance. */
.detail-replay-ball-fly.is-popup svg {
  animation: anim-ball-flyby-land var(--duration--mlb-ball-flyby) linear forwards, anim-hit-popup 0.55s linear var(--duration--mlb-ball-flyby) forwards;
}
@keyframes anim-hit-popup {
  0%   { transform: translate(0, 0) scale(1) rotate(360deg); opacity: 1; }
  50%  { transform: translate(calc(var(--hit-dx) * 0.4), calc(var(--hit-dy) * 0.4)) scale(2.3) rotate(630deg); opacity: 1; }
  100% { transform: translate(var(--hit-dx), var(--hit-dy)) scale(0.5) rotate(900deg); opacity: 0; }
}

/* The call itself (see _mlbReplayMatchupHtml) — a plain ball/strike/foul
   (nothing that ends the at-bat, which gets its own card instead — see
   .detail-play-result below). Pinned to .detail-matchup's own bottom-right
   corner, mirroring .detail-count-board's bottom-left one — a sibling of
   .detail-diamond (not nested inside it) for the same reason: that corner's
   sized off .detail-matchup's own width, not the (often narrower, centered)
   diamond square. Same Teko treatment as the B/S/O board next to it. Fades
   in at the same mark as everything else this pitch reveals (reusing
   anim-finals-fade-in like the count board's own crossfade does — see
   --duration--mlb-ball-flyby), and just stays up while the step does. */
.detail-pitch-call {
  position: absolute;
  bottom: 0;
  right: 16px;
  white-space: nowrap;
  font-family: var(--font--league-gothic);
  padding-top: var(--padding-top--teko-font);
  font-size: 28px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-primary);
  opacity: 0;
  pointer-events: none;
}
.detail-pitch-call.is-entering { animation: anim-finals-fade-in var(--duration--mlb-reveal-fade) ease-out var(--duration--mlb-ball-flyby) both; }

/* Backwards K: a called (looking) strikeout's own scorebook mark — same
   letter, just mirrored, rather than hunting for a Unicode glyph no font
   can be trusted to render consistently. */
.detail-pitch-call.is-looking { transform: translate(-50%, -50%) scaleX(-1); }

/* An at-bat-ending play's own card (see _mlbReplayMatchupHtml) — same
   reveal moment as the plain .detail-pitch-call it replaces, just dead
   center on the diamond instead of pinned between pitcher/batter, and
   wide enough to hold the feed's own prose underneath the bare call. */
.detail-play-result {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  background: rgba(255, 255, 255, 0.8);
  border-radius: 10px;
  padding: 18px 14px;
  opacity: 0;
  pointer-events: none;
  text-align: center;
}
.detail-play-result.is-entering { animation: anim-finals-fade-in var(--duration--mlb-reveal-fade) ease-out var(--duration--mlb-text-delay) both; }

/* The status (title) slides/fades in from the left first — same
   anim-last-play-in treatment as NFL/NHL/NBA's own last-play text — then
   the feed's prose follows a beat later (see .detail-play-result-text),
   both starting once the ball's landed plus a little breathing room
   (--duration--mlb-text-delay). */
.detail-play-result-status {
  font-family: var(--font--league-gothic);
  padding-top: var(--padding-top--teko-font);
  font-size: 28px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-primary);
  opacity: 0;
}
.detail-play-result-status.is-entering { animation: anim-last-play-in 0.35s ease-out var(--duration--mlb-text-delay) both; }
.detail-play-result-status.is-looking { display: inline-block; }
/* Backwards K: same mirrored scorebook mark as .detail-pitch-call.is-looking,
   just baked into its own slide-in keyframe so the entrance's translateX
   doesn't clobber the static scaleX(-1) the way a plain transform rule
   would. */
.detail-play-result-status.is-entering.is-looking { animation: anim-last-play-in-mirror 0.35s ease-out var(--duration--mlb-text-delay) both; }
@keyframes anim-last-play-in-mirror {
  from { opacity: 0; transform: translateX(-24px) scaleX(-1); }
  to   { opacity: 1; transform: translateX(0) scaleX(-1); }
}

.detail-play-result-text {
  font-family: var(--font--base);
  font-size: 13px;
  font-weight: 500;
  color: var(--text-secondary);
  line-height: 1.3;
  opacity: 0;
}
/* Same slide-from-left as the status above, just starting a beat after it
   (the status's own delay plus a further 0.2s stagger) so the title reads
   first. */
.detail-play-result-text.is-entering { animation: anim-last-play-in 0.3s ease-out calc(var(--duration--mlb-text-delay) + 0.2s) both; }

/* The half-inning's own turn (Mid/End/Final — see inningTurn in
   _mlbReplayMatchupHtml) — nested inside .detail-play-result so it flies in
   directly under the status/prose above it regardless of how tall that
   card ends up (a plain sibling at a fixed offset would either clip a long
   line of prose or leave a gap under a short one), `top: 100%` putting it
   right past the card's own bottom edge no matter which. Its own small
   pill, not part of the card's light-glass background — reversed instead
   (dark bg, light text) so it still reads as a second, distinct beat
   rather than more of the same card. */
.detail-inning-turn {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-top: 8px;
  width: fit-content;
  white-space: nowrap;
  background: rgba(0, 0, 0, 0.8);
  color: #fff;
  font-family: var(--font--league-gothic);
  padding-top: calc(var(--padding-top--teko-font) + 3px);
  padding-bottom: 3px;
  padding-left: 12px;
  padding-right: 12px;
  font-size: 18px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  border-radius: 6px;
  opacity: 0;
  pointer-events: none;
}
/* Same slide-from-left as the title/prose above, just the last of the
   three to land — a further 0.2s behind the prose's own delay (rather than
   landing at the same time as it) so it reads as a distinct third beat.
   Its own keyframe, not plain anim-last-play-in: that one's `to` step ends
   on `transform: translateX(0)`, which would permanently overwrite (not
   combine with) this element's own static `translateX(-50%)` centering for
   as long as `both` keeps the animation's end state applied — the exact
   problem anim-last-play-in-mirror above already exists to dodge for the
   backwards-K case, just needing its own variant here since centering
   (-50%) isn't the same offset as that mirror's own scaleX(-1). */
.detail-inning-turn.is-entering { animation: anim-inning-turn-in 0.3s ease-out calc(var(--duration--mlb-text-delay) + 0.4s) both; }
@keyframes anim-inning-turn-in {
  from { opacity: 0; transform: translateX(calc(-50% - 24px)); }
  to   { opacity: 1; transform: translateX(-50%); }
}

/* ...and, once the ball's through, the B/S/O number that actually changed
   shows it — nothing here is ever hidden first: it sits at its old value
   the whole time the ball's crossing, then crossfades to the new one once
   it lands. Base runners are their own separate, independently animated
   markers now (see _mlbRunnerMarkersHtml) rather than a fade tied to this
   same reveal. */

/* A runner with nowhere to go this pitch (an out, a pickoff — see
   _mlbRunnerMarkersHtml) fades out right where it was instead of traveling
   anywhere; one that scores fades out too, once its own run home is done,
   since nobody actually stands on home plate. Shared by both the marker's
   own SVG <animate> and the name's CSS animation referencing it. */
@keyframes mlb-run-fade-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

@keyframes anim-last-play-in {
  from { opacity: 0; transform: translateX(-24px); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes anim-last-play-out {
  from { opacity: 1; transform: translateX(0); }
  to   { opacity: 0; transform: translateX(24px); }
}

.sport-header {
  font-family: var(--font--league-gothic);
  font-size: var(--font-size--subheading);
  text-transform: uppercase;
  color: var(--text-primary);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;

  .sport-header-label {
    font-weight: 500;
    padding-top: var(--padding-top--teko-font);
  }

  .sport-header-line {
    flex: 1;
    height: 1px;
    background: rgba(0, 0, 0, 0.15);
  }
}

.compact-section {
  display: flex;
  flex-direction: column;
  max-width: 100%;
  margin: 0 auto;

  /* Driven from the section rather than each tile: the poll's reconcile
     resets tile attributes every time, which would cut a tile-level class
     (and its animation) short. Durations match FINALS_FADE_MS in global.js. */
  &.finals-fading-in  > .compact-tile.compact-final { animation: anim-finals-fade-in 0.3s ease both; }
  &.finals-fading-out > .compact-tile.compact-final { animation: anim-finals-fade-out 0.2s ease both; }
}

@keyframes anim-finals-fade-in  { from { opacity: 0; } to { opacity: 1; } }
@keyframes anim-finals-fade-out { from { opacity: 1; } to { opacity: 0; } }

/* The wavy divider under the last finished tile — or, while the finished
   games are folded away, under the "N completed games" button below. Same
   line either way, so it reads as the one divider between finished and the
   rest whether or not they're showing. */
.compact-tile.last-final::after,
.finals-toggle::after {
  content: '';
  position: absolute;
  bottom: -10px;
  left: 0;
  right: 0;
  height: 4px;
  /* The wave is a mask over a solid colour, so it can be black or white (see
     _updateDividerContrast) like the button above it. */
  background: var(--color--snug-black);
  -webkit-mask-image: var(--image--wavy-line);
  mask-image: var(--image--wavy-line);
  -webkit-mask-repeat: repeat-x;
  mask-repeat: repeat-x;
  -webkit-mask-size: 20px 4px;
  mask-size: 20px 4px;
  pointer-events: none;
}

.compact-tile.last-final.divider-light::after,
.finals-toggle.divider-light::after { background: #fff; }

/* Collapsed stand-in for a run of finished games (see _syncFinalsCollapse):
   a full-width row (so the wavy divider can span the list like it does
   under a tile) holding the button. Sized like a tile's spacing — a little
   above, and the same 16px below as .last-final so the divider sits the same
   distance from what follows. It fades in whenever it goes from display:none
   to shown, via the animation. */
.finals-toggle {
  position: relative;
  display: flex;
  justify-content: center;
  margin: 6px 0 16px;
  /* On the row rather than more margin so the wavy divider (anchored to the
     row's bottom edge) moves down with it — more room between the button and
     the line, not just below the line. */
  padding-bottom: 10px;
  animation: anim-finals-fade-in 0.25s ease both;
  transition: opacity 0.2s ease;

  &.is-fading { opacity: 0; pointer-events: none; }

  /* Today's board: a backdrop of the day's finished teams' logos rises up
     behind the button toward the header instead of empty space (see
     .finals-wallpaper for the grid itself, which does its own clipping —
     not this element, so its wavy divider ::after below isn't cut off too).
     Full-bleed edge to edge (same trick as #all-view-sky/#all-view-stars) so
     the mesh and the wavy divider (::after, which spans this element's own
     width) both run the full 100vw instead of stopping at .app's side
     padding. The button sits dead center over it (see
     .has-wallpaper .finals-toggle-btn below) instead of below it. */
  &.has-wallpaper {
    display: block;
    width: calc(100% + 2 * var(--padding--app-sides));
    margin-left: calc(-1 * var(--padding--app-sides));
    margin-right: calc(-1 * var(--padding--app-sides));
  }

  /* Expanded: same as the plain toggle — hides the mesh and button entirely
     so the actual finished tiles show, same as before .has-wallpaper existed.
     After (source order matters: both this and .has-wallpaper are one class
     each, so whichever comes last wins when an element has both). */
  &.is-hidden { display: none; }
}

.finals-wallpaper {
  height: 20vh;
  overflow: hidden;
  display: flex;
  flex-direction: column-reverse; /* first row (DOM order) sits at the bottom, nearest the button */
  align-items: center;
  gap: 8px;
  /* Fades into the clipped top edge instead of ending on a hard line — stays
     solid for the bottom 90%, so only a thin sliver at the top fades. */
  -webkit-mask-image: linear-gradient(to top, #000 90%, transparent 100%);
  mask-image: linear-gradient(to top, #000 90%, transparent 100%);
}

.finals-wallpaper-row {
  display: flex;
  justify-content: center;
  gap: 8px;

  img {
    width: 32px;
    height: 32px;
    object-fit: contain;
    opacity: 0.4;
  }
}

/* Styled like a finished game's bug (see .compact-final .game-score .bug):
   transparent, a black border, and the feathered white wash bleeding out
   from behind it via ::before instead of a solid/glass fill. Just with a
   label in it. */
.finals-toggle-btn {
  min-width: var(--width--game-score-box);
  height: calc(var(--height--game-score-box) - 2px);
  padding: var(--padding-top--teko-font) 24px 0;
  appearance: none;
  -webkit-appearance: none;
  border: 1px solid var(--color--snug-black);
  border-radius: 24px;
  background: transparent;
  color: var(--text-primary);
  font-family: var(--font--league-gothic);
  font-size: 20px;
  font-weight: 500;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;

  /* Over the dark part of the sky (see _updateFinalsToggleContrast). */
  &.is-light {
    color: #fff;
    border-color: #fff;
  }
}

/* Dead center over the wallpaper — a solid-ish white wash (rather than the
   plain transparent button) so the label still reads over the logos
   underneath it. */
.has-wallpaper .finals-toggle-btn {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: rgba(255, 255, 255, 0.75);

  /* The wash is always light regardless of the sky, so ignore the sky-driven
     .is-light flip (see _updateFinalsToggleContrast) that would otherwise
     put white text on it. */
  &.is-light {
    color: var(--text-primary);
    border-color: var(--color--snug-black);
  }
}

/* Folds the finished games back up — bottom right of the last finished
   tile, just above its wavy divider. A small round glass button rather than
   a bare icon so it reads against any sky color. */
.finals-chevron {
  position: absolute;
  right: 6px;
  bottom: 2px;
  z-index: 3;
  width: 36px;
  height: 36px;
  padding: 0;
  appearance: none;
  -webkit-appearance: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border-bug);
  border-radius: var(--border-radius--circular);
  background-color: var(--color--glass-bg);
  background-image: var(--color--glass-highlight);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.7),
              inset 0 -2px 3px rgba(0, 0, 0, 0.08);
  color: var(--text-primary);
  cursor: pointer;

  svg { width: 18px; height: 18px; }
}

::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
}

::view-transition-group(site-header),
::view-transition-group(site-footer) {
  z-index: 100;
}

::view-transition-old(site-header),
::view-transition-new(site-header),
::view-transition-old(site-footer),
::view-transition-new(site-footer) {
  animation: none;
}

.bug {
  position: relative;
  touch-action: pan-y;
  box-sizing: border-box;

  @media (hover: hover) and (pointer: fine) {
    cursor: grab;
  }

  height: calc(var(--height--game-score-box) - 2px);
  display: flex;
  align-items: center;
  gap: 1px;
  background: var(--bg-card);
  border-radius: 24px;
  /* Always white, every state — upcoming, in-progress, and final alike
     (see the removed .compact-final/.compact-live overrides this replaced). */
  border: 1px solid var(--border-bug);
  padding: 4px 2px;
  width: 100%;
  /* A tiny press-down, same as the header's own date pill — held for the
     whole time a finger's down, drag included, not just a plain tap. The
     shrink itself should read instantly; only the release back to full
     size (the base transition below, picked up once neither state below
     still matches) eases out. :active alone can drop mid-drag on some
     mobile browsers once the touch is read as a pan rather than a tap
     (even with setPointerCapture) — .is-dragging (toggled in global.js's
     own _scrubStart/_scrubEnd/_scrubCancel) is the reliable fallback that
     keeps it held down for a real scrub regardless. */
  transition: transform 0.15s ease;

  &:active, &.is-dragging {
    transition: transform 0.04s ease;
    transform: scale(0.95);
  }

  .my-final { background-color: transparent; }

  .bug-team {
    display: flex;
    align-items: center;
    gap: 1px;
    flex: 1;
    min-width: 0;

    &.bug-team-away { justify-content: flex-start; }
    &.bug-team-home { justify-content: flex-end; }
  }

  /* College ranking, a superscript centered over the top of the pregame
     abbreviation (the only time the bug shows one). Out of the flow, so the
     abbreviation stays where it is. */
  .bug-rank {
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    margin-bottom: -10px;
    font-family: var(--font--base);
    font-size: 10px;
    font-weight: 600;
    line-height: 1;
    letter-spacing: -0.5px;
    color: var(--text-secondary);
    pointer-events: none;
  }

  .bug-logo-circle {
    position: relative;
    width: 34px;
    height: 34px;
    border-radius: var(--border-radius--circular);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    border-color: var(--text-primary);
    box-shadow: none;
    overflow: hidden;

    .bug-team.is-fav-team & {
      scale: 1.05;
      rotate: var(--rotate--fav-logo);
      border-radius: var(--border-radius--circular);
      box-shadow: rgba(0, 0, 0, 0.3) 0 2px,
                  rgba(0, 0, 0, 0.2) 0 4px,
                  rgba(0, 0, 0, 0.1) 0 6px,
                  rgba(0, 0, 0, 0.05) 0 8px,
                  rgba(0, 0, 0, 0.025) 0 10px;
    }

    .bug-team-away & {
      border-top-right-radius: 0;
      border-bottom-right-radius: 0;
    }

    .bug-team-home & {
      border-top-left-radius: 0;
      border-bottom-left-radius: 0;
    }

    .postseason-game & {
      background-color: transparent;
      border-radius: 0;
    }

    img {
      position: relative;
      z-index: 1;
      width: 29px;
      height: 29px;
      object-fit: contain;
    }
  }

  .bug-score {
    font-family: var(--font--league-gothic);
    padding-top: var(--padding-top--teko-font-bug);
    font-size: 24px;
    letter-spacing: normal;
    font-weight: 500;
    flex-shrink: 0;
    width: 32px;
    text-align: center;
    position: relative;

    &.bug-score-away { text-align: right; }
    &.bug-score-home { text-align: left; }
    &.bug-score-pregame { text-align: center; font-weight: 400; }

    /* O-Only, honored on the board too (see boardOOnlyBlurred in
       global.js): the locked-out team's real score just goes blank — the
       real digits stay in the DOM (bugScoreHtml), this just hides them,
       so a board drag's own count-up (_boardFlickerScores, which writes
       straight into the digits span regardless) still reads blank the
       whole time it's moving, not just on a plain render. A thick sharpie
       scribble (mask-image, same recoloring trick the winner triangle
       uses — background:var(--text-primary) through an SVG shape) marks
       the blank spot instead of leaving it empty — anchored to whichever
       edge that side's own digits actually sit against (away text-aligns
       right, home left — see .bug-score-away/.bug-score-home above),
       same as the old blurred-"8" treatment did, rather than centered in
       the box: centering it there instead sat noticeably off from where
       the real digits themselves would be. */
    &.bug-score-oonly {
      color: transparent !important;

      &::before {
        content: '';
        position: absolute;
        top: 50%;
        width: 18px;
        height: 20px;
        transform: translateY(-50%);
        background: var(--text-primary);
        -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 90 100'%3E%3Cpath d='M10,75 L24,25 L38,75 L52,25 L66,75 L80,25' transform='rotate(45 45 50)' fill='none' stroke='black' stroke-width='20' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
        mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 90 100'%3E%3Cpath d='M10,75 L24,25 L38,75 L52,25 L66,75 L80,25' transform='rotate(45 45 50)' fill='none' stroke='black' stroke-width='20' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
        -webkit-mask-repeat: no-repeat;
        mask-repeat: no-repeat;
        -webkit-mask-size: contain;
        mask-size: contain;
        pointer-events: none;
      }
      &.bug-score-away::before { right: 0; }
      &.bug-score-home::before { left: 0; }
    }
  }

  /* Finished games only (see bugAbbrHtml): the team abbreviation back
     between its logo and score — same look (and darkness) as the score
     next to it, just a little more air off the logo than the bug's own
     1px gap gives every other pair. margin, not padding: the box itself
     is a fixed 32px (border-box, same as .bug-score), so padding only
     shrinks the content area and re-centers the text inside that same
     32px — it never actually moves the box away from the logo next to
     it the way margin does. */
  .bug-abbr {
    font-family: var(--font--league-gothic);
    padding-top: var(--padding-top--teko-font-bug);
    font-size: 20px;
    letter-spacing: normal;
    font-weight: 400;
    flex-shrink: 0;
    width: 32px;
    text-align: center;

    &.bug-abbr-away { margin-left: 13px; }
    &.bug-abbr-home { margin-right: 13px; }
  }

  .bug-score-value {
    position: relative;
    display: inline-block;
  }

  .bug-score.winner .bug-score-value::before {
    content: '';
    position: absolute;
    top: -6px;
    left: 50%;
    transform: translateX(-50%);
    width: 8px;
    height: 6px;
    background: var(--text-primary);
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 6'%3E%3Cpolygon points='0,0 8,0 4,6'/%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 6'%3E%3Cpolygon points='0,0 8,0 4,6'/%3E%3C/svg%3E");
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: contain;
    mask-size: contain;
  }

  /* Held back while the score is still counting up to it (see
     _boardFlickerScores in global.js, which toggles this) — showing the
     winner's triangle before the number's actually finished climbing
     would give the result away mid-count. */
  .bug-score.winner.is-counting .bug-score-value::before {
    display: none;
  }

  .bug-possession {
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    gap: 1px;
    margin-bottom: -7px;
    font-size: 7px;
    font-weight: 500;
    line-height: 1;
    letter-spacing: -0.2px;
    color: var(--text-secondary);
    white-space: nowrap;
  }

  .bug-football-icon {
    width: 13px;
    height: 9px;
    flex-shrink: 0;
  }

  .bug-possession.in-redzone .bug-football-icon { animation: anim-football-buhbump 1.6s ease-in-out infinite; }

  .bug-center {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1px;
    flex-shrink: 0;
    width: 40px;
    height: 28px;
    border-radius: var(--border-radius--circular);
    box-shadow: none;

    &.bug-center-pregame {
      width: 14px;

      .bug-status { font-size: 0; }
    }

    .bug-status {
      font-size: 12px;
      letter-spacing: -0.5px;
      font-variant-numeric: normal;
      font-weight: 600;
      text-align: center;
      white-space: normal;
      line-height: 1.1;

      .final-main {
        font-weight: 650;
      }

      .final-suffix {
        display: block;
        font-size: 0.7em;
        font-weight: 600;
        line-height: 1;
      }
    }
  }

  .bug-pregame-marker {
    display: block;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 14px;
    color: var(--text-secondary);
    pointer-events: none;
  }

  .bug-mlb-situation {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    margin-top: 0;
    gap: 1px;

    .mlb-diamond { width: 22px; height: 22px; }
  }

  .bug-mlb-diamond-wrap {
    position: relative;

    .mlb-inning-indicator {
      position: absolute;
      bottom: 0;
      left: 50%;
      transform: translateX(-50%);
      font-size: 10px;
      font-weight: 600;
      line-height: 1;
      white-space: nowrap;
    }
  }

  .situation-dots > * {
    width: 6px;
    height: 6px;
  }
}

.compact-tile {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  min-height: calc(var(--height--game-score-box) + 2px);
  padding: 0 var(--padding--left-right-compact-tile);
  z-index: 0;
  container-type: inline-size;

  &.tile-dark-bg {
    --color--wick: rgba(255, 255, 255, 0.85);
    --color--wick-solid: rgb(255, 255, 255);
    --color--upcoming-time-text: var(--bg-page);
  }

  .favorite-track {
    position: absolute;
    top: 0;
    bottom: 0;

    left: calc(-1 * var(--padding--app-sides));
    right: calc(-1 * var(--padding--app-sides));
    z-index: -2;
    background: var(--color--favorite-track);
    pointer-events: none;
  }

  &.compact-final {

    /* The centered, widened (room for the paired abbreviation — see
       bugAbbrHtml) layout only applies once the real final score is
       actually showing (.is-live — the viewer dragged to the dot, or
       tapped "Unmute?"). Short of that (the default muted state, or
       pinned to some earlier moment), a final tile's pill looks exactly
       like an in-progress one: parked at its own position on the track,
       the base .game-score rule above. */
    &.is-live .game-score {
      left: 50%;
      overflow: visible;
      transform: translate(-50%, -50%);
      width: calc(var(--width--game-score-box) + 2 * (32px + 13px));
    }

    /* Solid white once the real final score is actually showing (.is-live).
       Short of that, a final tile keeps the exact same solid/glass
       background a live tile has (see &.compact-final:not(.is-live) added
       to that rule below), matching the border's own "keep it identical to
       in-progress until revealed" treatment above. */
    &.is-live .game-score .bug {
      background: #fff !important;
    }

    /* Postseason finals keep their gold diagonal instead of the flat white
       above — same gradient the live/upcoming bug gets (see bugGradient in
       global.js; it's a fixed string, not per-game, so it's safe to repeat
       here), just over solid white instead. The gradient's own transparent
       bands are the diagonal stripe pattern itself, not bg transparency —
       the base underneath them is opaque. */
    &.postseason-game.is-live .game-score .bug {
      background: linear-gradient(
        -15deg,
        transparent 0% 7%,
        rgba(255, 190, 0, 0.5) 7% 22%,
        rgba(255, 210, 40, 0.45) 22% 37%,
        rgba(255, 230, 100, 0.4) 37% 52%,
        transparent 52% 100%
      ), #fff !important;
    }

    /* No press-down shrink once a final's fully revealed — there's nothing
       left to drag or tap-and-hold for (tapping it does nothing; see
       _scrubStart's own revealedFinal check), so it shouldn't look pressable
       either. Overrides .bug's own base :active rule below via specificity. */
    &.is-live .game-score .bug:active {
      transform: none;
    }

    &.last-final {
      margin-bottom: 16px;
    }
  }

  /* Once a game's begun (live, or final but not yet revealed) its pill is
     drag-to-scrub-able — native vertical pan is turned off here (overriding
     .bug's own pan-y) so the browser never claims the touch for a page
     scroll based on an early wobble before the drag gesture's own direction
     check in global.js has even run. A pregame tile has no track to drag
     along, so it keeps pan-y — resting a finger there to scroll the board
     still works as expected. Same for a revealed final (.is-live) — its
     wide pill has nothing left to scrub to (see _scrubStart's own
     revealedFinal check), so it keeps pan-y too. */
  &.compact-live .game-score .bug,
  &.compact-final:not(.is-live) .game-score .bug {
    touch-action: none;
  }

  &.compact-upcoming .game-score .bug,
  &.compact-live .game-score .bug,
  &.compact-final:not(.is-live) .game-score .bug {
    background-color: var(--color--glass-bg);
    background-image: var(--color--glass-highlight);
    backdrop-filter: blur(24px);
    -webkit-backdrop-filter: blur(24px);
    /* Inset highlights only, no outer drop shadow. */
    box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.7),
                inset 0 -2px 3px rgba(0, 0, 0, 0.08);
  }

  > * {
    display: flex;
    align-items: center;
    white-space: nowrap;
  }

  .game-score {
    position: absolute;
    top: 50%;
    /* Slides into the remaining track space proportionally to --pill-pct
       (0 at rest, up to the dot's own --progress-pct once live) — the
       translateX counters left by the pill's own width so it reaches the
       track's right edge exactly as --pill-pct reaches 100, same trick as
       the dot/line below, just on the pill itself. Decoupled from
       --progress-pct (the track/dot's real, unconditional elapsed time) so
       a parked pill can sit still while the game keeps playing. */
    left: calc(var(--pill-pct, 0) * 1%);
    transform: translate(calc(var(--pill-pct, 0) * -1%), -50%);
    width: var(--width--game-score-box);
    z-index: 1;
    overflow: hidden;
    align-items: flex-start;
    justify-content: flex-start;
    border-radius: 24px;
    transition: left 0.6s ease;
  }

  /* .game-score clips to the pill's own rounded shape (overflow: hidden,
     see the base rule above) — lifted once live in case anything else
     still wants to extend past that edge. The border itself is just the
     pill's own base rule now (always white, every state — see .bug). */
  &.is-live .game-score {
    overflow: visible;
  }

  /* Shown once a game's begun — live or final — upcoming tiles hide it
     entirely (see &.compact-live/&.compact-final below). ::before is the
     lit portion — the same dark, inset-shadowed groove look as
     .header-date-track (the slot the header's own date pill slides
     within), just growing in length instead of staying a fixed size, out
     from the pill's own right edge as the game clock runs (see
     --progress-pct, set on .compact-tile itself so this sibling of
     .game-score can read it too). Sits BEHIND the pill (z-index 0, same as
     this outer element — .game-score's own z-index is 1) so its box-shadow
     never paints over the pill's edges. The pill itself (.game-score,
     above) is draggable along this same track once it exists — see the
     board's drag-to-scrub gesture in global.js. */
  .compact-track {
    display: none;
    position: absolute;
    top: 50%;
    left: 0;
    right: 0;
    height: 0;
    z-index: 0;
    pointer-events: none;

    &::before {
      content: '';
      position: absolute;
      top: 50%;
      /* Drawn from the track's true left edge (not the pill's right edge)
         so it runs the full width behind the resting pill too — only the
         growth RATE/bound still counts from the pill's own right edge (same
         width this always was, just with that fixed pill-width extent
         added back in so the bar reaches all the way there from 0). */
      left: 0;
      width: calc(var(--width--game-score-box) + (100% - var(--width--game-score-box)) * var(--progress-pct, 0) / 100);
      /* Slightly less than the pill's own height (44px = game-score-box
         minus 2px) — same couple-px recess .header-date-track has under
         its own, taller title pill. */
      height: calc(var(--height--game-score-box) - 6px);
      transform: translateY(-50%);
      border-radius: var(--border-radius--circular);
      background: rgba(26, 26, 26, 0.1);
      box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4),
                  inset 0 -1px 2px rgba(255, 255, 255, 0.1);
      transition: width 0.6s ease;
    }
  }

  &.compact-live .compact-track,
  &.compact-final .compact-track {
    display: block;
  }

  /* Once a final game's actually revealed (swooshed to the end), there's
     nothing left to track or decide — drop the track entirely and let the
     pill sit alone with the real final score, same as a plain finished
     tile always looked before any of this. */
  &.compact-final.is-live .compact-track {
    display: none;
  }


  .compact-upcoming-time {
    position: absolute;
    top: 50%;
    right: 0;
    transform: translateY(-50%);
    z-index: 2;
    font-family: var(--font--base);
    font-size: 14px;
    font-weight: 600;
    letter-spacing: -0.5px;
    color: var(--color--upcoming-time-text);
    white-space: nowrap;
    pointer-events: none;
    padding: 4px 0 4px 6px;
    width: var(--width--upcoming-time);
    justify-content: flex-end;
  }

  /* Stops short of the tile's true right edge by the upcoming-time badge's
     own width, so the wick never runs underneath it. */
  .compact-fuse {
    position: absolute;
    top: 50%;
    left: var(--width--game-score-box);
    right: var(--width--upcoming-time);
    height: 1px;
    transform: translateY(-50%);
    z-index: 1;
    pointer-events: none;
  }

  .compact-fuse--unlit {
    .compact-fuse-bar {
      background: rgba(0, 0, 0, 0.15);
    }
  }

  .compact-fuse-bar {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: var(--color--wick);
    animation: anim-fuse-burn 3600s linear forwards;
  }

  .compact-fuse-spark {
    position: absolute;
    top: 50%;
    left: 100%;
    width: 0;
    height: 0;

    &::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 2px;
      height: 2px;
      transform: translate(-50%, -50%);
      background: var(--color--wick);
      border-radius: 50%;
      box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.5);
    }

    i {
      position: absolute;
      top: 50%;
      left: 50%;
      width: 3px;
      height: 3px;
      background: var(--color--wick);
      border-radius: 50%;
      animation: anim-spark-fly 0.8s ease-out infinite;
    }
    i:nth-child(1) { --fx: 11px;  --fy: -9px; animation-delay: 0s; }
    i:nth-child(2) { --fx: -9px;  --fy: -11px; animation-delay: 0.15s; }
    i:nth-child(3) { --fx: 9px;   --fy: 11px;  animation-delay: 0.3s; }
    i:nth-child(4) { --fx: -11px; --fy: 9px;   animation-delay: 0.45s; }
    i:nth-child(5) { --fx: 0px;   --fy: -13px; animation-delay: 0.1s; }
    i:nth-child(6) { --fx: 13px;  --fy: 0px;   animation-delay: 0.35s; }
    i:nth-child(7) { --fx: 0px;   --fy: 13px;  animation-delay: 0.6s; }
  }

  .compact-status {
    margin-top: 4px;
    padding-top: 4px;
    border-top: solid 1px var(--text-dim);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: -0.75px;
    white-space: nowrap;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  &.for-conf-label {
    overflow: hidden;
    justify-content: center;
    align-items: center;
    align-self: stretch;
    filter: var(--filter--newspaper-slight);

    span {
      writing-mode: vertical-rl;
      transform: rotate(180deg);
      font-family: var(--font--league-gothic);
      text-transform: uppercase;
      font-size: 18px;
      letter-spacing: 1px;
      white-space: nowrap;
      color: rgba(255, 255, 255, 0.9);
    }

    &.ncaaf {
      background-image:    var(--bg-image--nfl);
      background-size:     var(--bg-size--nfl);
      background-position: var(--bg-position--nfl);
      background-repeat:   no-repeat;
    }
  }

}

.bug.is-dragging .winner .bug-score-value::before {
  display: none;
}

/* A thin orange sweep chasing around the pill's own border — the board's
   own "new plays are still coming in" cue. Only a revealed game that's
   still actually in progress (.compact-live.is-live) — not a revealed
   final, which has nothing left to update. A mask-xor ring trick
   (padding:2px inset, content-box vs border-box masks XORed so only that
   2px ring survives). A bolder, more vivid orange than the replay
   slider's own knob glow (rgb(232, 160, 32) was more amber/reddish), and
   a narrower background-size — a shorter track for the same bright band
   to cross, so it recurs much more often — with a shorter duration too,
   for less dead pause between sweeps. */
.compact-tile.compact-live.is-live .game-score .bug::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  border-radius: inherit;
  padding: 2px;
  background: linear-gradient(90deg,
    transparent 0%, transparent 20%,
    rgba(255, 255, 255, 0.9) 37%,
    rgb(255, 140, 0) 50%,
    rgba(255, 255, 255, 0.9) 63%,
    transparent 80%, transparent 100%);
  background-size: 200% 100%;
  background-repeat: no-repeat;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  animation: anim-live-sweep var(--duration--live-pulse) linear infinite;
  animation-delay: calc(-1 * var(--duration--live-pulse) / 6);
}

.countdown-colon {
  animation: anim-countdown-blink 1s step-start infinite;
}

.soon-dots::before {
  content: '...';
  display: inline-block;
  width: 1.2em;
  overflow: hidden;
  vertical-align: bottom;
  margin-right: -0.15em;
  animation: anim-soon-dots 1.2s steps(3, end) infinite;
}

.delay-icon {
  width: 18px;
  height: 18px;
}

.game-score.has-delay-icon {
  overflow: visible;
}

.delay-side-icon {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.delay-side-icon-right { left: 100%;  margin-left: 3px; }
.delay-side-icon-left  { right: 100%; margin-right: 3px; }

#fav-section {
  margin-top: 20px;
  display: flex;
  flex-direction: column;
  gap: 20px;

  .fav-sport-group {
    display: flex;
    flex-direction: column;
    gap: 10px;

    &:has(.sport-toggle input:not(:checked)) .fav-teams-container {
      display: none;
    }

    .fav-teams-container {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
      row-gap: 10px;
      /* Vertical padding so the active pill's scale/rotate/shadow trail has
         room to breathe instead of getting clipped. */
      padding: 14px 10px;

      button {
        display: flex;
        flex-direction: column;
        align-items: center;
        background: none;
        border: none;
        scale: 1;
        transition: scale var(--duration--spring-bounce) var(--ease--spring-bounce);

        .fav-pill-circle {
          position: relative;
          width: 50px;
          height: 50px;
          border-radius: var(--border-radius--circular);
          display: flex;
          align-items: center;
          justify-content: center;
          transition: rotate var(--duration--spring-bounce) var(--ease--spring-bounce);
        }

        .fav-pill-logo {
          position: relative;
          z-index: 1;
          width: 80%;
          height: 80%;
          object-fit: contain;
        }

        &.active {
          scale: 1.1;

          .fav-pill-circle {
            rotate: var(--rotate--fav-logo);
            border-color: var(--color--snug-black);
            box-shadow: rgba(0, 0, 0, 0.3) 0 2px,
                        rgba(0, 0, 0, 0.2) 0 4px,
                        rgba(0, 0, 0, 0.1) 0 6px,
                        rgba(0, 0, 0, 0.05) 0 8px,
                        rgba(0, 0, 0, 0.025) 0 10px;

            &::before {
              content: '';
              position: absolute;
              inset: 0;
              border-radius: var(--border-radius--circular);
              background: #fff;
              pointer-events: none;
            }
          }

          &.is-settling {
            animation: anim-fav-pop-bump 0.3s ease-out;

            .fav-pill-circle::before {
              animation: anim-fav-diagonal-sweep 0.3s ease-out forwards;
            }
          }
        }
      }
    }
  }
}

@keyframes anim-fav-diagonal-sweep {
  from { clip-path: circle(0% at 0% 0%); }
  to   { clip-path: circle(150% at 0% 0%); }
}

@keyframes anim-fav-pop-bump {
  0%   { translate: 0 0;     scale: 1; }
  40%  { translate: 0 -20%;  scale: 1.2; }
  100% { translate: 0 0;     scale: 1.1; }
}

.sport-toggle {
  position: relative;
  display: inline-block;
  width: 48px;
  height: 28px;
  flex-shrink: 0;

  input {
    opacity: 0;
    width: 0;
    height: 0;
    position: absolute;

    &:checked + span {
      background: #34c759;
    }

    &:checked + span::after {
      transform: translateX(20px);
    }
  }

  > span {
    position: absolute;
    inset: 0;
    border-radius: var(--border-radius--circular);
    background: var(--border-filter-btn);
    transition: background 0.2s;

    &::after {
      content: '';
      position: absolute;
      width: 22px;
      height: 22px;
      border-radius: var(--border-radius--circular);
      background: #fff;
      top: 3px;
      left: 3px;
      transition: transform 0.2s;
    }
  }
}

.situation-dots {
  display: flex;
  gap: 1px;

  > * {
    width: 8px;
    height: 8px;
    border-radius: var(--border-radius--circular);
    background: var(--bg-out-dot);
    border: 1px solid var(--border-out-dot);

    &.on {
      background: var(--out-dot-on-bg);
      border-color: var(--out-dot-on-border);
    }
  }
}

@keyframes anim-action-fadein {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes anim-switch-on {
  0%   { transform: scale(1); }
  30%  { transform: scale(1.4); }
  55%  { transform: scale(0.9); }
  75%  { transform: scale(1.15); }
  100% { transform: scale(1); }
}

@keyframes anim-soon-dots {
  0%  { clip-path: inset(0 0 0 66%); }
  33% { clip-path: inset(0 0 0 33%); }
  66% { clip-path: inset(0 0 0 0); }
}

@keyframes anim-countdown-blink {
  0%, 49%   { opacity: 1; }
  50%, 100% { opacity: 0; }
}

@keyframes anim-live-sweep {
  from { background-position: 100% 0; }
  to   { background-position: -100% 0; }
}

@keyframes anim-fuse-burn {
  to { width: 0%; }
}

@keyframes anim-spark-fly {
  0%   { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  100% { transform: translate(calc(-50% + var(--fx)), calc(-50% + var(--fy))) scale(0); opacity: 0; }
}

@keyframes anim-mute-flip-on {
  0%   { transform: perspective(500px) rotateY(0deg);   opacity: 1;   }
  100% { transform: perspective(500px) rotateY(180deg); opacity: 0.2; }
}

@keyframes anim-mute-flip-off {
  0%   { transform: perspective(500px) rotateY(180deg); opacity: 0.2; }
  100% { transform: perspective(500px) rotateY(0deg);   opacity: 1;   }
}

@keyframes anim-mute-cover-on {
  from { transform: scaleY(0); }
  to   { transform: scaleY(1); }
}

@keyframes anim-mute-cover-off {
  from { transform: scaleY(1); }
  to   { transform: scaleY(0); }
}

@keyframes anim-progress-pulse {
  0%, 100% { transform: translateX(50%) scale(1);  }
  50%       { transform: translateX(calc(50% + 2px)) scale(2);  }
}
