:root {
  --bg: #0e1420;
  --panel: #161f30;
  --panel-alt: #1c2740;
  --text: #eef1f6;
  --muted: #93a0b8;
  --border: #2a3550;
  --home: #4fa3ff;
  --away: #ff6b6b;
  /* Brand blue (from the wizard-hat in the logo) replaced the old gold
     (#f4c542, lifted from the logo wordmark) as the one interactive colour
     on 2026-09-24 - gold had ended up meaning "tap me", "this is on" AND
     "look here" all at once, and failed contrast on the light palette.
     --accent now means exactly two things: the single primary action on a
     sheet, and a binary on-state (switch track, checkbox tick). Choosing
     one-of-N (segments, chips, tiles) is neutral inversion instead
     (--invert-*), so a selected state can never clash with whichever
     arbitrary team colours the live screen happens to be showing. */
  --accent: #2563eb;
  --on-accent: #ffffff;
  --control: #232c39;
  --separator: rgba(255, 255, 255, 0.08);
  --invert-fill: #eef1f6;
  --invert-text: #0e1420;
  --switch-off: #3a4354;
  --danger: #ff6b6b;
  --radius: 14px;
  --radius-control: 10px;
  --radius-sheet: 22px;
}
/* Light palette - the app was dark-only until now, so this is a first pass on
   the core chrome tokens (backgrounds/text/borders/accent), not an every-
   pixel-verified redesign. --accent in particular is deliberately darkened
   here (not just the dark palette's gold): it's used as plain TEXT colour
   (e.g. the leading number in stat comparisons) and #f4c542 read fine on the
   dark panel it was picked for, but is too low-contrast on a white one.
   Module-specific fixed-look colours are untouched on purpose - the oval
   field's turf-green gradient and the injury/clock icon colours are meant to
   look the same regardless of app theme, not adapt to it. */
:root[data-theme="light"] {
  --bg: #f2f4f7;
  --panel: #ffffff;
  --panel-alt: #eef1f6;
  --text: #1a2233;
  --muted: #5b6b85;
  --border: #d5dbe6;
  --accent: #1d4ed8;
  --on-accent: #ffffff;
  --control: #e7ebf2;
  --separator: rgba(15, 30, 60, 0.10);
  --invert-fill: #1a2233;
  --invert-text: #ffffff;
  --switch-off: #cfd6e2;
  --danger: #c62e2e;
}
/* System-preference default, only when the user hasn't made an explicit
   choice yet (see applyTheme() in app.js, which sets data-theme="light" or
   "dark" directly once they have - that always wins over this). */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]):not([data-theme="light"]) {
    --bg: #f2f4f7;
    --panel: #ffffff;
    --panel-alt: #eef1f6;
    --text: #1a2233;
    --muted: #5b6b85;
    --border: #d5dbe6;
    --accent: #1d4ed8;
    --on-accent: #ffffff;
    --control: #e7ebf2;
    --separator: rgba(15, 30, 60, 0.10);
    --invert-fill: #1a2233;
    --invert-text: #ffffff;
    --switch-off: #cfd6e2;
    --danger: #c62e2e;
  }
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  overscroll-behavior: none;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}
/* The blanket user-select:none above (meant for buttons/labels, so a tap
   doesn't accidentally select surrounding text) was inherited by every text
   field too - WebKit/Safari has a long-standing bug where that makes the
   caret render in the wrong spot inside an input/textarea (visibly outside
   the field, not tracking where you're actually typing), which is exactly
   what this looked like in every small modal with a text field (Save Lineup
   Template and others). Re-enabling normal text selection specifically on
   form fields fixes the caret without reintroducing accidental text
   selection anywhere else in the app. */
input, textarea, select {
  -webkit-user-select: text;
  user-select: text;
}

.hidden { display: none !important; }

/* Universal pressed/depressed feedback - every button in the app gets a
   consistent tactile "press" (scale down slightly) regardless of its own
   colour/border treatment. Deliberately just transform, not colour/filter,
   so it layers on top of button-specific active states (e.g. .stat-btn's
   own brightness dim, the stat-flash confirmation animation) rather than
   fighting them. :disabled is excluded so a disabled button never looks
   tappable. */
button:not(:disabled) { transition: transform 0.08s ease; }
button:not(:disabled):active { transform: scale(0.96); }

/* Fields vs buttons - a text field previously shared the exact same
   background/border-radius as .secondary-btn (both var(--panel-alt), both
   10px), so a field read as just another tappable control instead of
   somewhere to type. Fields now recede into the surface they sit on (same
   background as the surrounding panel, border only) and only stand out on
   focus; buttons stay solid/bordered/high-contrast. Every specific input
   rule below (.stat-weight, .team-select, .detail-input, etc.) keeps its own
   sizing/spacing but no longer redeclares background/border/radius/color -
   this is the one place that look is defined. */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="date"],
input[type="time"],
select,
textarea {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  padding: 10px;
  font-size: 1rem;
}
input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
input[type="number"]:focus,
input[type="date"]:focus,
input[type="time"]:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--accent);
}

.screen { min-height: 100dvh; display: flex; flex-direction: column; }
/* #live-screen only (not .screen generally - setup/recap rely on the page
   itself scrolling when their content overflows) - a FIXED height plus
   overflow:hidden means this screen itself can never become scrollable as a
   whole, so the only thing that can ever scroll is whichever internal panel
   (.stat-columns, #recent-list, .stat-chart) has its own overflow-y:auto.
   Without this, if the live screen's total content ever exceeded the
   viewport (however marginally), the page would scroll as a unit and every
   area would shift together - the actual bug behind "scrolling the feed
   moves other areas too", not just scroll-chaining between feed/chart. */
#live-screen { height: 100dvh; overflow: hidden; }

/* Spectator on phone, specifically: a coach mid-recording needs the fixed-
   height/overflow:hidden layout above (their button grid + drag-resize
   workflow depends on it, and the "scroll one area, not the whole screen"
   fix it protects), but a spectator with several Widgets enabled has nothing
   to actively operate - forcing every enabled widget to squeeze into an
   equal, internally-scrolling slice of one fixed-height row (the layout
   above) just makes each one cramped. Below the tablet breakpoint (where
   widgets already stack in one column rather than sitting side by side),
   let the page itself grow to its natural content height and scroll as a
   whole instead - each stacked widget gets its own natural height rather
   than a forced equal share. Per Shaun 2026-09-01.
   .mid-bar/.cheer-btn/.crowd-heat are all position:fixed, so they stay
   correctly pinned to the viewport regardless of how far the page scrolls -
   this doesn't need to touch any of them. */
@media (max-width: 767px) {
  #live-screen.spectator-mode { height: auto; overflow: visible; }
  #live-screen.spectator-mode .bottom-panels {
    flex: 0 0 auto;
    gap: 12px;
    /* Bottom padding must still clear the fixed .mid-bar (Stats/Timeline/
       Leave), same as the base .bottom-panels rule already does for the
       normal fixed-height/internal-scroll layout - a flat 8px here (this
       rule's first version) undid that reserve, so scrolling this page all
       the way down left Ground Map's own bottom edge sitting behind the
       bar on iPhone/Android. Confirmed live 2026-09-01. */
    padding: 0 8px calc(var(--mid-bar-height, 60px) + 12px);
  }
  /* Same card-chrome the >=768px side-by-side row already uses (see
     .timeline-panel/.stat-chart/.ground-map-panel's own tablet-breakpoint
     rules) - each is now a distinct stacked block in a scrolling page, not
     a shared box's internal division, so it should read as its own card
     here too rather than the flush/borderless phone treatment other roles
     still get. */
  #live-screen.spectator-mode .timeline-panel,
  #live-screen.spectator-mode .stat-chart,
  #live-screen.spectator-mode .ground-map-panel {
    flex: 0 0 auto;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
  }
  /* Performance Charts and Ground Map are deliberately NOT capped here -
     Performance's own height already comes purely from how many stats are
     actually enabled (one row each, see .stat-chart-row), so removing the
     flex:1 equal-share constraint above is enough to make it genuinely
     dynamic; Ground Map's SVG oval is aspect-ratio-locked regardless of how
     many events are plotted on it, so it self-bounds without help. Timeline
     is the one genuinely unbounded case (a full match can log 150-250+
     events) - capped, with #recent-list's own existing overflow-y:auto
     handling the internal scroll once content exceeds it. */
  #live-screen.spectator-mode .timeline-panel { max-height: 400px; }
  /* .ground-map-svg's own base rule (further down this file) is
     position:absolute + object-fit:contain, deliberately fitted to a
     container that already HAS a definite height it must never exceed -
     correct for the normal fixed-height layout (host/recorder always,
     spectator on tablet/desktop), where .ground-map-panel is flex:1 sharing
     a bounded row's height. Here specifically, .ground-map-panel is instead
     flex:0 0 auto (see above) - deliberately auto-height, with the OVAL
     meant to be what DEFINES that height from its own aspect ratio, not
     something fitting into a height that already exists. position:absolute
     removes the SVG from that sizing chain entirely, so the auto-height
     panel had nothing left to size around and collapsed to near nothing -
     confirmed live 2026-09-01 ("size of a small pea"). Reverting to plain
     normal-flow width:100%/height:auto here restores the panel's ability to
     size around the oval's own natural aspect ratio, same as before the
     fixed-height fix existed. */
  #live-screen.spectator-mode .ground-map-svg {
    position: static;
    width: 100%;
    height: auto;
    max-width: none;
    max-height: none;
  }
}

/* Setup */
.setup-card {
  /* Was `margin: auto` (centers on both axes) - fine for one screen, but the
     setup tabs (New Game/My Team/Join/History) vary hugely in content height,
     so full centering re-centered the whole card vertically on every tab
     switch, reading as the page "jumping". A fixed top margin keeps the
     card's top edge - and the tab bar inside it - anchored in place; only
     the space below it changes as tab content grows/shrinks. */
  margin: 40px auto;
  width: min(90vw, 380px);
  background: var(--panel);
  border-radius: var(--radius);
  padding: 28px 24px;
  text-align: center;
}
.setup-logo { display: block; width: min(70%, 260px); margin: 0 auto 4px; }
/* Tablet/desktop: the setup screen was capped at phone width regardless of
   viewport (an iPad rendered the exact same 380px-wide card as a phone, just
   with more empty space around it) - the in-match/stat screens already scale
   past this same 768px breakpoint (see the bench board below), this brings
   the setup/login/My Team/History screens in line with that. */
@media (min-width: 768px) {
  .setup-card { width: min(70vw, 640px); padding: 36px 40px; }
}
.subtitle { color: var(--muted); margin-top: 16px; }
/* Join Game tab: makes it obvious at a glance which of the two blocks below
   is for you, before reading either one's own instructional subtitle - per
   Shaun 2026-09-01. Sits ABOVE .subtitle (which keeps its own top margin),
   so the first heading needs its own smaller top margin, not .subtitle's
   16px - it's the very first thing in the panel. */
.join-method-heading { margin: 16px 0 0; font-size: 1.05rem; font-weight: 700; color: var(--text); }
.panel-divider + .join-method-heading { margin-top: 4px; }
.version-tag { display: block; margin-top: 18px; font-size: 0.7rem; color: var(--muted); }
.modal-header h2 .version-tag { display: inline; margin: 0 0 0 6px; font-weight: 400; }
.modal-header h2 #changelog-link { margin-left: 10px; font-size: 0.75rem; vertical-align: middle; }
.changelog-list { max-height: 70vh; overflow-y: auto; }
.changelog-entry { padding: 12px 0; border-bottom: 1px solid var(--border); }
.changelog-entry:last-child { border-bottom: none; }
.changelog-entry-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; }
.changelog-entry-version { font-weight: 600; color: var(--accent); }
.changelog-entry-date { font-size: 0.75rem; color: var(--muted); }
.changelog-entry-message { font-size: 0.9rem; color: var(--text); line-height: 1.4; }
.setup-card label, #profile-modal label, #share-modal label {
  display: block;
  text-align: left;
  font-size: 0.85rem;
  color: var(--muted);
  margin-top: 16px;
}
.setup-card input, #profile-modal input, #share-modal input, #note-modal textarea, .textarea-field textarea {
  width: 100%;
  margin-top: 6px;
  padding: 12px;
  font-size: 1rem;
}
#note-modal textarea, .textarea-field textarea { font-family: inherit; resize: vertical; box-sizing: border-box; }
/* Was a bare <label>Text<textarea> - with no layout of its own, an inline
   label's text and its inline-block textarea child render side by side/
   overlapping rather than stacking, which is exactly what made the Debrief
   panel (this class's original motivating case) look broken: labels
   floating over/beside the fields instead of above them. */
.textarea-field { display: flex; flex-direction: column; width: 100%; box-sizing: border-box; text-align: left; font-size: 0.85rem; color: var(--muted); margin: 4px 0 16px; gap: 2px; }
.team-row-inputs { display: flex; gap: 8px; }
.team-row-inputs input[type="text"] { flex: 1; }
input[type="color"] {
  -webkit-appearance: none;
  appearance: none;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel-alt);
  flex-shrink: 0;
}
input[type="color"]::-webkit-color-swatch-wrapper { padding: 4px; }
input[type="color"]::-webkit-color-swatch { border: none; border-radius: 6px; }
.primary-btn {
  margin-top: 22px;
  width: 100%;
  padding: 14px;
  border-radius: 10px;
  border: none;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 1.05rem;
  font-weight: 600;
}

.setup-tabs { display: flex; gap: 8px; margin-top: 18px; }
.setup-tab {
  flex: 1;
  padding: 10px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--panel-alt);
  color: var(--muted);
  font-size: 0.9rem;
  font-weight: 600;
}
.setup-tab.active { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }

/* Accounts */
.account-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 14px;
  font-size: 0.85rem;
  color: var(--muted);
}
.link-btn {
  background: none;
  border: none;
  color: var(--accent);
  font-size: 0.85rem;
  font-weight: 600;
  padding: 0;
}
.account-bar-actions { display: flex; gap: 14px; }
.account-subtabs { display: flex; gap: 8px; margin-top: 18px; }
.account-subtab {
  flex: 1;
  padding: 8px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--muted);
  font-size: 0.85rem;
  font-weight: 600;
}
.account-subtab.active { border-color: var(--accent); color: var(--accent); }

/* Top bar */
.top-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: max(10px, env(safe-area-inset-top)) 12px 10px;
  background: var(--panel);
  gap: 8px;
}
.team-score { flex: 1; text-align: center; min-width: 0; }
.team-name { display: block; font-weight: 600; font-size: 0.95rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.team-score-logo { display: block; width: 32px; height: 32px; object-fit: contain; margin: 0 auto 2px; }
.score { display: block; color: var(--muted); font-variant-numeric: tabular-nums; font-size: 0.9rem; }
/* Fires alongside the total's count-up animation (see animateScoreTotal() in
   app.js) whenever a goal/behind lands - transform+color only, never touches
   layout (no size/margin changes) so it can't shove the clock block sideways
   mid-animation. */
.score.score-pop { animation: score-pop 0.6s ease; }
@keyframes score-pop {
  0% { transform: scale(1); }
  30% { transform: scale(1.3); color: var(--accent); }
  100% { transform: scale(1); }
}
.clock-block { text-align: center; flex-shrink: 0; }
#quarter-label { display: block; font-size: 0.8rem; font-weight: 700; letter-spacing: 0.04em; color: var(--text); }
#game-clock { display: block; font-size: 1.3rem; font-variant-numeric: tabular-nums; font-weight: 700; }
#game-clock.time-up { color: var(--danger); animation: pulse 1s infinite; }
.break-clock { display: block; font-size: 0.75rem; color: var(--accent); font-variant-numeric: tabular-nums; }
@keyframes pulse { 50% { opacity: 0.4; } }
.clock-btn {
  margin-top: 4px;
  padding: 6px 10px;
  border-radius: 8px;
  border: none;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 0.75rem;
  font-weight: 600;
}
.clock-btn:disabled { background: var(--border); color: var(--muted); }

/* In-match alerts - a stacked layer over the score/clock/momentum area, dismissed
   by tapping any individual banner. Hidden entirely (no backdrop) when empty. */
.score-clock-area { position: relative; }
/* Top-right, no border by design (Shaun's own wording) - just the bare icon,
   floating above the header rather than taking a slot in it, so it stays in
   the same corner regardless of how long the team names get. */
.ui-toggle-btn {
  position: absolute;
  top: max(4px, env(safe-area-inset-top));
  right: 4px;
  z-index: 60;
}
/* Stacked directly under .ui-toggle-btn (48px = its 44px tap target + a 4px
   gap) - same floating, no-border treatment. Icon itself is drawn small
   (18px) inside the 44px tap target, see renderWeatherIndicator() in app.js. */
.weather-indicator-btn {
  position: absolute;
  top: calc(max(4px, env(safe-area-inset-top)) + 48px);
  right: 4px;
  z-index: 60;
}
.weather-indicator-icon { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; }
/* Mirrors .weather-indicator-btn's top-right placement, on the left. */
.best-player-btn-trigger {
  position: absolute;
  top: max(4px, env(safe-area-inset-top));
  left: 4px;
  z-index: 60;
}
/* Stacked directly under .best-player-btn-trigger, same 48px offset as
   .weather-indicator-btn under .ui-toggle-btn on the opposite corner. */
.video-btn-trigger {
  position: absolute;
  top: calc(max(4px, env(safe-area-inset-top)) + 48px);
  left: 4px;
  z-index: 60;
}
/* Utility class the show/hide drawer applies - !important so it always wins
   over whatever content-driven hidden-class logic (renderGoalkickers() etc.)
   is doing on the same element, since they're deliberately different classes
   that never fight over the same one. */
.ui-hidden { display: none !important; }
/* Wrapper divs only exist so Score Worm / Momentum Worm can be toggled as one
   unit each - display:contents makes them invisible to layout normally (the
   panel's direct children still stack exactly as before), collapsing to
   display:none only once .ui-hidden is added. */
.score-worm-group, .momentum-worm-group { display: contents; }

.alerts-overlay {
  position: absolute;
  inset: 0;
  z-index: 50;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 8px;
  padding: 14px;
  background: rgba(6, 9, 16, 0.88);
  overflow-y: auto;
}
.alert-banner {
  border-radius: 12px;
  padding: 12px 16px;
  font-size: 0.9rem;
  font-weight: 600;
  line-height: 1.35;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  animation: alert-in 0.2s ease;
  border-left: 4px solid var(--accent);
  background: var(--panel-alt);
  color: var(--text);
}
.alert-banner.alert-home { border-left-color: var(--home); }
.alert-banner.alert-away { border-left-color: var(--away); }
.alert-banner-hint { display: block; margin-top: 4px; font-size: 0.7rem; font-weight: 400; color: var(--muted); }
@keyframes alert-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } }
/* Only rendered once there are 2+ banners stacked - a lone banner already
   has its own "Tap to dismiss". */
.alert-dismiss-all-btn {
  align-self: center;
  border-radius: 999px;
  padding: 8px 16px;
  font-size: 0.8rem;
  font-weight: 700;
  border: 1px solid var(--accent);
  background: var(--accent);
  color: var(--on-accent);
}

/* Momentum */
.momentum-panel { padding: 10px 16px; background: var(--panel); }
.score-worm { width: 100%; height: 24px; display: block; margin-bottom: 4px; }
/* bevel, not miter - a miter join at the sharp/near-back-to-back angles this step
   line produces (e.g. two scores in quick succession) projects a disproportionate
   spike past the actual stroke width, reading as inconsistent line thickness.
   bevel keeps corners square (unlike round) without that spike.
   non-scaling-stroke: the viewBox (300x24) is stretched non-uniformly to fill a
   wide/short box (preserveAspectRatio="none"), so without this, a vertical step's
   stroke width scales with the (much larger) x scale factor while a horizontal
   run's scales with y - steps render visibly thicker than flat runs. This pins
   stroke width to real screen pixels regardless of that per-axis stretch. */
.score-worm-line { fill: none; stroke: var(--text); stroke-width: 2; stroke-linejoin: bevel; vector-effect: non-scaling-stroke; }
.momentum-worm { width: 100%; height: 48px; display: block; margin-bottom: 6px; }
.worm-baseline { stroke: var(--border); stroke-width: 1; }
.worm-home-area { fill: var(--home); opacity: 0.55; }
.worm-away-area { fill: var(--away); opacity: 0.55; }
.momentum-track {
  position: relative;
  height: 14px;
  border-radius: 7px;
  background: linear-gradient(90deg, var(--home), var(--border) 48%, var(--border) 52%, var(--away));
}
.momentum-needle {
  position: absolute;
  top: -5px;
  left: 50%;
  width: 4px;
  height: 24px;
  background: #fff;
  border-radius: 2px;
  transform: translateX(-2px);
  transition: left 0.4s ease;
}
.momentum-readout { text-align: center; margin-top: 6px; font-size: 0.85rem; color: var(--muted); }

/* Stat buttons - height is user-adjustable via #feed-resize-handle (dragged
   height persists per-device in db 'ui-prefs', applied in app.js), rather
   than the fixed flex:1 (always fill remaining space) this used to be. The
   var() fallback is only what a brand-new device sees before its first drag. */
.stat-columns {
  flex: 0 0 auto;
  height: var(--stat-columns-height, 45vh);
  min-height: 90px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  padding: 10px;
  overflow-y: auto;
}
/* Drag handle between the stat buttons and the recent-events feed - lets a
   coach running only a handful of stats reclaim that space for a taller
   feed instead. Hit height was 20px around a thin 4px visible grip - found
   live (Shaun's feedback 2026-08-19) that's not enough margin for a real
   thumb: .stat-columns right above it is itself natively scrollable
   (overflow: auto, no touch-action of its own), so a touch-start landing
   even a few px into the button grid instead of the handle gets claimed as
   a native scroll rather than ever reaching this element's own
   touch-action: none - the drag silently never starts. Bumped 20px -> 36px
   -> 44px (Apple's HIG touch-target minimum) across two rounds, but a
   recorder's report 2026-08-31 with a screenshot (iPhone, a stat button
   genuinely clipped right above the handle - .stat-columns' own height had
   overflowed) showed the handle was STILL unreachable. Growing the handle's
   OWN box was never going to fully fix this: the handle sits AFTER
   .stat-columns in normal flow, so its box can only get taller downward,
   never reclaim the pixels directly above it that .stat-columns' native
   scroll already owns - exactly where a real thumb lands when the content
   right above the handle is what it's trying to reveal more of. The ::before
   below is a genuinely overlapping (position:absolute, negative top) hit
   target stacked ON TOP of .stat-columns' own last ~16px - invisible, but a
   touch landing there now hit-tests to this element instead of the
   scrollable grid underneath, regardless of how tall the button content
   above it happens to be. */
.feed-resize-handle {
  position: relative;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 44px;
  cursor: row-resize;
  touch-action: none;
  background: var(--panel);
  -webkit-user-select: none;
  user-select: none;
}
.feed-resize-handle::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: -16px;
  height: 16px;
}
.feed-resize-handle .handle-grip {
  width: 36px;
  height: 4px;
  border-radius: 2px;
  background: var(--border);
  transition: background 0.15s;
}
.feed-resize-handle:active .handle-grip,
.feed-resize-handle.dragging .handle-grip {
  background: var(--muted);
}
.stat-column { display: flex; flex-direction: column; gap: 8px; }
.stat-btn {
  min-height: 43px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--panel-alt);
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 16px;
  font-size: 1rem;
}
/* Filled with the team's own colour, text/count colour picked automatically
   for contrast (contrastTextColor() in app.js) - a yellow team gets black
   text, a red team gets white, etc. Border stays neutral rather than
   matching the fill (a same-colour border would just vanish). */
.stat-column:first-child .stat-btn { background: var(--home); color: var(--home-text); }
.stat-column:last-child .stat-btn { background: var(--away); color: var(--away-text); }
/* Brightness-based rather than a fixed override colour, since the resting
   background is now an arbitrary team colour, not always var(--panel-alt). */
.stat-btn:active { filter: brightness(0.85); }
/* Immediate "just recorded" confirmation flash on the exact button tapped -
   also brightness-based for the same reason (an override background colour
   would fight/hide whichever team colour the button actually sits on). */
@keyframes stat-flash { 0% { filter: brightness(1.7); } 100% { filter: brightness(1); } }
.stat-btn-flash { animation: stat-flash 0.4s ease; }
/* A two-word label ("Center Clearance") or a count that's grown wide (double/
   triple-digit tallies squeeze the label's share of the button) both wrap
   onto a second line here - <button> UA styles default to text-align:center,
   which read as ragged/awkward once wrapped (found live: "Handball Back"
   started wrapping once its own count passed single digits). Left-align
   instead, for any label that wraps for either reason. The count group
   itself is flex-shrink:0 so it's always the label that absorbs the squeeze,
   never the numbers getting cramped instead. */
.stat-label { text-align: left; flex: 1 1 auto; min-width: 0; }
.stat-count-group { display: flex; align-items: baseline; gap: 5px; flex-shrink: 0; }
.stat-count { font-size: 1.4rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.stat-count-total { font-size: 0.85rem; font-weight: 400; color: inherit; opacity: 0.75; font-variant-numeric: tabular-nums; }

/* Clock not running (not started, paused, or past the grace period) - no actions allowed */
.stat-columns.clock-stopped .stat-btn { opacity: 0.35; pointer-events: none; }

.waiting-banner {
  margin: 0 10px 10px;
  padding: 10px 12px;
  border-radius: 10px;
  background: var(--panel-alt);
  border: 1px solid var(--accent);
  color: var(--accent);
  font-size: 0.85rem;
  text-align: center;
}
.practice-banner {
  margin: 0 10px 10px;
  padding: 8px 12px;
  border-radius: 10px;
  background: #4a3a12;
  border: 1px solid #d9a441;
  color: #f4c869;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-align: center;
}

/* Icon-labelled buttons (Tier 1 pass) - a single-colour mask-image rather than
   an <img>, so each icon inherits the button's own `color` (accent/text/danger)
   automatically instead of needing a light/dark or per-button colour export.
   Requires the source SVGs to be plain single-path shapes with no baked-in
   fill - confirmed true for all icons in /icons/ before wiring this up. */
.btn-icon {
  display: inline-block;
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  background-color: currentColor;
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}
.icon-settings { -webkit-mask-image: url('../icons/settings.svg'); mask-image: url('../icons/settings.svg'); }
.icon-stats { -webkit-mask-image: url('../icons/stats.svg'); mask-image: url('../icons/stats.svg'); }
.icon-timeline { -webkit-mask-image: url('../icons/timeline.svg'); mask-image: url('../icons/timeline.svg'); }
.icon-note { -webkit-mask-image: url('../icons/note.svg'); mask-image: url('../icons/note.svg'); }
.icon-video { -webkit-mask-image: url('../icons/video.svg'); mask-image: url('../icons/video.svg'); }
.icon-bench { -webkit-mask-image: url('../icons/bench.svg'); mask-image: url('../icons/bench.svg'); }
.icon-ai { -webkit-mask-image: url('../icons/ai.svg'); mask-image: url('../icons/ai.svg'); }
.icon-undo { -webkit-mask-image: url('../icons/undo.svg'); mask-image: url('../icons/undo.svg'); }
.icon-leave { -webkit-mask-image: url('../icons/leave.svg'); mask-image: url('../icons/leave.svg'); }
.icon-end-match { -webkit-mask-image: url('../icons/end-match.svg'); mask-image: url('../icons/end-match.svg'); }
.icon-delete { -webkit-mask-image: url('../icons/delete.svg'); mask-image: url('../icons/delete.svg'); }
.icon-interface { -webkit-mask-image: url('../icons/interface.svg'); mask-image: url('../icons/interface.svg'); }
.icon-filter { -webkit-mask-image: url('../icons/filter.svg'); mask-image: url('../icons/filter.svg'); }
.icon-trophy { -webkit-mask-image: url('../icons/best.svg'); mask-image: url('../icons/best.svg'); }

/* Icon-only on the live game/stat-entry screen's own action row (mid-bar)
   below the tablet breakpoint - a phone coach mid-match needs the vertical
   room more than the label text, and these buttons already read fine from
   the icon alone once you know them. Settings-modal buttons (End Match,
   Delete Match, etc.) keep their text at every width - they're rare,
   high-consequence taps where clarity matters more than space. */
.mid-bar .btn-label {
  display: none;
}
@media (min-width: 768px) {
  .mid-bar .btn-label {
    display: inline;
  }
}

.stats-btn {
  margin: 0 10px;
  padding: 12px;
  border-radius: 10px;
  border: none;
  background: transparent;
  color: var(--text);
  font-size: 0.95rem;
  font-weight: 600;
}
.stats-btn,
.secondary-btn,
.danger-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}
/* Every tier-1 action (Stats/Bench/AI Coach/Note/Undo/Timeline/Settings/Leave)
   is the same size now - no single button (Stats, previously) grows to fill
   leftover space, so wrap+space-between is what keeps a variable-length row
   (spectators see far fewer buttons than a host) looking intentional rather
   than stretched or bunched to one side. Borderless standalone icons on
   phone (no box around them at all); the bordered pill look only returns
   once labels do, at the tablet breakpoint. */
/* Genuinely frozen to the bottom of the viewport on every device - flex
   `order` (the previous approach) only ever placed this row at the bottom of
   #live-screen's OWN content flow, which stays correct only as long as that
   flow's total height never exceeds the viewport. It can (mobile Safari's
   toolbar resizing the visual viewport, a keyboard opening, enough stat
   categories to overflow .bottom-panels' own scroll) - position:fixed
   removes it from the flow entirely so it can't ever be pushed off-screen
   or need to be scrolled to. safeAreaBottom below reserves room in the
   scrollable content so the fixed bar never overlaps the last visible row
   of the feed/chart - see applyMidBarHeightVar() in app.js, which measures
   this row's actual rendered height (icon-only vs labelled, one row vs
   wrapped-two on a very narrow phone) rather than assuming a fixed number. */
.mid-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 100;
  background: var(--panel);
  padding: 6px 8px max(6px, env(safe-area-inset-bottom));
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  justify-content: space-between;
}
.bottom-panels { padding-bottom: var(--mid-bar-height, 60px); }
/* Icon-only width (12px horizontal padding) is tight enough that all 7
   possible tier-1 buttons (Stats/Bench/AI Coach/Note/Undo/Timeline/Settings)
   fit on one row even on a 375px-wide iPhone - checked live, they wrapped to
   2 lines at the old 18px padding. Roomier again once labels reappear at the
   tablet breakpoint, where width is no longer the constraint. */
.mid-bar .stats-btn { margin: 0; flex: 0 0 auto; padding: 12px; border: none; }
/* Undo is the one destructive/reversal action in the tier-1 row - a distinct
   orange (not the gold --accent, not --danger red) makes it pop without
   reading as an error state. */
.stats-btn.undo-btn-accent { color: #ff8c00; }
@media (min-width: 768px) {
  .mid-bar .stats-btn { padding: 12px 18px; border: 1px solid var(--border); }
}

.secondary-btn {
  flex: 1;
  padding: 12px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--panel-alt);
  color: var(--text);
  font-size: 0.95rem;
}
#watch-game-btn { margin-top: 22px; }

.sync-status {
  flex-shrink: 0;
  font-size: 0.75rem;
  color: var(--muted);
  white-space: nowrap;
  padding: 0 4px;
}

/* Spectator (read-only live view via ?watch= link): hide every input surface -
   the shared score/clock/worm/Match Stats rendering is all a viewer needs. */
.screen.spectator-mode .stat-columns,
.screen.spectator-mode .feed-resize-handle,
.screen.spectator-mode #add-note-btn,
.screen.spectator-mode #bench-btn,
.screen.spectator-mode #ai-coach-btn,
.screen.spectator-mode #undo-btn,
.screen.spectator-mode #settings-btn,
.screen.spectator-mode #player-picker,
.screen.spectator-mode #freekick-reason-picker,
.screen.spectator-mode #waiting-banner { display: none !important; }
/* Leave is the one control that exists ONLY for spectators (a Join-tab viewer has
   no other way back to the setup screen - everything else is hidden above).
   Undo/Settings above are now hidden individually rather than via a wrapping
   .bottom-bar - that row got merged into .mid-bar so every tier-1 action
   (Stats/Bench/AI Coach/Note/Undo/Timeline/Settings/Leave) sits on one line. */
#spectator-leave-btn { display: none; }
.screen.spectator-mode #spectator-leave-btn { display: inline-flex; }
/* The Match Stats modal sits outside #live-screen, so it carries its own
   spectator flag (set in renderMatchStats): stats visible, header actions
   (Report/Timeline/CSV/Events/Players CSV/AI Summary/Share) hidden - only
   the close button stays. */
#stats-modal.spectator .modal-header-actions button:not(#stats-close) { display: none !important; }

/* Spectator watch code + QR (Team Mode settings) */
#watch-code-display { font-size: 1.3rem; letter-spacing: 2px; color: var(--accent); }
.panel-divider { border: none; border-top: 1px solid var(--border); margin: 18px 0 12px; }
.watch-qr {
  width: fit-content;
  margin: 10px auto;
  padding: 8px;
  background: #ffffff; /* quiet zone - QR needs light surround to scan on the dark theme */
  border-radius: 8px;
  line-height: 0;
}
.watch-qr svg { width: 180px; height: 180px; display: block; }

/* Coach targets */
.targets-list { display: flex; flex-direction: column; }
.target-row {
  display: grid;
  grid-template-columns: 1fr 70px 70px;
  gap: 8px;
  align-items: center;
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.9rem;
}
.target-row select {
  padding: 6px 4px;
  font-size: 0.85rem;
}
.target-hint { color: var(--muted); font-size: 0.75rem; }
#targets-save-btn { margin-top: 10px; width: 100%; }

/* AI match summary */
.summary-text {
  margin-top: 12px;
  padding: 4px 2px 12px;
  font-size: 0.95rem;
  line-height: 1.6;
  color: var(--text);
  white-space: pre-wrap;
  user-select: text;
}

/* Ask Jock */
.jock-title { display: flex; align-items: center; gap: 10px; }
.jock-avatar { width: 40px; height: 40px; flex-shrink: 0; }
.jock-avatar-sm { width: 22px; height: 22px; flex-shrink: 0; margin-top: 2px; }
.ask-stats-answers {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 12px;
  max-height: 50vh;
  overflow-y: auto;
}
.ask-stats-qa { display: flex; flex-direction: column; gap: 4px; }
.ask-stats-question {
  align-self: flex-end;
  background: var(--accent);
  color: var(--on-accent);
  padding: 8px 12px;
  border-radius: 12px 12px 2px 12px;
  font-size: 0.9rem;
  max-width: 85%;
}
.ask-stats-answer-row { align-self: flex-start; display: flex; align-items: flex-start; gap: 8px; max-width: 85%; }
.ask-stats-answer {
  background: var(--panel-alt);
  color: var(--text);
  padding: 8px 12px;
  border-radius: 12px 12px 12px 2px;
  font-size: 0.9rem;
  line-height: 1.5;
  white-space: pre-wrap;
}
/* Was a bare flex row: the input's `flex: 1` competed against the submit
   button's global .primary-btn width:100% (meant for full-width setup-screen
   buttons), which made the button claim almost the whole row and left the
   input cramped down to a sliver - fix scoped to this row only. */
.ask-stats-input-row {
  display: flex;
  gap: 8px;
}
.ask-stats-input-row input {
  flex: 1;
  min-width: 0;
}
.ask-stats-input-row .primary-btn {
  width: auto;
  flex: 0 0 auto;
  margin-top: 0;
  padding: 10px 20px;
}

/* Recent list */
/* Phone has no spare width to put Performance beside the feed, so below the
   tablet breakpoint any enabled widgets (Timeline/Performance) stack instead
   - each flex:1, sharing the bottom area's height. Hiding either one via the
   Widgets drawer leaves the other as the column's only flex item, filling
   the full height on its own - no extra CSS needed for that. */
.bottom-panels { flex: 1; min-height: 0; display: flex; flex-direction: column; }
@media (min-width: 768px) {
  /* From the tablet breakpoint up, widgets sit side by side as standalone
     cards instead of stacked. The 12px gap between them was showing the
     page's own --bg grey through it (light mode) - it should read as white
     like the cards themselves, with grey confined to being the tint INSIDE
     each widget's own body (#recent-list/#stat-chart-body), not the space
     between them. Matching the cards' own --panel here does that. */
  .bottom-panels { flex-direction: row; gap: 12px; background: var(--panel); }
}
/* Wraps the feed so it can carry a bold "Timeline" title (matching the
   Performance panel's own header) without disturbing the feed's own flex/
   scroll sizing - that all still lives on #recent-list itself, one level in. */
.timeline-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; }
@media (min-width: 768px) {
  .timeline-panel {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
  }
}
.timeline-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 4px 8px 4px 12px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  position: relative;
}
/* Smaller than the app-wide 44px .icon-btn tap target (that minimum matters
   for primary actions like closing a modal; this is an infrequently-tapped
   filter icon in a data-dense header, where the extra height costs more than
   it's worth) - still a comfortable ~32px target, just not the full 44px. */
.timeline-panel-header .icon-btn,
.stat-chart-header .icon-btn,
.ground-map-header .icon-btn {
  min-width: 32px;
  min-height: 32px;
  font-size: 1.1rem;
}
.timeline-panel-title { font-size: 0.85rem; font-weight: 700; color: var(--text); }
.recent-list {
  list-style: none;
  margin: 0;
  padding: 8px 16px max(16px, env(safe-area-inset-bottom));
  background: var(--bg);
  color: var(--muted);
  font-size: 0.8rem;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* Contained to its own box - without this, scrolling the feed past its own
     end/start can chain into the sibling #stat-chart (and vice versa),
     visually "scrolling" a panel the user never touched. Found live: the
     chart moved when only the feed was scrolled. */
  overscroll-behavior: contain;
}
/* Single-bar home-vs-away comparison, one row per stat - a single track split
   into two coloured segments (proportional to each side's share of that
   stat's total), not two separate bars, per Shaun's reference image. Every
   enabled stat shows by default (same set as the button grid, Goal/Behind
   included this time - the reference image shows them too) with a filter
   (icon top-right of this panel) to narrow to a quarter and/or a stat
   subset. */
/* Standalone card, matching .timeline-panel (a shared border-left used to
   join them flush - now spaced apart, each is its own widget). Title
   reinstated in v2.43 (removed, then wanted back) - the filter icon opens as
   its own small floating popover (see .chart-filter-popover). Same phone/
   tablet split as .timeline-panel: flush on phone, card-styled from the
   tablet breakpoint up. */
.stat-chart { display: flex; flex-direction: column; flex: 1; min-height: 0; }
@media (min-width: 768px) {
  .stat-chart {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
  }
}
.stat-chart-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 4px 8px 4px 12px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  position: relative;
}
.stat-chart-title { font-size: 0.85rem; font-weight: 700; color: var(--text); }
#stat-chart-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--bg);
  padding: 12px 16px;
}
.stat-chart-row { margin-bottom: 16px; }
.stat-chart-row:last-child { margin-bottom: 0; }
/* Ground Map widget - third card alongside Timeline/Performance, same
   docked-card treatment (see .timeline-panel/.stat-chart above): flush on
   phone, card-styled from the tablet breakpoint up. Previously hidden
   outright below 768px (display:none) - unlike Timeline/Performance, which
   both stack visibly on phone via .bottom-panels' flex-direction:column -
   so a spectator (or anyone) on an iPhone saw a "Ground Map" toggle in the
   Widgets drawer that did nothing regardless of its state, since the panel
   itself was never going to appear at that width no matter what. Confirmed
   this wasn't spectator-specific - the same display:none applied to every
   role on a narrow viewport. Per Shaun's report 2026-08-31.
   */
.ground-map-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; }
@media (min-width: 768px) {
  .ground-map-panel {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
  }
}
.ground-map-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 4px 8px 4px 12px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  position: relative;
}
.ground-map-title { font-size: 0.85rem; font-weight: 700; color: var(--text); }
/* Stat Recording off + Timeline/Performance/Ground Map all on (see
   applyUiToggles in app.js) - Timeline fills a full-height left column,
   Performance and Ground Map stack in a right column, instead of the default
   three-equal-columns row that leaves each one cramped once there's this
   much spare vertical room. Only meaningful once the three-widget row itself
   exists (min-width:768px - Performance/Ground Map never render below that). */
@media (min-width: 768px) {
  .bottom-panels.timeline-focus-layout {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr 1fr;
  }
  .bottom-panels.timeline-focus-layout .timeline-panel {
    grid-column: 1;
    grid-row: 1 / 3;
  }
  .bottom-panels.timeline-focus-layout .stat-chart {
    grid-column: 2;
    grid-row: 1;
  }
  .bottom-panels.timeline-focus-layout .ground-map-panel {
    grid-column: 2;
    grid-row: 2;
  }
}
/* Row wrapping the oval (button/svg) plus the two attack-direction arrows -
   see renderGroundMapArrows() in app.js. flex:1/min-height:0 hand the
   .ground-map-panel column's remaining space down to .ground-map-body-btn
   inside, same as before this row existed. align-items:stretch (was
   center) - center sized .ground-map-body-btn to its own CONTENT height,
   which used to be (however unreliably) established by its normal-flow SVG
   child; now that the SVG is position:absolute (see its own comment), it
   contributes nothing to content height at all, and center collapsed the
   button to just its own padding (16px) - which then made the absolutely-
   positioned SVG inside it tiny too, sized against that near-zero box.
   stretch makes the button fill the row's actual height regardless of
   content, which is what it always needed. The arrow spans stretching too
   is harmless - each already centers its own icon internally via its own
   nested flex, so it still reads as vertically centered either way. */
.ground-map-body-row { display: flex; align-items: stretch; gap: 4px; flex: 1; min-height: 0; }
.ground-map-direction-arrow {
  flex-shrink: 0;
  width: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* position:relative - the anchor for .ground-map-svg's absolute sizing
   below. min-width/min-height:0 stay too (belt and suspenders against the
   same flex-item auto-minimum-size issue, even though the real fix is the
   child no longer being percentage-sized at all - see that comment). */
.ground-map-body-btn {
  position: relative;
  flex: 1;
  min-height: 0;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px;
  border: none;
  background: none;
  cursor: pointer;
  overflow: hidden;
}
/* Confirmed live 2026-09-01 (desktop/tablet width, only one sibling widget -
   or none - showing alongside Ground Map, so its own share of the row is
   unusually wide): the oval grew taller than .ground-map-panel's box and
   was cropped top and bottom by #live-screen's own fixed-height
   overflow:hidden, not by this card's own border. Root cause: this SVG's
   width:100%/height:100% were CSS percentages, and percentage-height only
   resolves against a containing block whose height was set explicitly -
   not one this flex item's OWN cross-axis stretch just happened to resolve
   to at layout time - so height:100% silently computed as auto instead,
   letting the SVG grow to its viewBox's own aspect ratio regardless of the
   box actually available. position:absolute + inset:0 sidesteps that
   entirely (it always sizes against the nearest positioned ancestor's
   padding box, not a percentage lookup), and object-fit:contain then does
   the same "fit the whole oval without distortion, letterbox if needed"
   job the SVG's own default preserveAspectRatio was already doing - just
   now the box it's fitting into can never exceed what's actually there. */
.ground-map-svg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.ground-map-dot-label {
  font-size: 34px;
  font-weight: 700;
  fill: #ffffff;
  text-anchor: middle;
  dominant-baseline: central;
  pointer-events: none;
}
.ground-map-overlay-card { max-width: 900px; width: 94vw; }
.ground-map-overlay-svg { width: 100%; height: auto; display: block; }
/* Anchors the overlay's own filter popover directly under its filter icon,
   independent of .modal-header (shared by every other modal, left alone). */
.ground-map-overlay-filter-wrap { position: relative; display: inline-flex; }

.stat-chart-numbers { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 4px; }
.stat-chart-num { font-size: 1.05rem; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; min-width: 20px; }
.stat-chart-num.away { text-align: right; }
.stat-chart-label { font-size: 0.7rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; text-align: center; flex: 1; }
.stat-chart-bar-track { display: flex; height: 6px; border-radius: 3px; overflow: hidden; background: var(--panel-alt); }
.stat-chart-bar-home { height: 100%; background: var(--home); }
.stat-chart-bar-away { height: 100%; background: var(--away); }
/* Filter icon + bottom-sheet (icon-btn already gives it no border/background -
   same borderless-icon treatment as the show/hide toggle). */
.filter-chip-row { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 16px; }
.filter-chip {
  padding: 8px 16px;
  border-radius: 20px;
  border: 1px solid var(--border);
  background: var(--panel-alt);
  color: var(--text);
  font-size: 0.9rem;
  font-weight: 600;
}
.filter-chip.active { background: var(--accent); border-color: var(--accent); color: #1a1200; }
/* Small floating popover anchored under the filter icon (.stat-chart-header
   is its position:relative anchor) - deliberately NOT the bottom-sheet
   .modal pattern used elsewhere, since this is meant to read as a compact
   dropdown-style picker, not a full dialog. */
.chart-filter-popover {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 6px;
  width: min(260px, 80vw);
  max-height: min(60vh, 360px);
  overflow-y: auto;
  background: var(--panel);
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
  padding: 12px 14px;
  z-index: 50;
}
.chart-filter-popover-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.chart-filter-popover-title { font-size: 0.85rem; font-weight: 700; color: var(--text); }
/* Each event reads as its own tile (not a line of plain running text), with a
   left accent bar in the team's own colour so which side an event belongs to
   is visible at a glance. Notes aren't team-specific, so they get a neutral
   bar instead. Tappable rows (re-attribution) no longer get an underline -
   coaches learn the pattern from use, and the dotted underline just read as
   visual noise on every row once the tile treatment made rows this distinct
   already. */
.recent-list li {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  margin-bottom: 6px;
  border-radius: 8px;
  background: var(--panel-alt);
  border-left: 4px solid var(--border);
}
.recent-list li:last-child { margin-bottom: 0; }
.recent-list li.feed-home { border-left-color: var(--home); }
.recent-list li.feed-away { border-left-color: var(--away); }
.recent-list li.tappable { cursor: pointer; }
/* #stat-events-modal's list reuses .recent-list's row look (same left-accent
   chip styling as the Timeline feed) but sits inside a plain .modal-card,
   which already scrolls itself at 80vh - .recent-list's own flex:1/overflow
   rules assume a flex-column widget parent (see .timeline-panel) that isn't
   there here, so this neutralises them back to plain block flow instead. */
.stat-events-list { flex: none; max-height: none; overflow: visible; margin-top: 12px; }
.stat-chart-row.tappable { cursor: pointer; }
/* Player/ground-position status indicator cluster on a feed row - grey/black/
   green glance icons only, not their own tap target (the row's own click
   handles re-attribution for player AND position together). margin-left:auto
   on the WRAPPER (not the row's own gap, and not each icon individually)
   pushes the whole cluster to the row's right edge regardless of how short
   the label text is, while the icons inside stay tight against each other. */
.feed-status-icons {
  display: flex;
  align-items: center;
  gap: 2px;
  margin-left: auto;
  flex-shrink: 0;
}
.feed-status-icon {
  flex-shrink: 0;
  padding: 2px;
  min-width: 20px;
  min-height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Modal */
/* z-index must beat .mid-bar's 100 - .mid-bar is position:fixed with an
   explicit z-index, so without one here it was winning the stacking order
   over every dialog (Squad PDF's Export/Cancel, the reporting panel's
   actions, etc.), leaving their bottom buttons visually covered by the tier-1
   bar and unusable. Still below .ui-toggle-panel's 200, so that drawer can
   still sit above an open modal if both were ever open at once. */
.modal {
  position: fixed;
  inset: 0;
  z-index: 150;
  background: rgba(0,0,0,0.6);
  display: flex;
  align-items: flex-end;
}
.modal-card {
  width: 100%;
  max-height: 80vh;
  overflow-y: auto;
  background: var(--panel);
  border-radius: 16px 16px 0 0;
  padding: 16px;
}
.modal-header { display: flex; justify-content: space-between; align-items: center; }
.modal-header-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; justify-content: flex-end; }
.small-btn { flex: none; width: auto; padding: 8px 14px; font-size: 0.85rem; }

/* Show/hide drawer - slides in from the right, distinct from the bottom-sheet
   .modal pattern used everywhere else in this app (a lateral slide reads as
   "panel of settings" rather than "a dialog to act on/dismiss"). */
.ui-toggle-panel { position: fixed; inset: 0; z-index: 200; }
.ui-toggle-backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.5); }
.ui-toggle-drawer {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(300px, 82vw);
  background: var(--panel);
  box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3);
  padding: 16px;
  overflow-y: auto;
  animation: ui-toggle-slide-in 0.2s ease;
}
@keyframes ui-toggle-slide-in {
  from { transform: translateX(100%); }
  to { transform: translateX(0); }
}
.ui-toggle-drawer .settings-row:last-child { border-bottom: none; }

/* Above every sheet/picker (200), the widgets drawer (200) and the filter
   card (400), below toasts (1000) - a confirm can be raised from inside any
   of those (e.g. the wrong-end check while the Ground Position picker is
   up) and was rendering UNDER the picker at .modal's 150 (Shaun 2026-09-24). */
#confirm-modal { align-items: center; justify-content: center; z-index: 500; }
.confirm-card { max-width: 340px; width: 90%; border-radius: 16px; text-align: center; }
#confirm-message { margin: 4px 0 20px; font-size: 1rem; }
.confirm-actions { display: flex; gap: 10px; }
.confirm-actions button { flex: 1; margin: 0; }

#video-record-modal { align-items: center; justify-content: center; }
.video-record-card { max-width: 420px; width: 92vw; border-radius: 16px; text-align: center; }
.video-record-preview {
  width: 100%;
  max-height: 60vh;
  border-radius: 12px;
  background: #000;
  object-fit: cover;
}
.video-record-status { margin: 10px 0 4px; font-weight: 700; color: var(--accent); }
.video-record-zoom-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 8px 0 16px;
  font-size: 0.85rem;
  color: var(--muted);
}
.video-record-zoom-row input[type="range"] { flex: 1; }
#video-record-zoom-label { min-width: 34px; text-align: right; font-variant-numeric: tabular-nums; }

/* Landscape phone - a portrait-stacked layout (preview, then status/zoom/
   buttons all below it) easily runs taller than a landscape phone's own
   viewport height, pushing Record off-screen behind a scroll. max-height
   (not just orientation) targets an actual short/landscape viewport
   specifically, not a landscape TABLET which has plenty of vertical room to
   spare and doesn't need the row treatment. The title is dropped here too -
   "Record Video" isn't worth the row of vertical space it costs when every
   pixel here is fighting for room. */
@media (orientation: landscape) and (max-height: 500px) {
  #video-record-modal .subtitle { display: none; }
  .video-record-card {
    display: flex;
    align-items: stretch;
    gap: 12px;
    max-width: 96vw;
    width: 96vw;
    max-height: 94vh;
    overflow: visible;
  }
  .video-record-card .video-record-preview {
    flex: 1 1 55%;
    width: auto;
    max-width: 55%;
    height: 100%;
    max-height: 94vh;
  }
  .video-record-controls {
    flex: 1 1 45%;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }
  .video-record-status { margin: 0 0 8px; }
  .video-record-zoom-row { margin: 0 0 12px; }
}

#video-lightbox-modal { align-items: center; justify-content: center; }
.video-lightbox-card { max-width: 480px; width: 92vw; border-radius: 16px; }
.video-lightbox-player { width: 100%; max-height: 70vh; border-radius: 8px; background: #000; margin-top: 8px; }
/* Wider than .confirm-card and left-aligned - two full textarea fields never
   fit .confirm-card's 340px/centred layout without cramping and (per the
   labels being centred over multi-line fields) looking broken. Title/intro
   stay centred to read as a heading; the form fields below are left-aligned. */
.debrief-card { max-width: 480px; width: 92vw; border-radius: 16px; text-align: left; }
.debrief-card > .subtitle, .debrief-card > .stats-callout { text-align: center; }
.recap-share-block { margin-top: 4px; padding-top: 16px; border-top: 1px solid var(--border); }
.icon-btn {
  background: none;
  border: none;
  color: var(--text);
  font-size: 1.5rem;
  /* 44px is the standard minimum comfortable tap target - the glyph alone was
     noticeably harder to hit reliably than the rest of the sheet's buttons. */
  min-width: 44px;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.settings-close-bottom { margin-top: 18px; }
.settings-subheading { margin: 18px 0 4px; font-size: 0.9rem; color: var(--muted); }
/* Settings tab bar - one panel visible at a time instead of one long scroll.
   .hidden (display:none !important) still outranks .active, so the existing
   host/joined show-hide logic on squad/targets keeps working unchanged. */
.settings-tabs {
  display: flex;
  gap: 4px;
  overflow-x: auto;
  margin: 4px 0 14px;
  border-bottom: 1px solid var(--border);
}
/* Flat segmented nav, not six boxed buttons - these switch which section of
   the SAME sheet you're looking at, not six separate actions, so only the
   active one gets a mark (an underline) rather than every tab getting its
   own button-like box regardless of state. */
.settings-tab {
  flex: 1;
  white-space: nowrap;
  padding: 8px 10px 10px;
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  color: var(--muted);
  font: inherit;
  font-size: 0.78rem;
}
.settings-tab.active {
  color: var(--text);
  border-bottom-color: var(--accent);
  font-weight: 600;
}
/* Fixed height regardless of which tab is active - without this the whole
   modal-card resized on every tab switch (a 3-row Match tab vs. an 8-row Stats
   list), which read as the settings sheet "jumping around", worst on iPad
   where the taller viewport made the jump more obvious. Each panel scrolls
   internally if its own content overflows. */
.settings-tab-panel { display: none; }
/* padding-right (and the matching negative margin so the panel's own left
   edge doesn't shift) gives the scrollbar its own lane - without it, the
   scrollbar this panel grows when its content overflows sat flush against
   field/button right edges with no gap, and could overlap them entirely on
   platforms whose scrollbar overlays content rather than reserving space. */
.settings-tab-panel.active {
  display: block;
  height: 50vh;
  overflow-y: auto;
  padding-right: 10px;
  margin-right: -10px;
}
#end-match-btn { margin-top: 18px; }
.settings-list { margin-top: 12px; }
.settings-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}
.settings-toggle { display: flex; align-items: center; gap: 8px; flex: 1; }
/* The recorder's "You are <Name>" row reads as a single sentence, not a
   label-on-the-left/control-on-the-right pair like every other settings row -
   the name belongs directly after the label, not pushed to the far edge. */
.settings-row-inline { justify-content: flex-start; gap: 8px; }
.settings-stat-row { gap: 8px; }
.settings-stat-row.dragging { opacity: 0.5; background: var(--panel-alt); }
/* Pointer Events, not the HTML5 native drag-and-drop API - native DnD has no touch
   support on iOS Safari (this app's primary platform), so a draggable="true" handle
   would simply not work when actually dragged with a finger. touch-action: none
   stops the page itself from scrolling underneath a touch-drag on the handle. */
.drag-handle {
  min-width: 28px;
  flex-shrink: 0;
  text-align: center;
  cursor: grab;
  touch-action: none;
  color: var(--muted);
  font-size: 1.1rem;
  line-height: 1;
  user-select: none;
  -webkit-user-select: none; /* iOS Safari needs the prefixed form */
  -webkit-touch-callout: none; /* and long-press callout suppressed explicitly */
}
.settings-list-header {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--muted);
  padding-bottom: 4px;
  /* Matches .settings-stat-row's own gap below - without it these columns
     sat flush against each other with zero breathing room, and "Position"/
     "Momentum Weight" (each wider than their own fixed-width box at this
     font size) visibly bled into their neighbours. */
  gap: 8px;
}
.settings-list-header span:first-child { width: 28px; flex-shrink: 0; }
.settings-list-header span:nth-child(2) { flex: 1; }
.settings-list-header span:last-child {
  width: 80px;
  text-align: right;
  flex-shrink: 0;
  line-height: 1.15;
}
/* Free Kick's own extra per-match option (not a 5th column on the shared row
   above, which would misalign every other stat's row against the 4-column
   header) - a small indented sub-row directly under that one stat only. The
   parent row's own border is suppressed so the pair reads as one grouped
   block with a single divider beneath it, not two boxed-in rows stacked with
   a cramped extra line squeezed between them. */
.settings-stat-row:has(+ .settings-substat-row) {
  border-bottom: none;
  padding-bottom: 2px;
}
.settings-substat-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 2px 0 10px 36px;
  font-size: 0.8rem;
  color: var(--muted);
  border-bottom: 1px solid var(--border);
}
/* "Player" column: whether recording the stat prompts for a player. Font-
   size/line-height only apply to the HEADER label ("Player"/"Position") -
   this class is never shared with the .stat-trackplayer/.stat-trackposition
   checkboxes below, so their own 44px width (matched here for alignment)
   is untouched. Slightly smaller than the header row's own 0.75rem so
   "Position" (the wider of the two words) fits its 44px column without
   overflowing into "Momentum Weight" beside it. */
.trackplayer-header { width: 44px; text-align: center; flex-shrink: 0; font-size: 0.65rem; line-height: 1.15; }
.stat-trackplayer { width: 44px; flex-shrink: 0; accent-color: var(--accent); }
.stat-trackposition { width: 44px; flex-shrink: 0; accent-color: var(--accent); }
.stat-weight {
  width: 64px;
  padding: 6px;
  text-align: right;
}

.danger-btn {
  width: 100%;
  margin: 8px 0 4px;
  padding: 12px;
  border-radius: 10px;
  border: 1px solid var(--danger);
  background: transparent;
  color: var(--danger);
  font-size: 0.95rem;
  font-weight: 600;
}

/* Match stats breakdown */
.stats-section { margin-bottom: 22px; }
.stats-section-title { margin: 0 0 6px; font-size: 1rem; }
.stats-callout {
  margin: 0 0 10px;
  padding: 8px 10px;
  border-radius: 8px;
  background: var(--panel-alt);
  color: var(--muted);
  font-size: 0.85rem;
  line-height: 1.5;
}
.stats-table { display: flex; flex-direction: column; }
.stats-row {
  display: grid;
  grid-template-columns: 1fr 70px 70px;
  align-items: center;
  padding: 8px 4px;
  border-bottom: 1px solid var(--border);
  font-size: 0.9rem;
}
.stats-row .stats-home, .stats-row .stats-away { text-align: center; font-variant-numeric: tabular-nums; }
.stats-header { font-weight: 600; font-size: 0.8rem; color: var(--muted); border-bottom: 2px solid var(--border); }
.stats-score-row { font-weight: 700; }
/* Was coloured per-team (var(--home)/var(--away)) - unreadable whenever a
   coach's chosen team colour was itself pale/low-contrast against the dark
   panel background. Team identity is already established by column position
   and the header row above; a plain readable colour for names, with just the
   leading number picked out in accent yellow, reads better regardless of
   which colour either team picked. */
.stats-row .stats-lead { font-weight: 700; color: var(--accent); }

.player-stats-wrap { overflow-x: auto; }
.player-stats-table { border-collapse: collapse; width: 100%; font-size: 0.85rem; white-space: nowrap; }
.player-stats-table th, .player-stats-table td { padding: 6px 8px; text-align: center; border-bottom: 1px solid var(--border); }
.player-stats-table th:nth-child(2), .player-stats-table td:nth-child(2) { text-align: left; }
.player-stats-table thead th { font-weight: 600; font-size: 0.75rem; color: var(--muted); border-bottom: 2px solid var(--border); }
.incident-note { color: var(--danger); font-size: 0.72rem; font-weight: 600; }

/* Toast */
.toast {
  position: fixed;
  left: 50%;
  bottom: max(24px, env(safe-area-inset-bottom));
  transform: translate(-50%, 20px);
  background: var(--panel-alt);
  color: var(--text);
  padding: 12px 20px;
  border-radius: 10px;
  font-size: 0.9rem;
  box-shadow: 0 4px 16px rgba(0,0,0,0.4);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease;
  z-index: 1000;
  /* Most toasts here are short confirmations that fit on one line - added
     for the longer diagnostic video-upload-error message, which needs to
     wrap and stay on-screen on a narrow phone rather than running off the
     edge or getting clipped. */
  max-width: 90vw;
  white-space: normal;
  text-align: center;
}
.toast.visible { opacity: 1; transform: translate(-50%, 0); }

/* Team Mode */
#match-pin-display { font-size: 1.3rem; letter-spacing: 2px; color: var(--accent); }
.recorder-card {
  margin-top: 10px;
  padding: 10px;
  border-radius: 10px;
  background: var(--panel-alt);
  border: 1px solid var(--border);
}
.recorder-card-title { font-weight: 600; margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.recorder-card-title-right { display: flex; align-items: center; gap: 10px; }
.recorder-last-seen { font-size: 0.75rem; font-weight: 400; color: #58c08a; }
.recorder-last-seen.stale { color: var(--danger); font-weight: 600; }
.recorder-remove-btn { color: var(--danger); }
.recorder-cell-grid {
  display: grid;
  grid-template-columns: 1.6fr 1fr 1fr;
  align-items: center;
  gap: 8px 12px;
}
.recorder-cell-header { font-weight: 600; font-size: 0.8rem; text-align: center; color: var(--muted); }
.recorder-cell-stat { font-size: 0.85rem; }
.recorder-cell { display: flex; align-items: center; justify-content: center; font-size: 0.8rem; color: var(--muted); }

/* Player tracking: roster, squad, picker, bench */
/* Single column at every width (Shaun 2026-09-25) - two columns never had
   room for number + full name + Edit + toggle on a phone, let alone inside
   the 560px-capped sheet on a tablet. */
.roster-list { display: grid; grid-template-columns: 1fr; gap: 0 10px; }
.roster-list .stats-callout { grid-column: 1 / -1; }
.roster-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.9rem;
}
.roster-row.inactive { opacity: 0.45; }
.roster-row .roster-num { min-width: 34px; font-weight: 700; font-variant-numeric: tabular-nums; }
.roster-row .roster-name { flex: 1; }
/* Editing spans both roster-list columns - the inline form needs the room a
   single half-width column doesn't have. */
.roster-row.editing { grid-column: 1 / -1; flex-wrap: wrap; gap: 6px; }
.roster-row.editing input {
  flex: 1 1 90px;
  min-width: 0;
  padding: 8px;
  font: inherit;
  font-size: 0.85rem;
}
.roster-row.editing input.roster-edit-number { flex: 0 0 60px; }
.roster-add-row { display: flex; gap: 6px; margin-top: 10px; align-items: center; }
.roster-add-row input[type="number"] { width: 56px; }
.roster-add-row input {
  padding: 10px;
  font-size: 0.9rem;
  min-width: 0;
  flex: 1;
}
.roster-add-row input[type="number"] { flex: 0 0 56px; }
.roster-import-row { display: flex; gap: 8px; margin-top: 10px; }
.roster-import-btn { flex: 1; margin: 0; }
.team-logo-row { display: flex; align-items: center; gap: 10px; justify-content: flex-end; }
.team-logo-preview { width: 36px; height: 36px; object-fit: contain; border-radius: 6px; background: var(--panel-alt); cursor: pointer; }
#logo-lightbox-modal { align-items: center; justify-content: center; }
.logo-lightbox-card {
  position: relative;
  max-width: 90vw;
  max-height: 85vh;
  padding: 24px;
  border-radius: 16px;
  background: var(--panel);
}
.logo-lightbox-card .icon-btn { position: absolute; top: 4px; right: 4px; }
#logo-lightbox-img {
  display: block;
  max-width: 100%;
  max-height: 70vh;
  border-radius: 8px;
  /* Checkerboard, same trick image editors use to preview transparency - a
     genuinely transparent background shows the checker through; an
     accidentally-opaque one (the black-background upload bug) obviously
     doesn't, making this the fastest way to confirm a logo is correct. */
  background-image:
    linear-gradient(45deg, #808080 25%, transparent 25%),
    linear-gradient(-45deg, #808080 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, #808080 75%),
    linear-gradient(-45deg, transparent 75%, #808080 75%);
  background-size: 20px 20px;
  background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
  background-color: #fff;
}
.myteam-identity-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 16px;
  padding: 14px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--panel-alt);
}
.myteam-identity-card .team-logo-row { justify-content: flex-start; }
.myteam-identity-card #myteam-logo-preview {
  width: 44px;
  height: 44px;
  /* Editing now only happens via "Edit team & roster" (which has its own
     click-to-enlarge logo) - this one is a plain status display, not a
     control, so it shouldn't look tappable. */
  cursor: default;
}
.myteam-name-display {
  width: 100%;
  font-weight: 600;
  font-size: 1.05rem;
  text-align: left;
}
.schedule-match-btn { margin-top: 14px; }
.practice-mode-btn { margin-top: 14px; }
.photo-roster-rows { display: flex; flex-direction: column; gap: 8px; max-height: 50vh; overflow-y: auto; margin: 10px 0; }
.photo-roster-row { display: flex; gap: 6px; align-items: center; }
.photo-roster-row input[type="number"] { flex: 0 0 56px; }
.photo-roster-row input[type="text"] { flex: 1; min-width: 0; }
.photo-roster-row input {
  padding: 10px;
  font-size: 0.9rem;
}
/* Sticky pill, not plain text above the list - the list is long enough on an
   18+ player roster that the count (previously just sitting above it) had
   scrolled off screen by the time the coach was ticking players near the
   bottom, with no way to see it again without scrolling back up. Sticking
   it to the top of the settings modal's own scroll area keeps it visible
   the whole time the squad list is being worked through. */
.squad-selected-count {
  position: sticky;
  top: 0;
  z-index: 5;
  display: inline-flex;
  width: fit-content;
  align-items: center;
  gap: 6px;
  padding: 6px 14px;
  margin: 0;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 0.8rem;
  font-weight: 700;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.squad-count-row {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin: 0 0 8px;
  /* Solid background behind the whole sticky row (not just the pill), so
     the list underneath doesn't visibly scroll through the gap next to it. */
  background: var(--panel);
  padding: 4px 0;
}
.squad-list { display: grid; grid-template-columns: 1fr; gap: 0 10px; }
.squad-list .stats-callout { grid-column: 1 / -1; }
.squad-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.9rem;
}
.myteam-toggle { display: flex; gap: 6px; }
.myteam-toggle .account-subtab { flex: none; padding: 6px 14px; }
#squad-save-btn { width: 100%; margin-top: 12px; }

/* Attribution picker - bottom sheet over the stat columns; the stat is already
   recorded, this only decorates it, so Skip/tap-out/timeout all cost nothing. */
.player-picker {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 200;
  background: var(--panel);
  border-top: 2px solid var(--accent);
  padding: 12px 14px max(14px, env(safe-area-inset-bottom));
  box-shadow: 0 -6px 24px rgba(0, 0, 0, 0.5);
}
.player-picker-title { font-size: 0.9rem; color: var(--muted); margin-bottom: 10px; }
/* Sort-order toggle in #player-picker's own title row only (mirrors
   #wind-picker's ID-scoped flex override above the shared block-level base
   rule) - "#" (jumper number) vs "A-Z" (name), persisted as this device's
   default for next time (see playerPickerSort in app.js). */
#player-picker .player-picker-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.player-picker-sort-row { display: flex; gap: 6px; flex-shrink: 0; }
.player-sort-chip { padding: 4px 12px; font-size: 0.8rem; }
.player-picker-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 8px;
  max-height: 40vh;
  overflow-y: auto;
}
.player-pick-btn {
  min-height: 56px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--panel-alt);
  color: var(--text);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
}
.player-pick-btn .pick-num { font-size: 1.2rem; font-weight: 700; }
.player-pick-btn .pick-name { font-size: 0.72rem; color: var(--muted); }
.player-pick-btn.off-field { opacity: 0.5; }
#player-picker-skip { width: 100%; margin-top: 10px; }

/* Best Players grid - reuses .player-picker-grid's layout, own row-shaped
   button (name left, vote count right) rather than .player-pick-btn's
   number-over-name column, since there's no "off field" concept here. */
.best-player-grid { grid-template-columns: 1fr; max-height: 50vh; gap: 6px; }
.best-player-btn {
  min-height: 48px;
  border-radius: 12px;
  border: none;
  background: var(--control);
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 14px;
  font-size: 0.9rem;
  font-weight: 600;
}
/* Vote tally as a neutral-inverted pill (the same "selected/count" treatment
   as the filter pills), hidden entirely at zero so unvoted rows stay quiet. */
.best-player-count { display: inline-flex; align-items: center; height: 24px; padding: 0 10px; border-radius: 999px; background: var(--invert-fill); color: var(--invert-text); font-size: 0.8rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.best-player-count:empty { display: none; }
#best-player-picker .drawer-hint { margin: 0 4px 10px; }

/* Free kick reason picker - same bottom sheet, but text-label buttons instead
   of player number/name, and a wider minimum column since labels like
   "Insufficient Intent" don't fit the player grid's 96px minimum. */
.freekick-reason-grid { grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); }
.reason-pick-btn {
  min-height: 48px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--panel-alt);
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 6px 8px;
  font-size: 0.8rem;
  line-height: 1.25;
}
#freekick-reason-skip { width: 100%; margin-top: 10px; }

/* Card duration picker - same bottom sheet/button style as the free kick
   reason grid above, three wide choices instead of many narrow ones. */
.card-duration-grid { grid-template-columns: repeat(3, 1fr); }
.card-duration-custom-row { display: flex; gap: 8px; margin-top: 10px; }
.card-duration-custom-input {
  flex: 1;
  min-height: 48px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--panel-alt);
  color: var(--text);
  padding: 6px 12px;
  font-size: 1rem;
  text-align: center;
}
#card-duration-cancel { width: 100%; margin-top: 10px; }

/* Ground position picker - same bottom-sheet shell as .player-picker, but the
   grid of buttons is replaced with a tap-anywhere oval diagram. Attack-
   direction arrows either side reuse .ground-map-direction-arrow's sizing
   (see renderGroundPickerArrows() in app.js). */
.ground-picker-oval-wrap { display: flex; align-items: center; justify-content: center; gap: 4px; }
.ground-picker-svg { width: 100%; max-width: 480px; max-height: 45vh; cursor: crosshair; touch-action: none; }
#ground-picker-skip { width: 100%; margin-top: 10px; }
/* Wind picker reuses .player-picker/.ground-picker wholesale - only its own
   title row (needs a close button, unlike ground-picker's plain text title)
   needs its own rule. Wind + Conditions are wrapped in matching .weather-
   section cards (same background/radius/padding/heading style) so the modal
   reads as ONE interface, not an oval graphic with some buttons bolted on
   underneath. */
#wind-picker .player-picker-title { display: flex; align-items: center; justify-content: space-between; }
.weather-section {
  background: var(--panel-alt);
  border-radius: 12px;
  padding: 12px;
  margin-top: 10px;
}
.weather-section:first-of-type { margin-top: 0; }
.weather-section-title {
  margin: 0 0 8px;
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.weather-section-hint { margin: 8px 0 0; font-size: 0.8rem; color: var(--muted); text-align: center; }
/* Drag cue, not tap - distinct from #ground-picker-svg's plain crosshair. */
#wind-picker-svg { cursor: grab; }
#wind-picker-svg:active { cursor: grabbing; }
/* Weather-condition icons - each its own immediate one-tap action (see
   saveWeatherNote() in app.js), not part of the Wind section's Save step
   above. Buttons match the Wind section's Save/No-wind pair in height/radius
   so both cards feel like the same control family. */
.weather-conditions-row { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin-top: 8px; }
.weather-cond-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 10px 8px;
  min-width: 62px;
  min-height: 48px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  font-size: 0.68rem;
  font-weight: 600;
}
.weather-cond-btn.active { border-color: var(--accent); background: var(--accent); color: var(--on-accent); }

/* Bench board */
/* Two columns to halve scrolling on an 18+ squad - each row is still a single
   full tap target. */
.bench-list { display: grid; grid-template-columns: 1fr 1fr; gap: 0 10px; }
.bench-row-wrapper { display: flex; align-items: center; border-bottom: 1px solid var(--border); }
.bench-row {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 10px 2px;
  border: none;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 0.88rem;
  text-align: left;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.bench-row:active { background: var(--panel-alt); }
/* Injured/Yellow/Red action row - appears exactly once per Position Picker
   sheet, at the top (RHS of the Sort by row via #position-picker-actions,
   see index.html) for whichever player currently occupies the slot this
   sheet was opened for - not per row in the switch-candidate grid below.
   Replaces the old single tiny bench-injury-btn/oval-slot-injury-btn
   overlay icon (see its own git-history comment for the tap-target
   struggle that motivated this) - three real, separately-tappable buttons
   instead of one icon bolted onto a card's corner. */
.bench-action-row { display: flex; gap: 8px; }
.bench-action-btn {
  flex: none;
  min-width: 40px;
  min-height: 40px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--panel-alt);
  display: flex;
  align-items: center;
  justify-content: center;
}
.bench-action-btn:active { background: var(--panel); }
/* In-match flag on the bench/interchange list for a player currently off
   injured or carded (not yet returned) - the incident itself is only ever
   recorded post-match in the Player Stats/CSV "Incident" column, which a
   coach can't see mid-game; this makes the open incident visible right
   where the bench is actually being managed. Clears itself the moment
   they're brought back on (see benchRows()'s injuredOff/cardOff, which
   reset to false on the next ON event), so no separate "returned" action is
   needed. */
.bench-injury-flag, .bench-card-flag {
  flex: none;
  display: flex;
  align-items: center;
  white-space: nowrap;
}
.bench-row.pending-swap { background: var(--panel-alt); outline: 2px solid var(--accent); border-radius: 8px; }
.bench-row .bench-num { min-width: 30px; font-weight: 700; font-variant-numeric: tabular-nums; }
.bench-row .bench-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bench-row .bench-time { color: var(--muted); font-variant-numeric: tabular-nums; font-size: 0.72rem; text-align: right; }
.bench-warning { color: var(--danger); font-size: 0.8rem; font-weight: 600; }

/* AFL-oval position board - tablet/desktop only. Phones (below the breakpoint)
   keep the plain On Field / Bench list above; this is the first @media
   breakpoint in the app, so #bench-modal also gets its own wide-screen
   presentation (centred dialog instead of a bottom sheet, more headroom) since
   the oval needs real estate the phone bottom-sheet pattern doesn't have. */
.bench-oval-view { display: none; }
.position-pick-bench { grid-column: 1 / -1; background: var(--panel); border-color: var(--danger); }
.position-pick-bench .pick-name { color: var(--danger); font-size: 0.85rem; }
.player-pick-btn .pick-tag { font-size: 0.65rem; color: var(--muted); margin-top: 2px; }
/* Reporting panel - replaces the row of 8-9 small buttons that used to sit
   in the Match Stats header (Report/Timeline/Squad PDF/CSV/Events/Players
   CSV/AI Summary/Debrief/Share all crammed in at once). One list, one place. */
.reporting-card { max-width: 360px; width: 90%; text-align: left; }
.reporting-list { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.reporting-subheading-first { margin-top: 0; }
.reporting-item {
  width: 100%;
  padding: 14px 16px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--panel-alt);
  color: var(--text);
  font-size: 0.95rem;
  font-weight: 600;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}
.reporting-item:active { background: var(--panel); }

.position-picker-card { max-width: 480px; width: 92vw; text-align: left; }
.position-picker-card .player-picker-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); max-height: 60vh; }
.position-picker-sort { display: flex; align-items: center; gap: 8px; margin: 4px 0 12px; }
.position-picker-sort-label { font-size: 0.8rem; color: var(--muted); }
.position-picker-sort .account-subtab { flex: none; padding: 6px 14px; }
/* Pushes the Injured/Yellow/Red action row (see buildBenchActionButtons in
   app.js) to the right end of this same row, per Shaun 2026-09-01. Empty
   when the slot has no occupant - margin-left:auto still applies but there's
   nothing to actually push since the div is empty, so this is harmless. */
.position-picker-actions { margin-left: auto; }
/* Primary/secondary swap with the chosen sort - whichever field the list is
   actually ordered by reads as the heading, the other as supporting detail. */
.pick-primary { font-size: 1.1rem; font-weight: 700; color: var(--text); }
.pick-secondary { font-size: 0.72rem; color: var(--muted); }
.player-pick-btn.current-position { outline: 2px solid #e8c547; outline-offset: -2px; }
/* On-field vs bench divider in the slot-tap picker - the .off-field opacity
   dim alone read as too subtle a separator, so this adds a literal rule
   spanning the full grid width right at the on-field/bench boundary. */
.position-picker-divider { grid-column: 1 / -1; border-top: 1px solid var(--border); margin: 4px 0; }
.pick-injury-flag { display: inline-flex; vertical-align: middle; margin-left: 4px; }

@media (min-width: 768px) {
  /* Opaque, not the phone bottom-sheet's rgba(0,0,0,0.6) - that alpha let the
     live screen behind (momentum/score worm charts included) show through
     faintly wherever the now much bigger, variable-height card didn't fully
     cover the backdrop, looking like a stray scribble/watermark. A centred
     dialog this size reads better fully opaque anyway. */
  #bench-modal { align-items: center; justify-content: center; background: var(--bg); }
  #bench-modal .modal-card {
    width: 92vw;
    max-width: 960px;
    height: 90vh;
    max-height: 90vh;
    border-radius: 16px;
    display: flex;
    flex-direction: column;
  }
  .bench-list-view { display: none; }
  .bench-oval-view { display: flex; flex-direction: column; flex: 1; min-height: 0; }

  /* stretch (not the default/previous flex-start) so the oval and the side
     boxes always share the same height - the oval no longer sits short with
     dead space below/around it regardless of squad size or interchange count. */
  .oval-layout { display: flex; gap: 18px; align-items: stretch; flex: 1; min-height: 0; }
  .oval-field {
    position: relative;
    flex: 1;
    min-width: 0;
    padding: 28px 20px;
  }
  /* The real ground-oval SVG (same shapes as Ground Map, rotated) sits behind
     .oval-rows as a plain absolute background layer - it never receives
     pointer events itself, so tapping through to the position tiles above it
     is unaffected. */
  .oval-field-bg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
  }
  /* height:100% + space-between - the 5 rows spread across the field's full
     height instead of stacking at the top with a small fixed gap, which used
     to leave a band of empty oval below the forward line whenever the field
     was taller than 5 rows' minimum content height. */
  .oval-rows { position: relative; height: 100%; display: flex; flex-direction: column; justify-content: space-between; }
  .oval-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
  .oval-slot-wrapper { position: relative; width: 100%; }
  .oval-slot {
    background: rgba(255, 255, 255, 0.94);
    /* Fixed light-grey, not var(--border) - this board is a fixed-look module
       regardless of app theme (see the turf-green/text-colour comment above).
       Corner positions (Forward/Back Pocket etc) sit outside the oval's green
       fill, over the surrounding white card background - without a border a
       tile there was the exact same white as what's behind it and read as
       invisible, not just low-contrast. */
    border: 1px solid #d5dbe6;
    border-radius: 8px;
    padding: 6px 4px;
    min-height: 50px;
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    text-align: center;
    cursor: pointer;
    /* Defensive, not load-bearing now that GT/on is always shown on the card
       itself rather than needing a long-press - a fast double-tap shouldn't
       trigger iOS/Android's text-selection loupe or "Copy" callout, even
       though nothing here times a press anymore. (v1.84 tried pairing this
       with touch-action:none + a touchstart preventDefault to kill the
       long-press gesture outright - that combination broke click synthesis
       on iOS instead and was reverted in v1.85; not needed at all now that
       the gesture it was defending doesn't exist.) */
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
  }
  .oval-slot:active { background: #fff; }
  .oval-slot.empty { background: rgba(255, 255, 255, 0.55); }
  .oval-slot-label { font-size: 0.62rem; color: #4a5568; text-transform: uppercase; letter-spacing: 0.02em; }
  .oval-slot-player { font-size: 0.82rem; font-weight: 700; color: #17202a; }
  .oval-slot.empty .oval-slot-player { font-weight: 400; font-style: italic; color: #6b7684; }
  /* Constantly visible now - there's room for it on iPad, so it no longer
     needs to hide behind a tap-to-reveal clock icon. */
  .oval-slot-time { font-size: 0.62rem; line-height: 1.3; color: #5c6b80; }

  .oval-side-boxes { display: flex; flex-direction: column; gap: 14px; width: 200px; flex-shrink: 0; }
  .oval-box { background: var(--panel-alt); border-radius: 10px; padding: 10px; }
  .oval-box h4 { margin: 0 0 8px; font-size: 0.85rem; color: var(--muted); }
  .oval-followers { display: flex; flex-direction: column; gap: 6px; }
  .oval-followers .oval-slot { align-items: flex-start; text-align: left; padding: 8px 10px; min-height: 0; }
  /* Interchange soaks up whatever height Followers doesn't need, with its own
     list independently scrollable within that (same bounded-flex pattern as
     the Timeline modal's feed list) - no more fixed 220px cap regardless of
     how tall the card ends up. */
  .oval-box:last-child { flex: 1; min-height: 0; display: flex; flex-direction: column; }
  .oval-interchange-list { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow-y: auto; }
  .oval-interchange-row { padding: 6px 2px; -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
}

@media (min-width: 1100px) {
  #bench-modal .modal-card { max-width: 1180px; }
  .oval-side-boxes { width: 240px; }
}
.squad-reminder {
  display: block;
  width: calc(100% - 24px);
  margin: 8px 12px 0;
  padding: 10px 12px;
  border: 1px dashed var(--accent);
  border-radius: 10px;
  background: var(--panel-alt);
  color: var(--text);
  font: inherit;
  font-size: 0.85rem;
  text-align: left;
}
.squad-reminder.hidden { display: none; }
/* Spectators/recorders never see it (host-only condition in JS), but belt and braces: */
.screen.spectator-mode .squad-reminder { display: none !important; }

/* Teams */
.team-select {
  width: 100%;
  margin: 2px 0 10px;
  padding: 8px;
  font: inherit;
  font-size: 0.85rem;
}
.settings-row .team-select { width: 55%; margin: 0; }
.team-add-row input { flex: 1; }

/* Match details (settings Match tab) */
.detail-input {
  width: 160px;
  padding: 6px 8px;
  font: inherit;
  font-size: 0.85rem;
}
/* Combined selector beats .detail-input alone on specificity regardless of
   declaration order - Location/Weather need the extra room a bare class after
   .detail-input in the cascade was silently losing to. */
.detail-input.detail-input-wide { width: 220px; }

/* Recorder role permissions (bench control / match notes) */
.recorder-roles { display: flex; gap: 16px; margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--border); }
.recorder-role { display: flex; align-items: center; gap: 6px; font-size: 0.85rem; color: var(--text); }
.recorder-role input { accent-color: var(--accent); }

/* Spectator pre-game countdown */
.spectator-countdown {
  margin: 8px 12px 0;
  padding: 10px 12px;
  border-radius: 10px;
  background: var(--panel-alt);
  border: 1px solid var(--accent);
  color: var(--text);
  font-size: 1rem;
  font-weight: 600;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* Goalkickers summary - under the momentum worm, above the stat columns.
   Two columns (home left, away right), just the kickers - no "Goals" label
   or team name, since the side + team colours elsewhere already say which
   team is which. The flex `gap` is the whitespace gutter keeping the two
   lists visually separate instead of reading as one run-on line. */
.goalkickers {
  margin: 6px 12px 10px;
  padding: 6px 10px;
  border-radius: 8px;
  background: var(--panel-alt);
  font-size: 0.8rem;
  color: var(--text);
  display: flex;
  justify-content: space-between;
  gap: 20px;
}
.gk-col { flex: 1 1 0; min-width: 0; }
/* Both columns read left-aligned, same as any normal list - text-align:right
   on the away column put its FIRST kicker at the far right edge with wraps
   growing leftward, which read backwards rather than as a plain name list.
   The column still sits on the right side of the row via flex layout alone. */
.gk-home, .gk-away { text-align: left; }

/* Interactive synced timeline - now its own dedicated modal (was inline on
   the spectator screen, but that pushed the chart off-screen the moment the
   feed below it needed scrolling - see the modal-card override below for the
   actual fix: a real bounded-height flex layout instead of a page that grows
   to fit everything and lets the WHOLE page scroll). */
.timeline-modal-card {
  height: 85vh;
  max-height: 85vh;
  /* dvh accounts for a mobile browser's address bar actually being on
     screen - plain vh is measured against the layout viewport (address bar
     hidden), so on a phone with the bar showing, the real visible area is
     shorter than 85vh and this bottom-anchored card's top (the header, with
     the only close button) got pushed above the visible screen with no way
     to scroll back to it - the SVG below captures touch-action for its own
     drag-to-scrub gesture, so there wasn't even a scroll gesture free to
     reveal it. Falls back to vh on browsers that don't support dvh. */
  height: 85dvh;
  max-height: 85dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden; /* the feed list scrolls internally instead - see below */
}
/* Belt-and-braces on top of the dvh fix above: pin the close button to the
   actual visual viewport regardless of where the card itself ends up, and
   let tapping the dimmed backdrop close it too (see the click handler in
   app.js) - two independent, viewport-clipping-proof ways out. */
#timeline-modal-close {
  position: fixed;
  top: max(10px, env(safe-area-inset-top));
  right: 14px;
  z-index: 65;
  background: var(--panel);
  border-radius: 50%;
}
.match-timeline-readout {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 0.8rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  margin: 4px 0 2px;
  flex-shrink: 0;
}
.timeline-live-btn {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  border-radius: 12px;
  color: var(--muted);
  font: inherit;
  font-size: 0.75rem;
  padding: 3px 10px;
}
.timeline-live-btn.active { color: var(--danger); border-color: var(--danger); font-weight: 600; }
.match-timeline-svg {
  width: 100%;
  height: 90px;
  display: block;
  flex-shrink: 0;
  touch-action: none;
  cursor: ew-resize;
}
.timeline-qlabel { fill: var(--muted); font-size: 11px; font-weight: 600; }
.timeline-home-area { fill: var(--home); opacity: 0.55; }
.timeline-away-area { fill: var(--away); opacity: 0.55; }
.timeline-score-line { fill: none; stroke: var(--text); stroke-width: 1.4; opacity: 0.85; }
.timeline-goal-home { fill: var(--home); }
.timeline-goal-away { fill: var(--away); }
.timeline-divider { stroke: var(--border); stroke-width: 1; }
.timeline-playhead-line { stroke: var(--accent); stroke-width: 2; }
.timeline-playhead-handle { fill: var(--accent); }
/* The feed - the ONLY thing inside this modal that scrolls (flex:1 + its own
   overflow-y + min-height:0, the classic fix that lets a flex child actually
   shrink instead of growing to fit its content like the old inline version did). */
.timeline-feed-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  font-size: 0.85rem;
  color: var(--muted);
}
.timeline-feed-list li {
  padding: 8px 4px;
  border-bottom: 1px solid #232d45;
  cursor: pointer;
}
.timeline-feed-list li.timeline-highlight { background: var(--panel-alt); border-radius: 6px; }
.timeline-feed-list li.timeline-quarter-break {
  background: #000;
  color: #fff;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-align: center;
  text-transform: uppercase;
  padding: 10px 4px;
  cursor: default;
  border-bottom: 1px solid #232d45;
}

/* Spectator goal celebration - big, brief, self-dismissing */
.goal-celebration {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  background: rgba(10, 14, 26, 0.82);
  z-index: 60;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
  text-align: center;
}
.goal-celebration.show { opacity: 1; }
.goal-logo { display: block; width: 88px; height: 88px; object-fit: contain; margin: 0 auto 4px; }
.goal-celebration .goal-word {
  font-size: 3.2rem;
  font-weight: 800;
  letter-spacing: 4px;
  color: var(--accent);
  animation: goal-pop 0.5s ease;
}
.goal-celebration .goal-team { font-size: 1.4rem; font-weight: 700; }
.goal-celebration .goal-player { font-size: 1.1rem; color: var(--muted); }
@keyframes goal-pop {
  0% { transform: scale(0.4); }
  70% { transform: scale(1.15); }
  100% { transform: scale(1); }
}

/* Item 29 - spectator tap-to-cheer + crowd heat. No data value, pure delight -
   a floating button so it never competes with the shared score/clock layout,
   and a small pill that fades in only while there's recent activity. */
.cheer-btn {
  position: fixed;
  right: 16px;
  /* Was bottom:max(16px, safe-area) - didn't know about .mid-bar (also
     position:fixed, bottom:0, z-index:100) sitting in that exact same
     region with a HIGHER z-index, so the cheer button rendered behind it -
     invisible, not just cramped - confirmed on iPhone as a spectator (per
     Shaun 2026-08-31; .mid-bar's own safe-area reserve was never the gap,
     since --mid-bar-height already bakes that in - see applyMidBarHeightVar
     in app.js). Anchoring off the same --mid-bar-height var every other
     bottom-reserving element already uses clears the real bar instead of a
     guessed flat offset. */
  bottom: calc(var(--mid-bar-height, 60px) + 16px);
  z-index: 55;
  padding: 12px 18px;
  border-radius: 999px;
  border: none;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 1rem;
  font-weight: 700;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
  cursor: pointer;
  transition: transform 0.1s ease;
}
.cheer-btn:active { transform: scale(0.92); }
.crowd-heat {
  position: fixed;
  top: max(10px, env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  z-index: 55;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 14px;
  border-radius: 999px;
  background: rgba(10, 14, 26, 0.85);
  color: #fff;
  font-weight: 700;
  font-size: 0.9rem;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s ease;
}
.crowd-heat.show { opacity: 1; }
#crowd-heat-emoji { animation: crowd-heat-pulse 0.6s ease infinite; }
@keyframes crowd-heat-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.25); }
}

/* Spectator: the recent feed becomes the full match feed using the free space
   below Match Stats (stat buttons are hidden, so the room is there). */
.screen.spectator-mode .recent-list {
  max-height: none;
  flex: 1;
  font-size: 0.85rem;
}
.screen.spectator-mode .recent-list li { padding: 10px 12px; }

.swap-banner {
  margin: 0 0 8px;
  padding: 8px 10px;
  border-radius: 8px;
  background: var(--panel-alt);
  border: 1px solid var(--accent);
  color: var(--text);
  font-size: 0.85rem;
}
.count-step-btn {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  font: inherit;
  padding: 0 8px;
  margin: 0 4px;
  line-height: 1.4;
}

/* Admin/history */
.history-list { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; max-height: 340px; overflow-y: auto; }
.season-btn { width: 100%; margin-top: 12px; }
.history-archive-btn { color: var(--muted); }
.history-empty { margin: 0; padding: 8px 10px; color: var(--muted); font-size: 0.85rem; text-align: center; }
.history-row-wrapper { display: flex; align-items: center; gap: 8px; }
.history-row {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--panel-alt);
  color: var(--text);
  text-align: left;
  font: inherit;
}
.history-row-teams { font-weight: 600; font-size: 0.95rem; display: flex; align-items: center; gap: 6px; }
.history-row-logo { width: 20px; height: 20px; object-fit: contain; border-radius: 4px; flex-shrink: 0; }
.history-row-meta { color: var(--muted); font-size: 0.8rem; text-transform: capitalize; }
.history-row-meta-live { color: var(--accent); font-weight: 600; }
.history-share-btn { flex-shrink: 0; }

/* Item 31 - Coach Access (My Team) */
.myteam-share-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 10px;
  background: var(--panel-alt);
  border-radius: 8px;
  font-size: 0.9rem;
}
.myteam-share-add-row { display: flex; gap: 8px; margin-top: 10px; }
.myteam-share-add-row input { flex: 1; }

/* Public, no-login match recap (?recap=<matchId>) */
.recap-card {
  margin: 24px auto;
  width: min(92vw, 480px);
  background: var(--panel);
  border-radius: var(--radius);
  padding: 20px;
  text-align: left;
}
.recap-eyebrow { font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); font-weight: 600; }
.recap-section-label { margin: 18px 0 8px; }
.recap-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.recap-title { font-size: 1.1rem; font-weight: 700; margin-top: 2px; }
.recap-meta { font-size: 0.8rem; color: var(--muted); margin-top: 2px; }
.recap-margin-block { text-align: right; flex-shrink: 0; }
.recap-margin-value { font-size: 1.3rem; font-weight: 700; }
.recap-margin-value.recap-negative { color: var(--danger); }
.recap-margin-value.recap-positive { color: var(--accent); }
.recap-score-band {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--panel-alt);
  border-radius: 10px;
  padding: 14px 16px;
  margin: 14px 0;
}
.recap-score-side { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.recap-score-side-right { align-items: flex-end; }
.recap-team-logo { width: 22px; height: 22px; object-fit: contain; border-radius: 4px; }
.recap-score-name { font-size: 0.85rem; font-weight: 600; }
.recap-score-value { font-size: 1.4rem; font-weight: 700; }
.recap-score-vs { font-size: 0.75rem; color: var(--muted); }
.recap-tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.recap-tile { background: var(--panel-alt); border-radius: 10px; padding: 10px 12px; }
.recap-tile-label { font-size: 0.75rem; color: var(--muted); }
.recap-tile-value { font-size: 1rem; font-weight: 700; margin: 2px 0; }
.recap-tile-value span { font-size: 0.8rem; font-weight: 400; color: var(--muted); }
.recap-tile-verdict { font-size: 0.75rem; color: var(--accent); }
.recap-table-wrap { overflow-x: auto; }
.recap-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
.recap-table th { text-align: right; color: var(--muted); font-weight: 600; padding: 4px 6px; }
.recap-table th:first-child, .recap-table td:first-child { text-align: left; }
.recap-table td { text-align: right; padding: 5px 6px; border-top: 1px solid var(--border); }
.recap-debrief-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.recap-bullet-list { margin: 0; padding-left: 18px; font-size: 0.85rem; line-height: 1.6; }
.recap-message { background: var(--panel-alt); border-radius: 10px; padding: 12px 14px; font-size: 0.85rem; line-height: 1.6; margin-top: 18px; }
.recap-footer { display: flex; justify-content: flex-end; margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--border); }
@media (max-width: 420px) {
  .recap-tiles, .recap-debrief-grid { grid-template-columns: 1fr; }
}

/* ------------------------------------------------------------------------
   Design refresh 2026-09-24 - shared native-feel controls (switches,
   segmented controls, filter popover, sheet anatomy). Deliberately appended
   so these win over the older rules above at equal specificity; the
   surfaces they restyle first are the Widgets drawer, the Weather sheet and
   the three widget filters, per the approved canvas mockups. */

/* Text-style buttons (sheet Done / Cancel / Reset) */
.text-btn { background: none; border: none; padding: 6px 4px; color: var(--accent); font-size: 1rem; font-weight: 600; cursor: pointer; }
.text-btn-muted { color: var(--muted); }
.text-btn-small { font-size: 0.85rem; padding: 4px; }

/* Sheet anatomy */
.sheet-grabber { display: block; width: 36px; height: 5px; border-radius: 3px; background: var(--control); margin: 0 auto 10px; }
.sheet-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.sheet-heading { display: flex; flex-direction: column; gap: 2px; }
.sheet-title { margin: 0; font-size: 1.3rem; font-weight: 700; letter-spacing: -0.2px; color: var(--text); }
.eyebrow { font-size: 0.7rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
.player-picker { border-top: none; border-radius: var(--radius-sheet) var(--radius-sheet) 0 0; box-shadow: 0 -18px 50px rgba(0, 0, 0, 0.45); }

/* iOS-style switch. The <input> itself is the control, so every existing
   checked/change handler keeps working unchanged - only the paint differs. */
input[type="checkbox"].switch,
.squad-row input[type="checkbox"],
.recorder-role input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  width: 51px;
  height: 31px;
  margin: 0;
  flex-shrink: 0;
  border: none;
  border-radius: 16px;
  background: var(--switch-off);
  transition: background 0.2s ease;
  cursor: pointer;
}
input[type="checkbox"].switch::after,
.squad-row input[type="checkbox"]::after,
.recorder-role input[type="checkbox"]::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 27px;
  height: 27px;
  border-radius: 50%;
  background: #ffffff;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
  transition: left 0.2s ease;
}
input[type="checkbox"].switch:checked,
.squad-row input[type="checkbox"]:checked,
.recorder-role input[type="checkbox"]:checked { background: var(--accent); }
input[type="checkbox"].switch:checked::after,
.squad-row input[type="checkbox"]:checked::after,
.recorder-role input[type="checkbox"]:checked::after { left: 22px; }
.chart-filter-popover input[type="checkbox"].switch { width: 44px; height: 26px; }
.chart-filter-popover input[type="checkbox"].switch::after { width: 22px; height: 22px; }
.chart-filter-popover input[type="checkbox"].switch:checked::after { left: 20px; }

/* Grouped list (Widgets drawer): one rounded card, [icon][label][switch]
   rows, hairline separators inset to the label column. */
.settings-group { background: var(--panel-alt); border-radius: var(--radius); overflow: hidden; }
.settings-group .settings-row { position: relative; border-bottom: none; padding: 0 14px; min-height: 54px; }
.settings-group .settings-row:not(:last-child)::after { content: ''; position: absolute; left: 14px; right: 0; bottom: 0; border-bottom: 1px solid var(--separator); }
.settings-row-icon { display: flex; align-items: center; gap: 12px; }
.row-icon { width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center; color: var(--muted); flex-shrink: 0; }
.row-icon svg { width: 20px; height: 20px; }
.row-label { flex: 1; font-size: 0.95rem; font-weight: 500; }
.drawer-hint { margin: 14px 4px 0; font-size: 0.75rem; line-height: 1.45; color: var(--muted); }
/* Flex column so Done can sit pinned to the foot of the drawer (same
   full-width primary button as the filter cards' Done - one affordance for
   "I'm finished" everywhere, per Shaun 2026-09-24) while the group above
   keeps its natural height; margin-top:auto only pushes when there's spare
   room, so on a short screen the button simply follows the hint. */
.ui-toggle-drawer { display: flex; flex-direction: column; padding: max(16px, env(safe-area-inset-top)) 16px max(24px, env(safe-area-inset-bottom)); }
.ui-toggle-drawer .drawer-hint { margin-bottom: 16px; }
.ui-toggle-drawer .filter-done-btn { margin-top: auto; flex-shrink: 0; }

/* Segmented control. .filter-chip-row is always a mutually-exclusive set
   (quarters, team, sort), so it gets equal-width segments rather than a
   wrapping row of pills - always balanced, never a ragged right edge. The
   selected segment is neutral inversion, never a colour, so it can't clash
   with whichever team colours are on screen. A quarter that hasn't been
   played yet is disabled (see syncQuarterChipAvailability in app.js). */
.filter-chip-row { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 2px; padding: 3px; border-radius: var(--radius-control); background: var(--control); margin: 6px 0 12px; }
.filter-chip { padding: 7px 4px; border-radius: 8px; border: none; background: transparent; color: var(--muted); font-size: 0.85rem; font-weight: 700; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.filter-chip.active { background: var(--invert-fill); border-color: var(--invert-fill); color: var(--invert-text); }
.filter-chip:disabled { opacity: 0.35; cursor: default; }
.player-picker-sort-row { display: grid; grid-auto-flow: column; gap: 2px; padding: 3px; border-radius: 8px; background: var(--control); }

/* Filter popover - one anatomy for Performance, Timeline and Ground Map:
   Filter / Reset header, then eyebrow-labelled sections. */
/* Centred in the viewport, not anchored under the widget's own header - the
   widgets live at the bottom of the screen, so an anchored popover on an
   iPhone opened half off-screen and needed a scroll to reach (Shaun
   2026-09-24). Only one filter is ever open at a time, so a single centred
   card with a dimmed backdrop reads correctly as "the filter". The backdrop
   is a huge box-shadow spread rather than a real element: shadows don't
   take clicks, so a tap on the dimmed area still reaches whatever's
   underneath and the existing outside-click handler (wireFilterPopover-
   OutsideClick in app.js) closes the card exactly as before. z-index sits
   above the ground-map overlay sheet (.modal) so the overlay's own filter
   still surfaces on top of it. */
.chart-filter-popover {
  position: fixed;
  top: 50%;
  left: 50%;
  right: auto;
  transform: translate(-50%, -50%);
  margin: 0;
  width: min(340px, 92vw);
  max-height: min(78vh, 600px);
  border-radius: 16px;
  padding: 10px 12px 12px;
  background: var(--panel);
  border: none;
  box-shadow: 0 0 0 100vmax rgba(8, 12, 20, 0.5), 0 18px 48px rgba(0, 0, 0, 0.45);
  z-index: 400;
}
.chart-filter-popover-header { height: 30px; padding: 0 4px; margin-bottom: 4px; }
.chart-filter-popover-title { font-size: 0.9rem; }
.chart-filter-popover .settings-subheading { margin: 10px 4px 4px; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
.chart-filter-popover .settings-row { padding: 6px 4px; border-bottom: none; min-height: 40px; font-size: 0.9rem; }
.team-row-label { display: flex; align-items: center; gap: 8px; min-width: 0; }
.team-row-label span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.team-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.team-dot-home { background: var(--home); }
.team-dot-away { background: var(--away); }

/* Widget header: title + current-filter pill on the left, filter button
   with a count badge on the right; the button inverts while its popover is
   open. */
.widget-title-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
.widget-title-row h2 { margin: 0; }
.filter-pill { display: inline-flex; align-items: center; gap: 5px; height: 22px; padding: 0 8px; border-radius: 999px; background: var(--control); color: var(--text); font-size: 0.7rem; font-weight: 700; white-space: nowrap; }
.filter-pill:empty { display: none; }
.filter-pill-sep { display: inline-block; width: 3px; height: 3px; border-radius: 50%; background: var(--muted); }
.filter-pill .team-dot + .team-dot { margin-left: -2px; }
.filter-btn { position: relative; border-radius: var(--radius-control); }
.filter-btn.filter-open { background: var(--invert-fill); color: var(--invert-text); }
.filter-badge { position: absolute; top: -5px; right: -6px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px; background: var(--accent); color: var(--on-accent); font-size: 0.62rem; font-weight: 700; line-height: 16px; text-align: center; }
.filter-badge:empty { display: none; }
.filter-done-btn { margin-top: 12px; padding: 0; height: 44px; font-size: 0.95rem; border-radius: var(--radius-control); }

/* Weather sheet: two matching cards (eyebrow + one small control in each
   header row), icon-only condition tiles with the chosen name read out
   beneath, one primary Save. */
.weather-section { border-radius: var(--radius); }
.weather-section-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 28px; margin-bottom: 8px; }
.weather-section-head .weather-section-title { margin: 0; }
.weather-section-note { font-size: 0.75rem; color: var(--muted); text-align: right; }
.weather-readout { margin: 8px 0 0; font-size: 1.05rem; font-weight: 700; text-align: center; color: var(--text); }
.weather-section-hint { margin-top: 4px; }
.chip-btn { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 12px; border-radius: 999px; border: none; background: var(--control); color: var(--text); font-size: 0.75rem; font-weight: 600; }
.chip-btn.active { background: var(--invert-fill); color: var(--invert-text); }
.weather-conditions-row { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; margin-top: 0; }
.weather-cond-btn { min-width: 0; min-height: 56px; padding: 0; border: none; border-radius: 12px; background: var(--control); color: var(--text); display: flex; align-items: center; justify-content: center; }
.weather-cond-btn.active { border-color: var(--invert-fill); background: var(--invert-fill); color: var(--invert-text); }
#wind-picker .primary-btn { margin-top: 14px; }

/* --- Sheets: every .modal-card bottom sheet (Settings, Match Stats, AI
   Coach, Reporting, Timeline, Ground Map overlay...). Grabber, sheet radius,
   grey-circle dismiss. Settings and the report sheets carry their own Save
   buttons per section, so a single bottom Done doesn't fit them the way it
   fits the drawer and filter cards - the grey circle is the sheet-wide
   close affordance instead. */
.modal-card { border-radius: var(--radius-sheet) var(--radius-sheet) 0 0; padding: 10px 16px max(16px, env(safe-area-inset-bottom)); box-shadow: 0 -18px 50px rgba(0, 0, 0, 0.45); }
.modal-card::before { content: ''; display: block; width: 36px; height: 5px; border-radius: 3px; background: var(--control); margin: 0 auto 10px; }
/* Centred dialogs are not sheets - no grabber, all four corners rounded. */
.modal-card.confirm-card { border-radius: 16px; padding: 16px; box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45); }
.modal-card.confirm-card::before { display: none; }
.modal-header { min-height: 40px; margin-bottom: 8px; }
.modal-header h2 { margin: 0; font-size: 1.3rem; font-weight: 700; letter-spacing: -0.2px; }
.modal-header .icon-btn { width: 32px; height: 32px; min-width: 32px; min-height: 32px; padding: 0; border-radius: 50%; background: var(--control); color: var(--muted); font-size: 1.15rem; line-height: 1; display: inline-flex; align-items: center; justify-content: center; }
.modal-header .icon-btn.filter-open { background: var(--invert-fill); color: var(--invert-text); }
.modal-header .secondary-btn.small-btn { border: none; background: var(--control); border-radius: 999px; padding: 6px 12px; font-size: 0.8rem; font-weight: 600; }

/* Hairline separators wherever a list still drew the heavier border. */
.settings-row, .squad-row, .stats-row, .changelog-entry { border-bottom-color: var(--separator); }

/* Settings tabs as a segmented control - same grammar as the quarter
   filters. Six segments fit a 375px phone at this size (hence the shorter
   "Team" label). */
/* Segments size to their labels and share any spare width equally; on a
   phone the six labels ("Team Sheet" etc.) overrun the track, so it scrolls
   sideways rather than truncating - never an ellipsized tab. */
.settings-tabs { display: flex; gap: 2px; padding: 3px; border-radius: var(--radius-control); background: var(--control); border-bottom: none; overflow-x: auto; scrollbar-width: none; margin: 4px 0 14px; }
.settings-tabs::-webkit-scrollbar { display: none; }
.settings-tab { flex: 1 0 auto; padding: 7px 10px; border: none; border-bottom: none; margin-bottom: 0; border-radius: 8px; background: transparent; color: var(--muted); font-size: 0.78rem; font-weight: 700; white-space: nowrap; }
.settings-tab.active { background: var(--invert-fill); color: var(--invert-text); border-bottom-color: transparent; }

/* Two/three-way toggles (Left end / Right end, My team / Opposition, Light /
   Dark / System, Name / Number, Log In / Create Account): neutral chips,
   inverted when selected - never the accent. */
.account-subtab { border: none; background: var(--control); color: var(--muted); border-radius: 8px; font-weight: 700; }
.account-subtab.active { background: var(--invert-fill); color: var(--invert-text); border-color: transparent; }
.account-subtabs { gap: 4px; }

/* Compact switches for the per-stat tracking columns and other in-row
   toggles - the two 44px columns keep their header alignment. Selectors
   carry the same specificity as the base switch rule so they win by order. */
input[type="checkbox"].switch.stat-enable, input[type="checkbox"].switch.stat-trackplayer, input[type="checkbox"].switch.stat-trackposition, .settings-substat-row input[type="checkbox"], .squad-row input[type="checkbox"], .recorder-role input[type="checkbox"] { width: 44px; height: 26px; }
input[type="checkbox"].switch.stat-enable::after, input[type="checkbox"].switch.stat-trackplayer::after, input[type="checkbox"].switch.stat-trackposition::after, .settings-substat-row input[type="checkbox"]::after, .squad-row input[type="checkbox"]::after, .recorder-role input[type="checkbox"]::after { width: 22px; height: 22px; }
input[type="checkbox"].switch.stat-enable:checked::after, input[type="checkbox"].switch.stat-trackplayer:checked::after, input[type="checkbox"].switch.stat-trackposition:checked::after, .settings-substat-row input[type="checkbox"]:checked::after, .squad-row input[type="checkbox"]:checked::after, .recorder-role input[type="checkbox"]:checked::after { left: 20px; }

/* Match Stats: eyebrow section titles, one rounded card per table. */
.stats-section-title { margin: 0 0 8px; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
.stats-callout { border-radius: 12px; padding: 10px 12px; }
.stats-table { background: var(--panel-alt); border-radius: var(--radius); padding: 2px 12px; }
.stats-row { padding: 9px 2px; }
.stats-row:last-child { border-bottom: none; }
.stats-header { border-bottom: 1px solid var(--separator); }

/* AI Coach / match summary text as a card. */
.summary-text { margin-top: 8px; padding: 14px 16px; background: var(--panel-alt); border-radius: var(--radius); }

/* Reporting list rows. */
.reporting-item { border: none; background: var(--control); border-radius: 12px; }
.reporting-item:active { background: var(--panel-alt); }

/* Settings footer: with the redundant bottom Close gone (the header's grey
   circle is the close), Leave Match sits full width and stacked like the
   danger buttons beneath it instead of inline beside a button that no
   longer exists. */
.settings-footer-btn { display: flex; align-items: center; justify-content: center; gap: 6px; width: 100%; margin: 18px 0 4px; }

/* Targets tab: a stat switched off on the Stats tab stays listed, greyed
   and locked, rather than silently vanishing. */
.target-row-disabled { opacity: 0.4; }
.target-row-disabled select, .target-row-disabled input { pointer-events: none; }

/* Team Mode sub-tabs (Stat collectors / Spectators). */
.team-mode-subtabs { margin: 0 0 14px; }

/* End-of-match actions now sit at the foot of the Match tab (Shaun
   2026-09-25) rather than as a footer under every tab. */
.settings-exit-actions { padding-bottom: 6px; }
.settings-exit-actions .settings-subheading { margin-top: 24px; }
.settings-exit-actions .settings-footer-btn { margin: 8px 0 4px; }
#end-match-btn { margin-top: 8px; }

/* Best Players: Votes / Summary views. The summary is a read-only whole-match
   table - thumbs up (inverted pill, same as the Votes tally), then goals,
   behinds and tackles per player, rows with nothing recorded dimmed. */
.sheet-header .best-player-views { margin: 0; align-self: center; flex: none; }
.best-player-views .account-subtab { flex: none; padding: 6px 14px; font-size: 0.8rem; }
.best-player-summary { max-height: 50vh; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }
.best-summary-row { display: grid; grid-template-columns: minmax(0, 1fr) repeat(var(--summary-cols, 3), 44px); align-items: center; min-height: 44px; padding: 6px 14px; border-radius: 12px; background: var(--control); font-size: 0.9rem; font-weight: 600; font-variant-numeric: tabular-nums; text-align: center; }
.best-summary-row .best-summary-name { text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.best-summary-head { background: transparent; min-height: 24px; padding-top: 0; padding-bottom: 0; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
.best-summary-votes:not(:empty) { justify-self: center; display: inline-flex; align-items: center; height: 24px; padding: 0 10px; border-radius: 999px; background: var(--invert-fill); color: var(--invert-text); font-size: 0.8rem; font-weight: 700; }
.best-summary-quiet { opacity: 0.5; }

/* Tablet/laptop trial (Shaun 2026-09-25): bottom sheets cap at 560px and sit
   centred instead of spanning the full width, so full-width Done/Save
   buttons stop stretching across an iPad. Centred dialogs keep their own
   narrower widths; the data-heavy cards (Timeline, Ground Map overlay,
   Bench) keep their wide layouts. The Widgets drawer is a side panel and
   is untouched. */
@media (min-width: 768px) {
  .modal { justify-content: center; }
  .modal-card:not(.confirm-card):not(.video-lightbox-card):not(.debrief-card):not(.reporting-card):not(.ground-map-overlay-card):not(.timeline-modal-card) { max-width: 560px; }
  .player-picker { left: 50%; right: auto; width: min(560px, 100%); transform: translateX(-50%); }
}

/* Settings sheet at a fixed 80% of the screen (Shaun 2026-09-25) rather
   than the 50vh tab panel plus header, which left the sheet just over half
   height on every tab. The card is a flex column so the active tab panel
   takes whatever is left under the header and tab strip, and scrolls
   internally as before. dvh where supported so a phone's address bar
   doesn't push the header off the top. */
#settings-modal .modal-card { height: 80vh; height: 80dvh; max-height: 80vh; max-height: 80dvh; display: flex; flex-direction: column; overflow: hidden; }
#settings-modal .modal-card::before, #settings-modal .modal-header, #settings-modal .settings-tabs { flex: none; }
#host-settings-sections { display: flex; flex-direction: column; flex: 1; min-height: 0; }
#host-settings-sections .settings-tab-panel.active { flex: 1; height: auto; min-height: 0; }
#recorder-settings-section { flex: 1; min-height: 0; overflow-y: auto; }

/* iOS Home Screen layout-viewport shortfall (see syncViewportFix in app.js):
   the document is sized to the real screen height and body - transformed so
   it becomes the containing block for position:fixed - stands in for the
   too-short viewport, so the bottom bar, sheets, drawers and backdrops all
   anchor to the true bottom instead of stopping a status-bar height short.
   Only ever set on the live screen, which never scrolls as a page. */
html.vp-short, html.vp-short body { height: var(--app-h); overflow: hidden; }
html.vp-short body { position: relative; transform: translateZ(0); }
html.vp-short #live-screen { height: var(--app-h); }

/* Settings > Sharing > Team page: members of a protected team page. */
.teampage-member { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 0; border-bottom: 1px solid var(--separator); }
.teampage-member-info { min-width: 0; flex: 1; }
.teampage-member-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.teampage-member-meta { margin-top: 2px; font-size: 0.78rem; color: var(--muted); overflow-wrap: anywhere; }
.teampage-chip { padding: 2px 8px; border-radius: 999px; font-size: 0.68rem; font-weight: 700; background: var(--control); color: var(--muted); }
.teampage-chip.chip-pending { background: var(--accent); color: var(--on-accent); }
.teampage-chip.chip-disabled { background: transparent; color: var(--danger); border: 1px solid var(--danger); }
.teampage-member.status-disabled .teampage-member-info { opacity: 0.6; }
.teampage-member-actions { display: flex; gap: 6px; flex: none; }
.secondary-btn.teampage-danger { color: var(--danger); }

/* My Team: team details line, switcher, New Team button, and the New Team screen. */
.myteam-meta { margin: 6px 0 0; font-size: 0.85rem; color: var(--muted); }
.myteam-new-btn { width: 100%; margin-top: 12px; }
.new-team-card { max-height: 88vh; overflow-y: auto; }
.nt-search { margin: 0 0 10px; width: 100%; }
.nt-players { max-height: 40vh; overflow-y: auto; border-radius: 10px; background: var(--panel-alt); padding: 4px 12px 8px; }
.nt-group + .nt-group { margin-top: 10px; }
.nt-group-head { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 0 6px; background: var(--panel-alt); border-bottom: 1px solid var(--separator); }
.nt-group-title { font-size: 0.7rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); overflow-wrap: anywhere; }
.nt-group-tools { display: flex; gap: 12px; flex: none; }
.nt-row { display: flex; align-items: center; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--separator); }
.nt-row:last-child { border-bottom: none; }
.nt-name { flex: 1; min-width: 0; font-weight: 600; overflow-wrap: anywhere; }
.nt-inactive .nt-name { opacity: 0.55; }
.nt-num { width: 58px; flex: none; text-align: center; padding: 8px 6px; border-radius: 8px; border: 1px solid var(--separator); background: var(--control); color: var(--text); font-size: 16px; }
.nt-summary { margin: 12px 2px 0; font-size: 0.85rem; color: var(--muted); }
.nt-error { margin: 10px 2px 0; font-size: 0.86rem; font-weight: 600; color: var(--danger); }
#nt-create { margin-top: 14px; }
