/* --- Login / Signup (LoginScreen.qml / SignUpScreen.qml) ---
   Redesigned 2026-08-15: this used to sit on --bg-overlay (ghostwhite, the QML-era light
   background), the one place in the app that didn't look like the same product as the dark
   Pick/Podium/Previous/Profile screens around it. Now dark like everywhere else, with the splash
   screen's own logo/wordmark treatment reused via .splash__logo/.splash__name (see index.html's
   glow filter) so a signed-out player's very first screen already reads as this app's brand
   before they've logged into anything. */
.auth-screen {
  position: absolute;
  inset: 0;
  background: var(--bg);
  color: var(--text-on-dark);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 24px;
  overflow-y: auto;
  z-index: 25;
}
.auth-screen[hidden] { display: none; }
/* Smaller than the full-screen splash's own logo — this shares the page with a form below it
   instead of standing alone, so it reads as a header, not the whole screen's content. */
.auth-screen__logo { width: 56px; margin-top: 8px; }
.auth-screen .splash__name { font-size: 1rem; margin-bottom: 4px; }
/* Deliberately keeps the light input fields the rest of the app already uses for form fields
   (e.g. Profile's display-name field, which relies on components.css's plain .field input with
   no color override) — high-contrast typed text on a dark page, not something this redesign
   needed to change, just no longer buried under a full-page light background to match. */
.auth-screen .field input { color: #1f1f1f; background: white; }
.auth-actions { display: flex; gap: 12px; width: 100%; max-width: 300px; justify-content: space-between; }
.auth-switch { margin-top: 8px; }
.auth-switch button {
  background: none; border: none; color: var(--accent); text-decoration: underline; cursor: pointer; font-size: 0.9rem;
}
.auth-error { color: var(--fail); min-height: 1.4em; text-align: center; }

/* "or" divider between the OAuth buttons and the username/password form below them. */
.auth-divider {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  max-width: 300px;
  color: var(--text-on-dark);
  opacity: 0.5;
  font-size: 0.8rem;
  margin: 4px 0;
}
.auth-divider::before, .auth-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: currentColor;
  opacity: 0.4;
}

/* --- Week header (Podium / Previous share this shape — Pick had its own copy until the
   2026-08-16 redesign replaced it with .week-summary-bar) ---
   Made sticky 2026-08-16, once the underlying #app/#screen-root scroll-container bug (see
   base.css's own comment on #screen-root) turned out to be exactly why the Pick screen's own
   sticky headers weren't working on real iOS Safari, and got fixed there — this is that same fix
   paying off a second time, applied to the Week/Player (Previous) and Week/metric/view (Podium)
   controls so they stay reachable without scrolling back up through a long standings list or
   picks history. */
/* PINNED WITH `position: fixed`, NOT `position: sticky` - changed 0.11.25, and the reason is
   worth keeping because five deploys were spent learning it. During an iOS rubber band the page
   translates down and takes in-flow and sticky content with it, while FIXED content is held still
   by the compositor. Cancelling that movement from JS is impossible: `window.scrollY` is the main
   thread's last synced offset and the compositor runs the bounce ahead of it, so any counter-
   transform is a frame or more stale. Measured off a real screen recording: mid-drag the header
   lagged BELOW where it belonged, and during the snap-back (703px of travel in 250ms) it shot
   hundreds of px above the screen. Fixed positioning wins for free, in the compositor, with no JS
   at all - which is exactly why the bottom nav and the pull-to-refresh badge never needed help.
   The cost is that it leaves the flow, so its host reserves the space via --page-header-h below.
   `left`/`transform`/`max-width` mirror #app's own centred column (base.css) so it lines up with
   the app at every width rather than spanning the whole desktop viewport. */
.week-header {
  position: fixed;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: var(--app-max-width);
  z-index: 5;
  background: var(--bg);
  border-bottom: 5px solid var(--divider);
  padding: 10px 16px;
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
/* Reserves the space the fixed header no longer occupies. --page-header-h is measured (never
   guessed) by whichever screen mounts the header, because this header wraps to two lines at some
   widths and its content varies by view mode - screens/podium.js and screens/previous.js both
   keep it current with a ResizeObserver. The same custom property is what .week-summary-bar and
   .matchup-weekday already offset themselves by, so one measurement serves all three. */
.pinned-header-host {
  padding-top: var(--page-header-h, 0px);
}

.week-header label { color: var(--accent); font-size: 0.85rem; }
.week-header select, .week-header input[type="number"] {
  background: #2a2a2a; color: var(--text-on-dark); border: 1px solid var(--divider);
  border-radius: 6px; padding: 6px 8px;
}
.week-header .toggle-group { margin-left: auto; }
/* matchupDetailModal.js's toggle sits in a narrow modal card rather than a wide header bar, so
   its buttons need their own explicit sizing — the .week-header context never set one explicitly
   either, it just inherited ambient button sizing from that wider layout, which doesn't carry
   over here. */
.matchup-detail-modal__toggle button { font-size: 0.78rem; padding: 4px 2px; }

/* --- Matchup list (Games.qml) ---
   Redesigned 2026-08-16: locked games used to show a lock/checkered-flag icon stamped over the
   card plus two full-width colored sentence boxes below it ("BAMA won 24-17" / "is covering"),
   and "worth" was a plain, state-blind "Npts" label above the teams regardless of whether the
   game was upcoming, live, or over. Replaced with: a "week at a glance" bar pinned above the
   list (.week-summary-bar); games bucketed into Live now / Upcoming / Final sections instead of
   pure chronological order (screens/pick.js's renderMatchupList) — only Upcoming keeps sticky
   per-day headers, since that's the one section where "which day is this" is still a live
   question; a colored glow ring directly on whichever team button was actually picked outright
   (components/fancyButton.js's --verdict-correct/--incorrect) instead of a caption below the
   card, with a compact pill row (.verdict-chips) underneath for the fuller outright+spread
   breakdown the ring alone can't show; and "worth" is now a poker-chip badge
   (components/worthChip.js) that's a stake before lock and a live/final payout after. */
/* display: block, not flex — fixed 2026-08-16, the day after this redesign first shipped, once
   the sticky day headers turned out to simply not stick at all on real iOS Safari. This is a
   known long-standing WebKit bug: position: sticky is unreliable (often just inert) on a flex
   item, and .matchup-weekday/.week-summary-bar were both direct children of a `display: flex;
   flex-direction: column` list. Desktop Chrome never showed it (Chromium doesn't have the bug),
   which is exactly why this got through the earlier Playwright-screenshot verification — that
   ran headless Chromium, not WebKit. Switched to plain block flow with margin-based spacing
   (.matchup-list > * + *) instead of flex's `gap`, which sidesteps the bug entirely since
   sticky-on-a-block-child is universally reliable. Every direct child here (.week-summary-bar,
   .matchup-section-header, .matchup-weekday, .matchup-card, .final-row) was already full-width,
   so nothing here depended on flex's own alignment behavior — this is a pure layout-model swap,
   not a visual change. */
/* The element pull-to-refresh slides down to hold the feed open while a refresh runs, and the
   half of that effect that has to be CSS.
   * z-index 1 over .ptr's 0, plus an OPAQUE background, is what lets the returning content cover
     the spinner instead of passing over it like a ghost. Without the background the badge shows
     through every gap between cards.
   * The transform is set inline per gesture; --closing carries it back to 0 afterwards.
   * NOTHING `position: fixed` MAY LIVE INSIDE THIS ELEMENT - a transformed ancestor becomes the
     containing block for fixed descendants, so it would drag the pinned .week-summary-bar down
     with it. That is exactly why the bar is now appended beside the list rather than inside it
     (screens/pick.js's renderMatchupList); being fixed, its position in the DOM costs nothing. */
.refresh-slide {
  position: relative;
  z-index: 1;
  background: var(--bg);
}
.refresh-slide--closing {
  transition: transform 340ms cubic-bezier(0.22, 1, 0.36, 1);
}

.matchup-list {
  /* --list-pad-top is set by whichever screen renders the list: 20px on its own, or 46px when a
     .week-summary-bar is fixed above it (20px of original padding, plus the 40px the bar used to
     occupy and the 6px gap under it). Set explicitly rather than derived from --summary-bar-h,
     so the two numbers can never drift into arithmetic nobody can read. */
  padding: var(--list-pad-top, 20px) 16px 32px;
  display: block;
  overscroll-behavior-y: contain;
}
.matchup-list > * + * {
  margin-top: 14px;
}

/* Pinned above every section (sticky top: 0) so the running record/points total never requires
   scrolling back up through a long list to check by eye. Height is a fixed 40px (matched by
   .matchup-weekday's --summary-bar-h offset below and by screens/pick.js's own
   renderMatchupList, which sets that custom property) rather than measured at runtime — simpler,
   and the content here is short/stable enough that a fixed height never clips it. */
/* `top` reads --page-header-h (added 2026-08-16, alongside making .week-header sticky above) so
   this stacks directly beneath a page-level .week-header when one exists above it — today only
   screens/previous.js's call into renderMatchupList sits under one; screens/pick.js's own call
   doesn't, and the custom property simply defaults to 0px there, unchanged from before. Set by
   whichever screen actually renders a sticky .week-header above this (previous.js measures its
   own header's real rendered height rather than a hardcoded guess, since Podium's sibling header
   is already documented elsewhere in this file as sometimes wrapping to two lines depending on
   viewport width) — inherits down through the DOM via plain CSS custom-property inheritance, no
   extra JS threading needed. */
.week-summary-bar {
  /* Fixed rather than sticky for the same reason as .week-header above - see that comment. Its
     own margins go with the flow position: .matchup-list's --list-pad-top reserves the space
     instead (a plain constant here, since this bar's height is a fixed 40px by design). */
  position: fixed;
  top: var(--page-header-h, 0px);
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: var(--app-max-width);
  z-index: 3;
  height: 40px;
  display: flex;
  align-items: center;
  gap: 14px;
  margin: 0;
  padding: 0 16px;
  background: rgba(31, 31, 31, 0.92);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--divider);
  font-size: 0.78rem;
  color: var(--text-on-dark);
  overflow-x: auto;
  white-space: nowrap;
}
.week-summary-bar__stat { opacity: 0.85; }
.week-summary-bar__stat strong { color: var(--accent); font-size: 0.95em; }
.week-summary-bar__stat--points strong { color: var(--success); }
.week-summary-bar__stat--pending { margin-left: auto; opacity: 0.55; }

.matchup-section-header {
  color: var(--accent-dim);
  font-weight: 700;
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 6px 2px 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

/* Optional trailing action on a section header — currently only the Upcoming section's "Collapse
   picked (N)" (screens/pick.js's renderMatchupList). Deliberately quiet: the header is a label,
   not a toolbar, and this is a tidy-up shortcut rather than something anyone has to find. */
.section-header-action {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  font: inherit;
  font-size: 0.68rem;
  letter-spacing: 0.02em;
  text-transform: none; /* the header itself is uppercase; this is a sentence-case action, not a label */
  opacity: 0.75;
  cursor: pointer;
  flex: 0 0 auto;
}

/* Sticky and stacking: as the next day's .matchup-weekday scrolls up to top: var(--summary-bar-h),
   it naturally covers/pushes the previous day's header out rather than overlapping it — the
   classic multiple-sibling-sticky-header effect, no JS scroll math needed. Needs its own solid
   background (not transparent) so a card scrolling underneath a stuck header doesn't show
   through it. */
.matchup-weekday {
  position: -webkit-sticky; /* harmless on modern iOS, cheap insurance for older WebKit */
  position: sticky;
  /* Stacks beneath both a page-level .week-header (--page-header-h, see .week-summary-bar's own
     comment above) *and* this list's own summary bar (--summary-bar-h) when both are present. */
  top: calc(var(--page-header-h, 0px) + var(--summary-bar-h, 0px));
  z-index: 2;
  background: var(--bg);
  color: darkgray;
  opacity: 0.9;
  font-size: 0.85rem;
  border-bottom: 3px solid darkgray;
  padding: 6px 2px 4px;
}

.matchup-card {
  /* Same as .card-face further down — the Pick screen's own card is a control surface you press
     repeatedly, and a held press must not raise iOS's selection callout over the top of it. This
     one does NOT use the shared tradingCardFace renderer (see screens/previous.js's header for
     why), so it needs the rule of its own rather than inheriting it. */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  position: relative;
  background: var(--row-even);
  border: 1px solid var(--divider);
  border-radius: var(--radius);
  padding: 12px 10px 10px;
}
.matchup-card--live {
  border-color: rgba(204, 51, 51, 0.4);
  background: linear-gradient(180deg, rgba(204, 51, 51, 0.08), var(--row-even) 40%);
}
.matchup-card--final { opacity: 0.96; }

.matchup-time {
  color: darkgray;
  opacity: 0.75;
  font-size: 0.8rem;
  margin-bottom: 6px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.live-tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--fail);
  color: white;
  font-weight: 800;
  font-size: 0.65rem;
  letter-spacing: 0.04em;
  padding: 2px 6px;
  border-radius: 999px;
}
.live-tag::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: white;
  /* The keyframes live further down this file, beside .final-row__live-dot — see the
     comment there for why there is exactly one copy. */
  animation: live-dot-pulse 1.2s ease-in-out infinite;
}

.matchup-row {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: 12px;
  position: relative;
}
/* The chip tray + "at"/"vs" share this middle column (moved 2026-08-16 — see
   screens/pick.js's own comment on why) — neutral ground between the two teams instead of
   pinned to the row's edge, where a chip here used to read as belonging to whichever team it was
   nearest. */
.matchup-row__center {
  align-self: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 0 2px;
}
.chip-tray {
  display: flex;
  flex-direction: column; /* stacked, not side-by-side (changed 2026-08-16, at the user's
    request) — a horizontal tray grew wider as chips arrived to rest in it, and since
    .matchup-row uses justify-content: center, that visibly nudged both team buttons apart each
    time a chip landed or left. Stacking keeps the tray's footprint just as wide as the info
    button beneath it, 0, 1, or 2 chips resting above it, so the team buttons never move. */
  align-items: center;
  gap: 4px;
  min-width: 28px; /* holds the row's width steady the same way min-height used to hold its
    height steady, before the stack direction flipped */
}
.matchup-vs {
  color: var(--text-on-dark);
  font-size: 0.9rem;
}
.matchup-team { width: 38%; display: flex; flex-direction: column; }
.team-info { text-align: center; }

/* Segmented "Won't cover" / "Covers" control (components/spreadToggle.js) nested under
   whichever team is favored (see screens/pick.js's renderMatchupCard), instead of floating
   beside the two teams as an equal-weight third option — the spread pick always belongs to the
   favorite (scoring.js's status enum), so this makes that relationship visible. Two flat
   segments rather than fancy-btn's 3D press treatment — this is a toggle between two states
   that's always fully visible, not a momentary action button. */
.matchup-team .spread-toggle {
  display: flex;
  width: 100%;
  margin-top: 10px; /* was 6px — the pinned chip now pokes above the segment (see
    .spread-toggle__option > .pick-chip--spread below), so this leaves it room without the
    wrapper needing `overflow: hidden` to look clean */
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 999px;
}
/* No `overflow: hidden` on the wrapper above — it used to be the only way to keep the two
   squared-off children reading as one rounded pill, but it silently clipped anything that
   overflowed the pill's own box: both "Win by N"'s text at the old fixed 50/50 width, and the
   pinned spread chip poking out the top. Rounding each segment's own outer corner instead gets
   the same pill look without clipping either. */
.spread-toggle__option {
  position: relative;
  flex: 1 1 auto; /* sized by content, not forced to an equal half — "Won't" and "Win by N" are
    rarely the same length, and forcing them equal is what caused the clipping in the first
    place */
  min-width: 0;
  padding: 6px 8px;
  font-size: 0.66rem;
  line-height: 1.15;
  font-weight: 700;
  text-align: center;
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-on-dark);
  border: none;
  cursor: pointer;
  white-space: normal; /* wraps to a second line under real width pressure instead of clipping */
  overflow-wrap: break-word;
}
.spread-toggle__option:first-child { border-radius: 999px 0 0 999px; }
.spread-toggle__option:last-child { border-radius: 0 999px 999px 0; }
.spread-toggle__option--active.spread-toggle__option--not-cover { background: rgba(204, 51, 51, 0.35); color: white; }
.spread-toggle__option--active.spread-toggle__option--cover { background: var(--accent-dim); color: white; }
.spread-toggle--locked .spread-toggle__option { cursor: default; opacity: 0.85; }

/* --- Pick-list "relaunch the card stack" bubble (screens/pickStack.js), shown below the
   desktop breakpoint whenever this week still has something worth picking — see
   screens/pick.js's stackAvailable(). Replaced the old always-visible header button
   2026-08-14: this now pairs with bottomNav.js's own collapsed-state nav bubble (opposite
   side, bottom-left) instead of always showing, toggled by the same
   .pick-cards-bubble--visible/nav-bubble--visible pattern via bottomNav.js's
   onCollapsedChange — see screens/pick.js's mountListView. Same glass-capsule material and
   sizing as .nav-bubble so the pair reads as one system. */
.pick-cards-bubble {
  position: fixed;
  right: var(--nav-float-gap);
  bottom: var(--nav-float-gap);
  width: 56px;
  height: 56px;
  border-radius: 50%;
  border: none;
  padding: 0;
  background: rgba(31, 31, 31, 0.82);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.05);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 20;
  opacity: 0;
  transform: scale(0.6);
  pointer-events: none;
  transition: opacity var(--speed-slide) ease, transform var(--speed-slide) ease;
}
.pick-cards-bubble--visible {
  opacity: 1;
  transform: scale(1);
  pointer-events: auto;
}
.pick-cards-bubble svg { width: 26px; height: 26px; }

/* Per-card save-state badge (screens/pick.js's setSaveIndicator) — the pick itself switches
   the instant you tap (assume success), this corner badge is just the follow-up on what the
   debounced write actually did: a small pulsing dot while it's in flight, then a brief green
   check or red X once it resolves. Not a persistent style change either way — the pending dot
   clears the moment the outcome is known, and the check/X pop in and fade on their own. */
.matchup-card--pending,
.matchup-card--saved,
.matchup-card--save-error,
.matchup-card--queued {
  position: relative;
}
.matchup-card--pending::after,
.matchup-card--saved::after,
.matchup-card--save-error::after,
.matchup-card--queued::after {
  content: '';
  position: absolute;
  top: -4px;
  right: -4px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  color: white;
  font-size: 0.8rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}
.matchup-card--pending::after {
  content: '';
  width: 10px;
  height: 10px;
  top: 2px;
  right: 2px;
  background: var(--accent);
  animation: pending-pulse 900ms ease-in-out infinite;
}
.matchup-card--saved::after {
  content: '✓';
  background: var(--success);
  animation: saved-pop 900ms var(--ease-spring) forwards;
}
.matchup-card--save-error::after {
  content: '✕';
  background: var(--fail);
  animation: saved-pop 900ms var(--ease-spring) forwards;
}
/* "Held on this device, waiting for a connection" (screens/pick.js's 'queued' phase, backed by
   js/pickQueue.js). Deliberately amber and deliberately STEADY — no animation and nothing that
   fades it out, unlike every other state above. The pick really is saved locally and really will
   be sent, so this shouldn't read as an error; but it also has to stay put until a flush actually
   succeeds, which is why it can't borrow the pending dot's pulse-then-resolve treatment. */
.matchup-card--queued::after {
  content: '⇡';
  background: #d98c00;
  font-size: 0.75rem;
  line-height: 1;
}
@keyframes pending-pulse {
  0%, 100% { opacity: 0.35; transform: scale(0.85); }
  50% { opacity: 1; transform: scale(1); }
}
@keyframes saved-pop {
  0% { opacity: 0; transform: scale(0.4); }
  25% { opacity: 1; transform: scale(1.15); }
  40% { transform: scale(1); }
  75% { opacity: 1; }
  100% { opacity: 0; }
}

/* --- Compact outright/spread verdict pills (screens/pick.js's buildVerdictChips), replacing
   the old full-width colored sentence boxes (.matchup-status-row/.matchup-status-box). One
   line, small — only as much detail as the ring above didn't already say.
   Each pill is two segments now (added 2026-08-16, at the user's request): a solid __label half
   ("Outright ✓") and a lighter __points "extension" showing the actual points that category
   contributed — a check/x alone doesn't say whether a win was worth 1 point or 4, which
   scoring.js's underdog/favorite multiplier can genuinely make true. The Outright half is also a
   real <button> (verdict-chip--tappable) into components/breakdownModal.js's plain-language
   explanation of that multiplier. */
.verdict-chips {
  display: flex;
  justify-content: center;
  gap: 6px;
  margin-top: 8px;
  flex-wrap: wrap;
}
.verdict-chip {
  display: flex;
  align-items: stretch;
  border-radius: 999px;
  overflow: hidden;
  font-size: 0.7rem;
  font-weight: 700;
  color: white;
  border: none;
  padding: 0;
  font-family: inherit;
  cursor: default;
}
.verdict-chip__label { padding: 3px 9px 3px 10px; }
.verdict-chip__points { padding: 3px 8px 3px 6px; background: rgba(255, 255, 255, 0.22); }
.verdict-chip--correct { background: var(--success); }
.verdict-chip--incorrect { background: var(--fail); }
.verdict-chip--push { background: var(--accent-dim); }
.verdict-chip--none { background: rgba(255, 255, 255, 0.1); color: var(--text-on-dark); opacity: 0.7; padding: 3px 8px; }
.verdict-chip--tappable {
  cursor: pointer;
}
.verdict-chip--tappable .verdict-chip__label {
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 2px;
}

/* Split OUT/SPR verdict capsule on a collapsed Final row (screens/pick.js's
   buildSplitVerdictCapsule) — one pill, two independently colored halves, so the ✓/✗ says which
   bet it's grading instead of silently meaning "outright" while the points chip beside it covers
   both. Same green/red/push palette as the expanded card's .verdict-chip, at dot size. */
.final-verdict {
  display: flex;
  flex: 0 0 auto;
  align-items: stretch;
  border-radius: 999px;
  overflow: hidden;
  font-size: 0.55rem;
  font-weight: 800;
  color: white;
  line-height: 1;
}
.final-verdict__half {
  display: flex;
  align-items: center;
  gap: 3px;
  padding: 5px 7px;
}
.final-verdict__half + .final-verdict__half { border-left: 1px solid rgba(0, 0, 0, 0.28); }
.final-verdict__label { opacity: 0.75; letter-spacing: 0.03em; }
.final-verdict__mark { font-size: 0.7rem; }
.final-verdict__half--correct { background: var(--success); }
.final-verdict__half--incorrect { background: var(--fail); }
.final-verdict__half--push { background: var(--accent-dim); }

/* --- Confetti micro-celebration (screens/pick.js's celebratedCards) — plays once, the first
   time a card is ever rendered as a correct-and-final outright pick this page session. Six
   particles fired from the card's center, staggered, pure CSS (no animation library). */
.confetti-burst {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: visible;
}
.confetti-particle {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 7px;
  height: 7px;
  border-radius: 2px;
  opacity: 0;
  animation: confetti-fly 900ms ease-out forwards;
}
.confetti-particle:nth-child(1) { background: var(--success); animation-delay: 0ms; --dx: -60px; --dy: -50px; }
.confetti-particle:nth-child(2) { background: #ffd700; animation-delay: 40ms; --dx: 55px; --dy: -60px; }
.confetti-particle:nth-child(3) { background: var(--accent); animation-delay: 80ms; --dx: -75px; --dy: 10px; }
.confetti-particle:nth-child(4) { background: var(--success); animation-delay: 120ms; --dx: 70px; --dy: 20px; }
.confetti-particle:nth-child(5) { background: #ffd700; animation-delay: 60ms; --dx: -20px; --dy: -80px; }
.confetti-particle:nth-child(6) { background: var(--accent); animation-delay: 100ms; --dx: 25px; --dy: -85px; }
@keyframes confetti-fly {
  0% { opacity: 1; transform: translate(-50%, -50%) rotate(0deg); }
  100% { opacity: 0; transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) rotate(220deg); }
}

/* --- Collapsed "Live"/"Final" row (screens/pick.js's renderSettledEntry) — one line per game, so
   the part of the week you can no longer act on stays out of the way of the games you can, by
   default. Tap to expand into that section's own full card; .final-collapse-btn below is how an
   expanded one collapses back, and .final-row--live is the still-playing variant. */
.final-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  background: var(--row-even);
  border: 1px solid var(--divider);
  border-radius: 12px;
  padding: 8px 12px;
  color: var(--text-on-dark);
  cursor: pointer;
}
.final-row__teams { flex: 1 1 auto; text-align: left; font-size: 0.85rem; font-weight: 600; }
.final-row__chevron { opacity: 0.5; flex: 0 0 auto; }

/* The same row serving a still-playing game (screens/pick.js's renderSettledEntry, mode 'live').
   Carries .matchup-card--live's red tint so it reads as live even out of its section's context —
   its score and ✓/✗ are provisional, and a row indistinguishable from a settled one invites
   reading a half-time lead as a final result. */
.final-row--live {
  border-color: rgba(204, 51, 51, 0.4);
  background: linear-gradient(180deg, rgba(204, 51, 51, 0.1), var(--row-even) 60%);
}
.final-row--live .final-row__teams { color: var(--fail); }
/* The live game clock (0.12.38), sitting between the pulsing dot and the teams. Deliberately
   NOT the same weight or color as .final-row__teams beside it: this row already carries a
   score, a split verdict capsule, a worth chip and a chevron inside 390px, so the clock has to
   read as an annotation on the dot rather than as a second heading competing with the teams.
   `flex: 0 0 auto` and tabular figures keep it from reflowing the row every time a second ticks
   over — the whole row is rebuilt on each poll, and a clock whose width changed with its digits
   would shove the teams sideways twice a minute. */
.final-row__clock {
  flex: 0 0 auto;
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--fail);
  opacity: 0.85;
}
.final-row__live-dot {
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--fail);
  animation: live-dot-pulse 1.6s ease-in-out infinite;
}
/* THE ONE live-dot pulse in this app, shared by three consumers with three different
   durations: .live-tag::before (1.2s, above), .final-row__live-dot (1.6s, directly above)
   and .card-face__score-dot (1.6s, css/components.css). Keyframes are GLOBAL regardless of
   which stylesheet declares them, which is what lets components.css reach this one.

   It was declared TWICE for months — once here and once beside .live-tag::before above,
   with different values (0.35/scale(0.7) up there). A duplicate @keyframes is not an error:
   the LAST declaration silently wins for every consumer, so .live-tag was already pulsing
   on these values and not on the ones written directly above it. Deleting the earlier copy
   was therefore a verified no-op — confirmed before the edit by reading the resolved
   keyframes back out of the CSSOM in a real browser, not by reasoning about order. If a
   consumer ever genuinely needs a different curve, give it its OWN NAME; a second
   `live-dot-pulse` will not do what it looks like it does. */
@keyframes live-dot-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.45; transform: scale(0.82); }
}

/* --- Collapsed already-picked "Upcoming" row (screens/pick.js's renderUpcomingRow) — the
   pre-kickoff counterpart to .final-row above, and deliberately the same object: same height,
   radius, padding, border and chevron, so a week in progress reads as one list of rows rather than
   two unrelated compact styles. It exists so an uncurated 71–86 game week stays navigable; see
   renderUpcomingEntry's header for the "a picked game is finished work" rule behind it.
   The one structural difference is the left side, which stacks two lines (teams over kickoff time)
   where the Final row has a single score line — the time is what tells you how long you still have
   to change your mind, so it can't be dropped, and it doesn't fit beside a pick capsule on a 360px
   phone. `min-width: 0` lets that column be the one that shrinks, since the capsule and the worth
   chip to its right are both fixed-size. */
.upcoming-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  background: var(--row-even);
  border: 1px solid var(--divider);
  border-radius: 12px;
  padding: 8px 12px;
  color: var(--text-on-dark);
  cursor: pointer;
}
.upcoming-row__main {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
}
.upcoming-row__teams {
  font-size: 0.85rem;
  font-weight: 600;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Kickoff time and the book line share the second line, as a row inside the column above. Both are
   secondary detail about the game, and stacking them would make this row taller than .final-row —
   the two are deliberately the same object at the same height. `min-width: 0` again so the line can
   ellipsize rather than push the pick capsule off a 360px phone. */
.upcoming-row__meta {
  display: flex;
  align-items: baseline;
  gap: 6px;
  min-width: 0;
  max-width: 100%;
}
.upcoming-row__time { font-size: 0.65rem; opacity: 0.55; flex: 0 0 auto; }
/* Slightly brighter than the time beside it, and tabular so a column of these lines up: it's the
   number a player is actually weighing, where the time is just a deadline. Kept to the same 0.65rem
   so neither competes with the team names on the line above. */
.upcoming-row__line {
  font-size: 0.65rem;
  font-weight: 600;
  opacity: 0.75;
  font-variant-numeric: tabular-nums;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.upcoming-row__line::before {
  content: '·';
  margin-right: 6px;
  opacity: 0.5;
  font-weight: 400;
}

/* The "what I picked" pill (screens/pick.js's buildPickCapsule) — same split-in-half geometry as
   .final-verdict, so the row's answer later lands in the same spot the question sat. Neutral
   surface rather than that one's green/red: nothing here is graded yet, and borrowing the verdict
   palette pre-kickoff would read as a game that had already been won or lost. */
.pick-capsule {
  display: flex;
  flex: 0 0 auto;
  align-items: stretch;
  border-radius: 999px;
  overflow: hidden;
  font-size: 0.55rem;
  font-weight: 800;
  line-height: 1;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid var(--divider);
  color: var(--text-on-dark);
}
.pick-capsule__half {
  display: flex;
  align-items: center;
  gap: 3px;
  padding: 5px 7px;
}
.pick-capsule__half + .pick-capsule__half { border-left: 1px solid var(--divider); }
.pick-capsule__label { opacity: 0.5; letter-spacing: 0.03em; }
.pick-capsule__value { font-size: 0.68rem; color: var(--accent); }

/* An expanded Live or Final card collapses on a tap anywhere that isn't a real button
   (screens/pick.js's renderSettledEntry). The team tiles and spread toggle are `disabled` on a
   locked game, and a disabled button swallows its click instead of letting it bubble — without
   this rule the middle of the card would be a dead zone that ignores the tap.
   Was .matchup-card--expanded-final and scoped to Final only, on the reasoning that a live card's
   disabled controls should keep swallowing taps since there's nothing for the tap to do. Once Live
   became collapsible too (2026-08-17) there IS something for it to do — close the card — so the
   rule covers both, which is also why the class no longer says "final". */
.matchup-card--expanded-settled { cursor: pointer; }
.matchup-card--expanded-settled .fancy-btn:disabled,
.matchup-card--expanded-settled .spread-toggle__option:disabled { pointer-events: none; }
.final-collapse-btn {
  display: block;
  margin: 10px auto 0;
  background: none;
  border: none;
  color: var(--accent-dim);
  font-size: 0.75rem;
  font-weight: 600;
  cursor: pointer;
}

/* --- Pick stack (screens/pickStack.js): the once-per-week swipeable deck ---
   A true full-screen takeover, not just the widest thing in #screen-root's normal flow: fixed
   to the viewport (so it isn't clipped by #screen-root's own overflow/padding, which is sized
   to leave room for the floating bottom nav) and stacked above the nav's own z-index: 20, with
   the nav additionally removed from layout entirely via bottom-nav--hidden while this is
   mounted (see components/bottomNav.js's setNavVisible). Picking is this app's one job, so
   while a player is mid-deck nothing else — nav, tab chrome — competes for the screen; the only
   way out besides finishing the deck is the explicit exit button below. */
.pick-stack {
  position: fixed;
  inset: 0;
  z-index: 30;
  background: var(--bg);
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: calc(16px + env(safe-area-inset-top)) 16px calc(24px + env(safe-area-inset-bottom));
  /* Was overflow-y: auto — a real gap, found right after the .swipe-card touch-action fix above:
     that fix only stops a drag starting ON a card (or one of its children) from leaking into a
     native page pan. A drag starting on THIS element's own background — the blank area around/
     behind the card, this container's own `background: var(--bg)` fill — was never covered by
     that fix at all, since it's not a descendant of .swipe-card. This element being overflow-y:
     auto with no touch-action of its own meant that exact gesture was free to natively scroll
     .pick-stack itself (the only thing behind the card in the first place, per this rule's own
     header comment — a "true full-screen takeover"), which read the same way to a player as the
     page-behind-the-card bug the touch-action fix addressed. hidden instead of auto: the deck's
     own layout (progress readout + the 3:4 card, max-width 340px + undo button) is sized to fit
     within a normal phone viewport without needing to scroll at all — see .pick-stack__deck's
     own comment on how it centers in whatever space is left. touch-action: none alongside it,
     matching .swipe-card's own rule just above, so a drag on the background can't trigger any
     other native gesture (overscroll bounce, pull-to-refresh) either, not just plain scrolling. */
  overflow: hidden;
  touch-action: none;
}
.pick-stack__exit {
  position: absolute;
  top: calc(12px + env(safe-area-inset-top));
  right: 12px;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: none;
  background: rgba(255, 255, 255, 0.08);
  color: var(--text-on-dark);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  z-index: 1;
}
.pick-stack__progress { color: darkgray; font-size: 0.85rem; margin-bottom: 8px; margin-top: 8px; }
.pick-stack__deck {
  position: relative;
  width: 100%;
  max-width: 340px;
  aspect-ratio: 3 / 4;
  /* No button row below anymore to anchor against — auto top/bottom margins in this flex
     column absorb whatever room is left between the progress readout and the undo button (or
     the bottom safe area, once undo has nothing to undo), so the card centers itself in
     whatever vertical space the viewport actually has instead of sitting flush under the
     readout. */
  margin: auto 0;
}
.pick-stack__undo {
  margin-top: 14px;
  padding: 8px 18px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  border: none;
  color: var(--accent);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
}
.pick-stack__complete {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  text-align: center;
  padding: 24px;
}
.pick-stack__complete-title { font-size: 1.2rem; font-weight: 700; color: var(--text-on-dark); }

/* --- Standings / Podium (Standings.qml) — redesigned 2026-08-15, see screens/podium.js's
   header comment for the full rationale. The header has three toggle-groups after the week
   <select> (Weekly/Cumulative, Total/Outright/Spread, List/Graph), and on a phone-width screen
   they don't all fit one line — Weekly/Cumulative ends up alone on the first line (right-anchored
   via .week-header's own margin-left: auto), Total/Outright/Spread + List/Graph wrap to a second.
   Total/Outright/Spread stays left-anchored on that second line (margin-left: 0, the row's normal
   gap next to whatever precedes it); List/Graph gets its own margin-left: auto so it's right-
   anchored too, matching Weekly/Cumulative above it — auto margins resolve per flex *line* in a
   wrapping container, so this doesn't need to coordinate with the first toggle-group's margin at
   all despite both being "auto." */
.week-header .toggle-group--metric { margin-left: 0; }
.week-header .toggle-group--view { margin-left: auto; }

/* --- Empty state (no weeklyPoints yet for the selected week) --- */
.podium-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 60px 24px;
  opacity: 0.6;
  text-align: center;
}
.podium-empty__icon { font-size: 2rem; }

/* --- The pre-week state (2026-08-19): a week that exists but hasn't decided anything yet.
   Replaces the medal block, which before this handed a gold medal to whoever sorted first
   alphabetically at 0 points — see screens/podium.js's PRE-WEEK STATE comment. Styled as one
   card in the same language as .podium-medal (rounded, bordered, a wash of the accent over
   --row-even) so it reads as occupying the podium's slot rather than as an error panel. --- */
.podium-preweek {
  margin: 20px 5% 2px;
  padding: 22px 18px 18px;
  border-radius: var(--radius);
  border: 1px solid var(--divider);
  background: linear-gradient(165deg, rgba(0, 191, 255, 0.14), var(--row-even) 70%);
  text-align: center;
}
.podium-preweek__icon { font-size: 1.7rem; line-height: 1; }
.podium-preweek__headline {
  margin: 8px 0 0;
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--text-on-dark);
}
.podium-preweek__sub {
  margin: 5px 0 0;
  font-size: 0.76rem;
  line-height: 1.35;
  color: var(--text-on-dark);
  opacity: 0.6;
}
/* The live countdown to the next kickoff — the panel's hero, since "when does this start" is the
   actual question someone opening an empty podium has. Tabular figures so the digits don't jitter
   as the seconds tick over, which they visibly do in a proportional face. */
.podium-preweek__countdown { margin-top: 14px; }
.podium-preweek__clock {
  font-size: 2rem;
  font-weight: 800;
  line-height: 1.05;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}
.podium-preweek__clock-label {
  margin-top: 3px;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-on-dark);
  opacity: 0.5;
}

/* Two tiles, the points range given the wider one — a range needs room for two numbers and a
   dash, and "points on the table" is a two-line label at phone widths. The row stretches its
   tiles to a common height rather than letting the longer label set one taller than the other. */
.podium-preweek__stats {
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: 8px;
  margin-top: 16px;
}
.podium-preweek__stat {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 10px 6px;
  border-radius: 12px;
  border: 1px solid var(--divider);
  background: rgba(0, 0, 0, 0.25);
}
.podium-preweek__stat--wide { flex: 1.6 1 0; }

/* The points figure is a <button> as of 2026-08-19 (it opens components/pointsOnTheLineModal.js),
   so it needs the browser's button defaults stripped back to what the sibling <div> stats look
   like. font: inherit is the load-bearing one — without it the value and label both fall back to
   the UA's button font and this stat alone renders in a different typeface at a different size,
   which is the sort of thing that looks like a rendering bug rather than a control.

   The affordance is the ⓘ glyph rather than a border or a background: this sits in a row of two
   stats and giving one of them a box would read as "this one is selected". */
.podium-preweek__stat--tappable {
  font: inherit;
  color: inherit;
  border: none;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.podium-preweek__stat-more {
  margin-left: 4px;
  opacity: 0.65;
  font-size: 0.85em;
}
.podium-preweek__stat--tappable:active .podium-preweek__stat-value { opacity: 0.6; }

/* The second button in the podium's bars-toggle row — opens the week's payout table on a decided
   week (2026-08-19). Inherits .podium-bar-toggle wholesale; only the leading gap is new, and it is
   a margin rather than a gap on the row so the row's own rule is untouched. */
.podium-bar-toggle--points { margin-left: 10px; }

/* The Pick screen's points stat became a <button> (2026-08-19). font: inherit is the load-bearing
   line — without it this stat alone falls back to the UA button font and reads as a rendering bug
   next to its siblings. Same treatment .podium-preweek__stat--tappable already gets. */
.week-summary-bar__stat--tappable {
  font: inherit;
  color: inherit;
  border: none;
  background: none;
  padding: 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.week-summary-bar__more { margin-left: 4px; opacity: 0.6; font-size: 0.85em; }
.week-summary-bar__stat--tappable:active { opacity: 0.6; }

/* Per-game payout rows inside components/pointsOnTheLineModal.js. The modal shell is
   .breakdown-modal's, deliberately reused (see that module's header) — only these rows are new. */
.points-line-list { display: flex; flex-direction: column; gap: 10px; margin-top: 4px; }
.points-line-row {
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  padding-top: 8px;
}
.points-line-row__teams {
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--text-on-dark);
  margin-bottom: 4px;
}
/* Wraps rather than scrolling sideways: three chips fit on one line at 390px, but a neutral-site
   "vs" row or a long pair of abbreviations should drop to a second line instead of pushing the
   modal into a horizontal scroll. */
.points-line-row__values { display: flex; flex-wrap: wrap; gap: 6px; }
.points-line-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  padding: 3px 8px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
  font-size: 0.72rem;
}
.points-line-chip__label { color: var(--text-on-dark); opacity: 0.65; }

/* An outcome that can no longer happen (2026-08-19) — set only once a game is FINISHED, and only
   from what scoring.js would actually have paid. See pointsOnTheLineModal.js.

   Dimmed and desaturated rather than struck through or hidden: the number is still real
   information ("that would have paid 4.0"), it just is not on the table any more. Hiding the row
   would make the table change shape as the week went on, which is exactly what a reference must
   not do. Line-through was tried in the head and rejected on the same grounds — it reads as
   "this value was wrong" rather than "this did not happen". */
.points-line-chip--dead {
  opacity: 0.32;
  background: rgba(255, 255, 255, 0.03);
}
/* The accent would still read as "live" at 32% opacity on this dark background, so the value's
   colour is neutralised too — otherwise a dead chip's number stays the brightest thing in it. */
.points-line-chip--dead .points-line-chip__value { color: var(--text-on-dark); }

/* The final score, so a greyed chip explains itself — without it a dead outcome is a claim the
   reader has no way to check. Sits on the teams line, pushed to the trailing edge. */
.points-line-row__teams { display: flex; align-items: baseline; gap: 8px; }
.points-line-row__score {
  margin-left: auto;
  font-size: 0.7rem;
  font-weight: 500;
  color: var(--text-on-dark);
  opacity: 0.5;
  white-space: nowrap;
}
.points-line-chip__value {
  color: var(--accent);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.podium-preweek__stat-value {
  font-size: 1rem;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  color: var(--accent);
  white-space: nowrap;
}
.podium-preweek__stat-label {
  font-size: 0.6rem;
  line-height: 1.2;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-on-dark);
  opacity: 0.55;
}
/* The readiness meter — shown only while games remain to be picked (see buildPreWeekPanel). */
.podium-preweek__ready { margin-top: 16px; text-align: left; }
.podium-preweek__ready-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 6px;
  font-size: 0.74rem;
  color: var(--text-on-dark);
  opacity: 0.8;
}
.podium-preweek__ready-pct { font-weight: 700; color: var(--accent); }
.podium-preweek__meter {
  height: 6px;
  border-radius: 3px;
  overflow: hidden;
  background: var(--divider-2);
}
.podium-preweek__meter-fill {
  height: 100%;
  border-radius: 3px;
  background: var(--accent);
  transition: width var(--speed-slide) ease;
}

/* Row cards in the pre-week state: no rank badge (there is no rank), no bar (nothing earned and
   no denominator), and the points column replaced by the player's pick count for the week.
   COUNTS ONLY — never which side anyone took; see screens/previous.js's decision D7. */
.podium-list--preweek .podium-row-card__name { margin-bottom: 0; }
.podium-row-card__ready {
  flex: 0 0 auto;
  min-width: 46px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
}
.podium-row-card__ready-count {
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--text-on-dark);
}
.podium-row-card__ready-of { font-size: 0.72rem; font-weight: 600; opacity: 0.45; }
.podium-row-card__ready-label {
  font-size: 0.56rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-on-dark);
  opacity: 0.5;
}
.podium-row-card__ready--done .podium-row-card__ready-count { color: var(--success); }
.podium-row-card__ready--done .podium-row-card__ready-label { color: var(--success); opacity: 0.85; }
.podium-row-card__ready--none .podium-row-card__ready-count { color: var(--fail); }
/* "Unknown", not "zero" — the readiness fetch hasn't landed yet, and a 0 here would be a wrong
   claim about a specific person rather than a blank. */
.podium-row-card__ready--unknown {
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--text-on-dark);
  opacity: 0.3;
}

/* --- Top-3 medal block — the literal "podium" the tab is named for. DOM order (silver, gold,
   bronze — see screens/podium.js's MEDAL_ORDER) already matches the desired left-to-right visual
   order, so layout is just a flex row with the gold card given extra height; no CSS `order`
   needed. Cards are keyed with data-player (set in JS) purely for the FLIP reorder animation —
   see podium.js's renderScreen — not styled here. */
.podium-top3 {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 10px;
  padding: 28px 16px 18px;
}
.podium-medal {
  flex: 1 1 0;
  max-width: 120px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  text-align: center;
  border-radius: var(--radius);
  border: 1px solid var(--divider);
  background: var(--row-even);
  padding: 14px 8px;
  min-height: 130px;
  cursor: pointer; /* tappable — opens components/profileCardModal.js's popup, 2026-08-16 */
}
/* Step heights are strictly descending — 1st tallest, 3rd shortest — and that ordering only
   holds because every card's *content* is the same height (see screens/podium.js's buildTop3
   comment: one-line name, one fixed-height badge row). `min-height`, not `height`, so content
   can never be clipped; the worst case if content ever outgrows these is all three flattening to
   the same height, never a lower rank rendering taller than a higher one. */
.podium-medal--gold {
  min-height: 176px;
  background: linear-gradient(165deg, rgba(255, 215, 0, 0.2), var(--row-even) 65%);
  border-color: rgba(255, 215, 0, 0.5);
}
.podium-medal--silver {
  min-height: 162px;
  background: linear-gradient(165deg, rgba(192, 192, 192, 0.16), var(--row-even) 65%);
  border-color: rgba(192, 192, 192, 0.45);
}
.podium-medal--bronze {
  min-height: 148px;
  background: linear-gradient(165deg, rgba(205, 127, 50, 0.16), var(--row-even) 65%);
  border-color: rgba(205, 127, 50, 0.45);
}
.podium-medal--you { box-shadow: 0 0 0 2px var(--accent); }
/* Mini version of components/profileCardModal.js's circular avatar (2026-08-16, at the user's
   request), with the rank number as a small badge overlapping its corner (changed the same day,
   also at the user's request — a separate rank circle sitting right next to the avatar circle
   read as two competing circles rather than one card element, so the two are now merged into a
   single shape instead). `-wrap` is the positioning context; a 2px border the same color as this
   card's own background creates a "cutout" ring around the badge so it reads as sitting on top of
   the avatar rather than just overlapping it. */
.podium-medal__avatar-wrap {
  position: relative;
  width: 44px;
  margin-bottom: 4px;
}
.podium-medal__avatar {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  overflow: hidden;
  position: relative;
  background: var(--row-even);
  border: 1px solid var(--divider);
  display: flex;
  align-items: center;
  justify-content: center;
}
.podium-medal__rank {
  position: absolute;
  bottom: -4px;
  right: -4px;
  width: 20px;
  height: 20px;
  line-height: 18px;
  border-radius: 50%;
  font-weight: 700;
  font-size: 0.7rem;
  border: 2px solid var(--row-even);
}
.podium-medal--gold .podium-medal__rank { background: #ffd700; color: #402d00; }
.podium-medal--silver .podium-medal__rank { background: #c0c0c0; color: #2b2b2b; }
.podium-medal--bronze .podium-medal__rank { background: #cd7f32; color: #2b1600; }
/* Deliberately clamped to one line with an ellipsis: a name that wrapped would make that one
   card taller than its neighbors and break the descending step order above (see the medal
   min-height comment and screens/podium.js's buildTop3). The full name is always available on
   the row cards below, which are full-width and free to wrap. */
.podium-medal__name {
  font-weight: 600;
  font-size: 0.82rem;
  color: var(--text-on-dark);
  line-height: 1.25;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.podium-medal__total { font-size: 1.25rem; font-weight: 700; color: var(--accent); }
/* "You" tag + movement arrow + streak flame, grouped into one flex item (fixed 2026-08-16 — see
   screens/podium.js's buildTop3 comment for the bugs this replaced: bare sibling badges each
   became their own row in the card's flex-column, and the "You" tag inlined in the name wrapped
   to a second line). `min-height` reserves one badge-row's worth of space even when every badge
   is absent, and `nowrap` keeps a card carrying all three from wrapping into a second row — so
   0/1/2/3 badges all cost exactly the same vertical space and the step order above holds. */
.podium-medal__badges {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: center;
  gap: 5px;
  min-height: 1.1em;
  max-width: 100%;
  overflow: hidden;
}
.podium-medal__badges > * { flex: 0 0 auto; }

/* --- "You" tag, movement arrow, hot-streak badge — small inline pills shared between the medal
   block and the row cards below. --- */
.podium-you-tag {
  display: inline-block;
  background: var(--accent);
  color: #05262e;
  font-size: 0.6rem;
  font-weight: 700;
  padding: 1px 6px;
  border-radius: 8px;
  vertical-align: middle;
}
.podium-movement { font-size: 0.75rem; font-weight: 700; }
.podium-movement--up { color: var(--success); }
.podium-movement--down { color: var(--fail); }
.podium-streak { font-size: 0.75rem; }

/* --- Row cards (rank 1 onward, full list — duplicates the medal block's top 3 on purpose, so
   sorting by a different column never hides anyone). Card-style with rounded corners/border,
   replacing the old flat alternating-stripe table rows, matching the glass/card language the
   rest of the app picked up in earlier redesigns. --- */
.podium-list { display: flex; flex-direction: column; gap: 8px; padding: 8px 5% 24px; }
.podium-row-card {
  --card-bg: var(--row-even); /* read by .podium-row-card__rank's cutout border below, so it
    still matches this specific card's own background even when --card-bg--you overrides it */
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--card-bg);
  border: 1px solid var(--divider);
  border-radius: 12px;
  padding: 10px 12px;
  cursor: pointer; /* tappable — opens components/profileCardModal.js's popup, 2026-08-16 */
}
.podium-row-card--you {
  --card-bg: rgba(0, 191, 255, 0.08);
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent) inset;
}
/* Mini version of components/profileCardModal.js's circular avatar (2026-08-16, at the user's
   request), with rank as a small badge overlapping its corner instead of its own separate circle
   (changed the same day, also at the user's request — two adjacent circles read as competing
   shapes rather than one card element). `-wrap` is the positioning context; the badge's border
   reads this card's own `--card-bg` (not a hardcoded color) so the "cutout" ring around it still
   matches on a highlighted "you" row, which has a different background than every other row. */
.podium-row-card__avatar-wrap {
  position: relative;
  flex: 0 0 auto;
  width: 36px;
}
.podium-row-card__avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  overflow: hidden;
  position: relative;
  background: var(--row-even);
  border: 1px solid var(--divider);
  display: flex;
  align-items: center;
  justify-content: center;
}
.podium-row-card__rank {
  position: absolute;
  bottom: -3px;
  right: -3px;
  width: 18px;
  height: 18px;
  line-height: 16px;
  text-align: center;
  border-radius: 50%;
  background: var(--accent);
  color: #05262e;
  font-weight: 700;
  font-size: 0.62rem;
  border: 2px solid var(--card-bg);
}
.podium-row-card__main { flex: 1 1 auto; min-width: 0; }
.podium-row-card__name {
  font-weight: 600;
  color: var(--text-on-dark);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 5px;
  margin-bottom: 5px;
}
.podium-row-card__total {
  flex: 0 0 auto;
  font-weight: 700;
  font-size: 1.05rem;
  color: var(--accent);
  min-width: 42px;
  text-align: right;
}
/* Earned-vs-missed progress bar (redesigned 2026-08-15, replacing the original proportional
   split bar — see screens/podium.js's header comment and buildBar for the full rationale).
   Outright keeps the app's own accent blue (already the primary identity color everywhere else);
   spread uses a warm gold instead of a second blue step — a shade of the same hue reads as
   barely-distinguishable at this size, and gold isn't already spoken for by --success/--fail,
   which mean something different elsewhere in this app. A "missed" segment reuses its earned
   counterpart's exact color at reduced opacity rather than a separate hardcoded darker hex — same
   hue, dimmed, reads as "this category, just not earned" at a glance instead of introducing a
   fifth unrelated color to learn. */
.podium-bar {
  display: flex;
  height: 6px;
  border-radius: 3px;
  overflow: hidden;
  background: var(--divider-2);
}
.podium-bar--empty { background: var(--divider-2); }
.podium-bar__seg--outright { background: var(--accent); }
.podium-bar__seg--outright-missed { background: var(--accent); opacity: 0.3; }
.podium-bar__seg--spread { background: #e0a72e; }
.podium-bar__seg--spread-missed { background: #e0a72e; opacity: 0.3; }
/* Awarded points (migrations/018) — the floor given to a player who didn't pick. Deliberately the
   SAME hue as its category, sitting between the earned and missed segments, so it reads as "these
   points, but not earned" rather than as an unrelated fourth colour. Distinguished by a hatch
   rather than by opacity alone, since the missed segment already owns "same colour, faded" and two
   dimmed variants of one hue would be indistinguishable at this bar's height. */
.podium-bar__seg--outright-awarded {
  background: repeating-linear-gradient(
    135deg,
    var(--accent) 0 3px,
    color-mix(in srgb, var(--accent) 35%, transparent) 3px 6px
  );
}
.podium-bar__seg--spread-awarded {
  background: repeating-linear-gradient(
    135deg,
    #e0a72e 0 3px,
    rgba(224, 167, 46, 0.35) 3px 6px
  );
}

/* The "Did not pick" group at the foot of the Weekly list. Greyed rather than hidden — the points
   are real and count toward the season, they just weren't earned by picking. */
.podium-list__section-heading {
  margin: 14px 0 2px;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.55;
}
.podium-row-card--unranked { opacity: 0.62; }
/* The rank cutout holds an em dash for these rows; centre it the same as a digit would be. */
.podium-row-card--unranked .podium-row-card__rank { letter-spacing: 0; }

/* AWAY — a player who has gone INACTIVE_AFTER_WEEKS settled weeks without picking (see
   standings.js). Deliberately fainter than the one-week "Did not pick" state above, and declared
   AFTER it so that a row which is both (the usual case in a settled week) takes this opacity —
   both are single-class selectors, so source order is what decides, not specificity.
   Faded, never hidden: their points are real and still count, and the moment they pick again the
   class simply stops being applied. */
.podium-row-card--away { opacity: 0.38; }
.podium-medal--away { opacity: 0.5; }

/* Neutral rather than alarming — the same pill geometry as .podium-you-tag, in the muted ink the
   section headings use, because this is a status and not a telling-off. */
.podium-away-tag {
  display: inline-block;
  border: 1px solid currentColor;
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0 5px;
  border-radius: 8px;
  vertical-align: middle;
  opacity: 0.7;
}

/* Sits under the readiness meter to explain its denominator — see buildPreWeekPanel. */
.podium-preweek__ready-note {
  margin-top: 6px;
  font-size: 0.68rem;
  opacity: 0.55;
  text-align: right;
}
.podium-bar__legend {
  display: flex;
  justify-content: space-between;
  font-size: 0.65rem;
  opacity: 0.6;
  margin-top: 3px;
}

/* One toggle for the whole list, above the rows (not per-row) — a standing on/off preference,
   not something worth repeating on every card. Styled as a small right-aligned text link rather
   than a full button to stay visually secondary to the rows themselves. */
.podium-bar-toggle-row {
  display: flex;
  justify-content: flex-end;
  padding: 0 5% 6px;
}
.podium-bar-toggle {
  background: none;
  border: none;
  color: var(--accent-dim);
  font-size: 0.75rem;
  font-weight: 600;
  cursor: pointer;
  padding: 4px 0;
}
.podium-bar-toggle:hover { color: var(--accent); }

/* --- Profile --- */
.profile-screen {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  padding: 40px 24px;
}
.profile-row { display: flex; align-items: flex-end; gap: 12px; width: 100%; max-width: 300px; }

/* League switcher (multi-league, 2026-08-18 — js/screens/profile.js's buildLeagueSection).
   Renders only for players in more than one league; same 300px column and heading treatment as
   the notifications section so the screen reads as one settings stack. */
/* Was a centred flex column around a segmented toggle group; became a heading + <select> in
   0.12.16 (see profile.js's buildLeagueSection for why). Now structurally identical to
   .profile-season, which is the point — the two switchers sit in the same column of 300px rows as
   the drawers, and one of them being centred while the other was left-aligned read as an
   accident. */
.profile-league {
  width: 100%;
  max-width: 300px;
}
/* Season switcher + the archive notice (phase 5, 2026-08-19). The select reuses the app's dark
   surface rather than the browser default, which on iOS renders as a white pill reading as a
   different app. `font-size: 16px` is not a style choice: anything smaller makes iOS Safari zoom
   the page when the control is focused. */
/* The 300px cap arrived in 0.12.16. This section was the ONLY one on the screen without one —
   measured 339px at x=26 against 300px at x=45 for every other row — which went unnoticed while it
   sat alone above Sign Out and became obvious the moment it moved to the head of three drawer
   rows. */
.profile-season {
  margin-top: 18px;
  width: 100%;
  max-width: 300px;
}
.profile-season__select,
.profile-league__select {
  width: 100%;
  margin-top: 8px;
  padding: 10px 12px;
  font-size: 16px;
  color: var(--text-on-dark);
  /* #2a2a2a, matching .week-header select — NOT var(--bg-overlay), which is `ghostwhite`. That
     token names the LOGIN/loading backdrop, not a dark surface, and pairing it with
     --text-on-dark (#f0f0f0) rendered this control as near-white text on a near-white pill:
     legible in the source, unreadable on screen. CLAUDE.md records the same mistake in PickDev.
     If a control here needs a surface, copy a real value from a control that already works. */
  /* background-COLOR, not the `background` shorthand: the caret below is a background-image, and
     the shorthand would reset it depending on declaration order. */
  background-color: #2a2a2a;
  border: 1px solid var(--divider);
  border-radius: 12px;
  appearance: none;
  /* `appearance: none` strips the native dropdown arrow, and nothing replaced it — so both of
     these read as inert filled fields rather than as controls. That was survivable while the
     season switcher sat alone at the foot of the page; with two of them stacked directly above
     three drawer rows that each show a chevron, a control with no marking at all is the odd one
     out. Same polyline, stroke-width and opacity as .collapsible__chevron on purpose: the things
     that open something on this screen should be marked the same way. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f0f0f0' stroke-opacity='0.55' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 16px 16px;
  padding-right: 36px;
}
.profile-season__hint {
  margin: 8px 0 0;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--text-on-dark);
  opacity: 0.6;
}
/* Replaces the whole Pick screen while a past season is open. Centred rather than pinned to the
   top: it is the only thing on the tab, and a lone paragraph under the header reads as a loading
   state that never resolves. */
.archive-notice {
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: center;
  justify-content: center;
  min-height: 50vh;
  padding: 24px 28px;
  text-align: center;
}
.archive-notice__title { margin: 0; font-size: 1.15rem; color: var(--text-on-dark); }
.archive-notice__body {
  margin: 0;
  max-width: 34ch;
  font-size: 0.9rem;
  line-height: 1.55;
  color: var(--text-on-dark);
  opacity: 0.72;
}

.profile-league__heading {
  font-size: 0.9rem;
  font-weight: 600;
  opacity: 0.8;
  margin: 0;
  text-transform: uppercase;
}

/* Notification settings (2026-08-17 — see js/screens/profile.js's buildNotificationsSection).
   Constrained to the same 300px column every other Profile section uses so it lines up with the
   display-name row above it rather than running the full width on desktop. */
.profile-notifications {
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
  max-width: 300px;
}
.profile-notifications__heading {
  font-size: 0.9rem;
  font-weight: 600;
  opacity: 0.8;
  margin: 0;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.profile-notifications__body {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.profile-notifications__row {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
/* The switch rows put their label/hint block and the checkbox side by side instead. The whole row
   is a <label>, so tapping anywhere on the text toggles the box — the tap target is the row, which
   matters more on a phone than the 16px checkbox itself. */
.profile-notifications__row--switch {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  cursor: pointer;
}
.profile-notifications__label { font-size: 0.9rem; }
.profile-notifications__hint {
  font-size: 0.75rem;
  opacity: 0.55;
  margin: 0;
  line-height: 1.35;
}
.profile-notifications__switch {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  /* Lets the native control pick up the app's own accent instead of the UA default blue, and gets
     the correct dark-mode rendering for free. */
  accent-color: var(--accent, #4c8dff);
}
/* The tri-state pick-reminder control reuses components/toggleGroup.js, but needs more chrome than
   the base .toggle-group provides. Everywhere else that component is used (the Podium's metric
   switchers, Previous's view mode) it sits in a header bar where a bare colored-text button reads
   unambiguously as the active view. In a settings list it does not: three words in two shades of
   blue, stacked among switch rows, don't look like a control at all, and "which one is currently
   selected" is exactly the question this row has to answer at a glance. Hence the segmented pill —
   base look inherited, selection made explicit with a filled background rather than a hue shift. */
.toggle-group--notify {
  width: 100%;
  gap: 0;
  background: var(--row-even);
  border-radius: 8px;
  padding: 3px;
}
.toggle-group--notify button {
  flex: 1;
  padding: 6px 4px;
  border-radius: 6px;
  font-size: 0.8rem;
}
.toggle-group--notify button[aria-pressed="true"] {
  background: var(--accent);
  /* Against a filled accent background the inherited accent-colored text would vanish. */
  color: #06121a;
}
/* Inside the About drawer since 0.12.15. Left-aligned there rather than centred: it is body text
   in a list of left-aligned rows now, not a footer under a centred column. No margin-top — the
   drawer body's gap supplies the spacing. */
.profile-disclaimer {
  font-size: 0.75rem;
  opacity: 0.5;
  max-width: 300px;
  margin: 0;
}
/* The manual app-refresh control, inside the About drawer (profile.js's buildAboutDrawer). The
   VERSION STRING IS NOT HERE ANY MORE — it moved to the drawer's own header summary in 0.12.15,
   where it is visible without opening anything, so this row is just the button. No margin-top:
   .collapsible__body already supplies the 14px gap, and a margin would add to it. */
.profile-version {
  display: flex;
  align-items: center;
  font-size: 0.75rem;
}
/* Auto-width pill rather than the old 22px icon circle, now that it carries its own label. */
.profile-version__refresh {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  min-height: 32px;
  padding: 6px 14px 6px 11px;
  border: 1px solid var(--divider);
  border-radius: 999px;
  background: transparent;
  color: inherit;
  font: inherit;
  opacity: 0.7;
  cursor: pointer;
}
.profile-version__refresh svg { width: 14px; height: 14px; }
.profile-version__refresh:hover { background: var(--row-even); opacity: 1; }
.profile-version__refresh:disabled { cursor: default; }
.profile-version__refresh--spinning svg { animation: profile-version-refresh-spin 0.8s linear infinite; }
@keyframes profile-version-refresh-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

/* --- Profile avatar + favorite team (2026-08-15) ---
   .profile-avatar__preview (read-only, screens/profile.js) and .avatar-cropper__viewport
   (interactive, components/avatarCropper.js) are both circular "viewport" containers that
   position an <img> with top:50%/left:50% + a JS-driven transform (translate(-50%,-50%) to
   truly center it, then the stored/live pan, then scale) rather than object-fit:cover — cover
   would auto-crop to whatever the browser picks, ignoring the player's own chosen pan/zoom
   entirely. They only differ in size and in whether input events are wired up. */
.profile-avatar { display: flex; flex-direction: column; align-items: center; gap: 10px; }
/* Must match screens/profile.js's AVATAR_PREVIEW_PX constant — that's the value actually fed
   into applyCircleCropStyle's containerPx math, so a mismatch here wouldn't break layout but
   would misalign the rendered crop against this box's real rendered size. */
.profile-avatar__preview {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  overflow: hidden;
  position: relative;
  background: var(--row-even);
  border: 2px solid var(--accent-dim);
  display: flex;
  align-items: center;
  justify-content: center;
}
/* .avatar-circle__image/__placeholder (components/avatarCropper.js's renderAvatarCircle,
   2026-08-16) are the shared img/placeholder rules for ANY circular avatar viewport this app
   renders — profile-avatar__preview here plus components/profileCardModal.js's own, larger
   viewport. Font size for the placeholder is set inline per-instance (containerPx-relative), not
   here, since it has to scale with whichever container size is actually rendering it. */
.avatar-circle__image {
  position: absolute;
  top: 50%;
  left: 50%;
  max-width: none;
  user-select: none;
  -webkit-user-drag: none;
}
.avatar-circle__placeholder { font-weight: 600; color: var(--accent); opacity: 0.6; }

.avatar-cropper { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.avatar-cropper__viewport {
  border-radius: 50%;
  overflow: hidden;
  background: var(--divider-2);
  position: relative;
  touch-action: none; /* so a drag pans the image instead of scrolling the page underneath it */
  cursor: grab;
}
.avatar-cropper__viewport:active { cursor: grabbing; }
.avatar-cropper__image {
  position: absolute;
  top: 50%;
  left: 50%;
  max-width: none;
  user-select: none;
  -webkit-user-drag: none;
}
.avatar-cropper__zoom { width: 200px; }
.profile-avatar__actions { display: flex; gap: 8px; }
/* screens/profile.js toggles both of these with the `hidden` IDL attribute (actions.hidden =
   true, picker.hidden = true) — without this, the display: flex above would win over the UA
   stylesheet's `[hidden] { display: none }` (author-origin rules always beat UA-origin ones
   regardless of specificity), same fix components.css's .status-overlay[hidden] already needed
   for the same reason. */
.profile-avatar__actions[hidden],
.profile-favorite-team__picker[hidden] { display: none; }

.profile-favorite-team { display: flex; flex-direction: column; align-items: center; gap: 10px; width: 100%; max-width: 300px; }
.profile-favorite-team__summary { display: flex; align-items: center; gap: 10px; }
.profile-favorite-team__summary img { width: 32px; height: 32px; object-fit: contain; }
.profile-favorite-team__summary span { font-size: 0.95rem; opacity: 0.85; }
.profile-favorite-team__picker { display: flex; flex-direction: column; gap: 8px; width: 100%; }
/* font-size must be >= 16px (1rem) — anything smaller makes iOS Safari auto-zoom the whole
   page in on focus, which is exactly the unwanted zoom this input was causing (see
   components.css's ".field input" for the same rule applied to other text fields). */
.profile-favorite-team__search {
  height: 44px;
  border-radius: 8px;
  border: 2px solid transparent;
  padding: 0 12px;
  font-size: 1rem;
  width: 100%;
}
.profile-favorite-team__list {
  max-height: 240px;
  overflow-y: auto;
  border-radius: 10px;
  background: var(--row-odd);
}
.profile-favorite-team__row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  cursor: pointer;
  background: none;
  border: none;
  width: 100%;
  color: var(--text-on-dark);
  font-size: 0.9rem;
  text-align: left;
}
.profile-favorite-team__row:nth-child(even) { background: var(--row-even); }
.profile-favorite-team__row img { width: 24px; height: 24px; object-fit: contain; flex-shrink: 0; }
.profile-favorite-team__row:hover { background: var(--btn-back-selected); }

/* --- Previous Weeks pickers share .week-header, plus a player <select> --- */
.previous-pickers { display: flex; gap: 8px; flex-wrap: wrap; }

/* Small tappable avatar next to the player <select> (screens/previous.js, 2026-08-16), opening
   components/profileCardModal.js's popup for whichever player the select is currently set to —
   Previous has no per-player row list the way Podium does, so this is its one card-popup entry
   point. Width/height must match PREVIOUS_AVATAR_PX in previous.js — that's the value fed into
   renderAvatarCircle's containerPx math. */
.previous-player-card-btn {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  overflow: hidden;
  position: relative;
  background: var(--row-even);
  border: 1px solid var(--divider);
  padding: 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

/* --- screens/previous.js's single-player week rows (View 1 of the Previous redesign) — a
   .card-face--row plus the away/home team names beneath it. Coloring (glow/dim/score) is applied
   by previous.js itself per Documents/roadmap.md's row case table, directly onto the parts
   tradingCardFace.js returns — nothing here, this is just the row's own chrome. */
.previous-row {
  /* Same reasoning as .card-face above — the entire row is a press target now that reactions
     landed, and its labels/verdict chips/meta line are all captions rather than prose. */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: var(--row-even);
  border: 1px solid var(--divider);
  border-radius: var(--radius);
  padding: 8px;
}
.previous-row__labels {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  padding: 0 4px;
  font-size: 0.72rem;
  color: var(--text-on-dark);
  opacity: 0.85;
}
.previous-row__meta {
  text-align: center;
  font-size: 0.7rem;
  color: darkgray;
  opacity: 0.8;
}

/* The two verdict chips under each row (screens/previous.js's buildVerdictChips) — one for the
   outright bet, one for the spread bet, each naming what was picked and what it earned. See that
   function's own comment for why a row needs to resolve two verdicts rather than one.

   A new class family rather than reusing .pick-chip: that one is a 28px circular poker chip built
   for the Pick screen's chip-*placement* metaphor (it physically moves to the team button or
   spread-toggle half you picked), which has nothing to do with a wide, labelled, read-only summary
   line. Only the verdict COLOR tokens are shared, plus the earned/missed/push vocabulary, so the
   two surfaces still agree on what green, red, blue and neutral mean. */
.previous-row__verdicts {
  display: flex;
  gap: 6px;
  padding: 0 2px;
}
.previous-verdict {
  /* `auto` basis, not `0` — the two chips carry very different amounts of text (an outright chip's
     call is a 3-letter abbreviation; a spread chip's is a whole phrase), and forcing them to exactly
     equal halves spent the outright chip's slack on nothing while ellipsizing the spread chip's
     label on a real phone. Sized by content, then sharing what's left over, both fit. Same fix and
     same reasoning as .spread-toggle__half above, which hit this on the Pick screen first. */
  flex: 1 1 auto;
  min-width: 0; /* still ellipsize rather than overflow the card, if a label ever gets long enough */
  display: flex;
  align-items: baseline;
  gap: 5px;
  padding: 3px 7px;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--divider);
  font-size: 0.68rem;
  line-height: 1.35;
}
.previous-verdict__kind {
  font-size: 0.52rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  opacity: 0.65;
  flex: 0 0 auto;
}
.previous-verdict__call {
  font-weight: 700;
  color: var(--text-on-dark);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.previous-verdict__points {
  margin-left: auto;
  font-weight: 800;
  font-variant-numeric: tabular-nums; /* keeps +0.0 and +12.5 from shifting the chip's layout */
  flex: 0 0 auto;
}
/* Verdict states. The left border does the at-a-glance work — a full colored fill on two chips per
   row, across a whole week of rows, competes with the card's own team colors directly above it. */
.previous-verdict--correct {
  border-color: var(--success);
  box-shadow: inset 3px 0 0 var(--success);
}
.previous-verdict--correct .previous-verdict__points { color: var(--success); }
.previous-verdict--incorrect {
  border-color: var(--fail);
  box-shadow: inset 3px 0 0 var(--fail);
}
.previous-verdict--incorrect .previous-verdict__points { color: var(--fail); }
.previous-verdict--incorrect .previous-verdict__call { opacity: 0.75; }
/* Live and push share the accent blue, matching this app's "blue while unresolved / not a
   win-or-loss" convention (see the half glow's own GLOW_COLOR.live in previous.js). A push is
   final but genuinely neither outcome, and it still pays full worth. */
.previous-verdict--live,
.previous-verdict--push {
  border-color: var(--accent);
  box-shadow: inset 3px 0 0 var(--accent);
}
.previous-verdict--live .previous-verdict__points,
.previous-verdict--push .previous-verdict__points {
  color: var(--accent);
}
/* No pick at all — deliberately flat and unemphasized, and it renders as a single full-width chip
   since there are no bets to report either way. */
.previous-verdict--none {
  opacity: 0.55;
  justify-content: center;
}

/* --- screens/previousBinder.js (View 2 — "everyone's picks", condensed into a binder grid of
   mini trading cards) --- */
.binder-grid {
  display: grid;
  /* 150px, raised from 140px when these cells gained .previous-row's 8px padding + 1px border
     (2026-08-16): that chrome costs the card face 18px of width, and at 140px the grid fit a third
     column inside the 480px desktop --app-max-width, leaving a ~123px face trying to hold two
     logos, a score, a spread badge and two bars.
     This value only decides the column COUNT (the `1fr` stretches whatever fits), and it's pinned
     between two real constraints rather than picked by feel: it must be low enough that two columns
     still fit a 360px phone (2 x 150 + 12 gap = 312, inside that viewport's 328px of content
     width — 160px was tried first and silently collapsed a 360px screen to ONE column, turning the
     binder back into the row view it exists to condense), and high enough that a third column
     doesn't fit the 480px desktop cap (3 x 150 + 24 = 474 > 448). Still auto-fill rather than a
     hardcoded `repeat(2, ...)` so a genuinely wider container — the desktop layout on the roadmap —
     picks up a third column on its own. */
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 12px;
  padding: 16px;
}
/* Framed like .previous-row (2026-08-16, at the user's request) — same background/border/radius/
   padding tokens, so the binder grid reads as a grid of the same cards the other view stacks
   vertically, rather than as bare card faces floating on the page background. The button reset
   (font/color/cursor/text-align) stays: this is still a <button>, it just no longer looks like the
   browser's idea of one. */
.binder-mini-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: var(--row-even);
  border: 1px solid var(--divider);
  border-radius: var(--radius);
  padding: 8px;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-align: left;
}
.binder-mini-card:active .card-face--mini { transform: scale(0.97); }
.card-face--mini { transition: transform 120ms ease; }
.binder-mini-card__bars {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 0 2px;
}
/* Live/Final status per card — the binder has no section grouping to carry it the way the row view
   does (screens/pick.js's buildSectionHeader), so each card states its own. Live gets the accent
   blue this app uses for "unresolved" everywhere else; Final stays muted, since a finished game's
   own colors and score are already doing the talking. */
.binder-mini-card__meta {
  text-align: center;
  font-size: 0.62rem;
  color: darkgray;
  opacity: 0.8;
  /* Grid cells stretch to the tallest in their row, so a card with no spread bar (a no-line game)
     is as tall as its neighbours but has one less row of content — without this its status line
     floats mid-card while the neighbour's sits lower. `auto` pushes every one to its own card's
     bottom edge, so they line up across the row regardless of how many bars each card has. */
  margin-top: auto;
}
.binder-mini-card__meta--live {
  color: var(--accent);
  opacity: 1;
  font-weight: 700;
}

/* Two fills growing outward from a shared center (left grows leftward from the midpoint, right
   grows rightward) rather than the stacked same-direction shape .podium-bar above uses — see
   Documents/roadmap.md's plan for why this needed to be a new component rather than reusing that
   one. Colors are set inline per bar (components/divergingBar.js) using the same token convention
   .podium-bar__seg--outright/--spread already established, so the two screens read as the same
   visual language despite the different shape. */
.diverging-bar {
  position: relative;
  height: 5px;
  border-radius: 3px;
  background: var(--divider-2);
  overflow: hidden;
}
.diverging-bar__fill {
  position: absolute;
  top: 0;
  bottom: 0;
  opacity: 0.85;
}
.diverging-bar__fill--left { right: 50%; }
.diverging-bar__fill--right { left: 50%; }
/* The side that got it wrong, once the game is final (components/divergingBar.js's `dimSide`).
   Darkened rather than faded, at the same brightness(0.45) .card-face__logo--loser uses, so "this
   was wrong" is one consistent strength everywhere on the card. Opacity would be the wrong tool
   here for the same reason it was on the logo: these fills are already at 0.85, and fading them
   further toward the track color behind them makes a small 5px band read as absent rather than as
   defeated. The remaining bright side is what the eye should land on. */
.diverging-bar__fill--dim { filter: brightness(0.45); }
