/* Gaffer IQ — components.css
   Reusable component styles: matchup card, score pill, breakdown bars,
   confidence indicator, counter-matchup pairings, GW navigator.
   BEM-lite per CONVENTIONS.md §5.1. All values via var(--…) from base.css.
   Band modifier classes (--great, --good, --neutral, --tough, --brutal) map
   directly from CompositeScore.band — see CONVENTIONS.md §5.2. */

/* ═══════════════════════════════════════════════════════════════════════════
   MATCHUP SHELL — GW navigator (left) + team cards (right)
   Mirrors .ranker-shell / .dash-shell / .planner-shell's 300px-sidebar +
   1fr-main pattern, just narrower — the navigator only ever holds a handful
   of short fixture rows, not a filter stack.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The shell is two stacked boxes: the three-column row (.matchup-shell__main)
   and the full-width season strip under it.

   The strip used to be a second row of ONE grid, spanning columns 1-2 while
   team-nav spanned both rows beside it. Widening that span to the full width
   is not something the single grid can express: .team-nav is position: sticky,
   and Chrome constrains a sticky grid item to the whole grid container rather
   than to its own grid area — so the pinned panel slid 343px down over the
   strip as soon as the strip stopped sitting beside it. Splitting the two rows
   into two boxes puts the strip outside the sticky element's containing block
   entirely, which is a structural answer rather than a z-index one. */
.matchup-shell {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  /* Break out of .app-main's max-width:1400px + centering (layout.css) —
     same escape .ranker-shell uses below, for the same reason: the fixed-
     width side navigators were squeezing the middle column's two team cards
     into well under half the viewport. width:100vw + these negative margins
     is the standard technique for escaping a centred ancestor's max-width;
     the side navigators land further out toward the viewport edges as a
     direct result, and the middle column (both nav columns are fixed-width)
     absorbs the rest — roughly doubling the cards' width on a typical
     desktop viewport. Padding restores the horizontal inset app-main's own
     padding provided before this element escaped its box. */
  width: 100vw;
  max-width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  padding: 0 var(--space-6);
}

.matchup-shell__main {
  display: grid;
  /* Team nav's rows are now a single content-hugging cluster (score, opponent,
     H/A, GW — see .gw-nav__fixture--team), not the GW nav's side-by-side
     comparison, so it no longer needs extra width of its own: same 260px as
     the GW nav column. */
  grid-template-columns: 260px 1fr 260px;
  gap: var(--space-6);
  align-items: start;
}

@media (max-width: 900px) {
  .matchup-shell__main {
    grid-template-columns: 1fr;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   GW NAVIGATOR — replaces the old fixture-picker <select>. One GW's fixtures
   at a time, paged with the header arrows; click a row to load that matchup.
   TEAM NAVIGATOR (.team-nav) mirrors it on the opposite side of the shell —
   same panel chrome, header, and .gw-nav__* row markup, just one team's full
   season instead of one GW's fixtures (see buildTeamNavPanel in matchup.js).

   The team navigator now lists EVERY remaining gameweek, not a 12-GW window,
   and is capped to the viewport with the list scrolling inside it. Both halves
   of that matter and neither works alone. Uncapped, 38 rows made the panel
   several screens tall, which defeats the sticky positioning the panel exists
   to have. Capped without scrolling — the previous arrangement — the rows past
   the cap were simply absent, with nothing on screen to distinguish "this
   team's season ends here" from "the list stopped here". A scrollbar says
   there is more; a silent slice says nothing.
   ═══════════════════════════════════════════════════════════════════════════ */

.gw-nav,
.team-nav {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  /* Stays in view while the (much taller) breakdown cards scroll past it. */
  position: sticky;
  top: calc(var(--app-header-height) + var(--space-6));
}

/* The team navigator is a column whose header is fixed and whose list takes
   whatever height is left, so the panel as a whole never outgrows the viewport
   it is pinned inside. The GW navigator is deliberately excluded: it holds one
   gameweek's fixtures — ten rows at most — and would never reach the cap, so
   giving it a scroll container only risks one appearing at an awkward zoom. */
.team-nav {
  display: flex;
  flex-direction: column;
  /* The panel ends exactly where the two matchup cards beside it do — it is
     one of three columns in a row, and a fourth column of unrelated length
     hanging below the other two read as an accident rather than a choice.
     Its own list is 38 rows long and would otherwise set the row's height
     itself, so this is not something align-self can express.

     height: 0 + min-height is the grid idiom for "fill the row, but do not
     size it". A definite height of 0 contributes nothing when the auto row is
     measured, so the row takes its height from .matchup-grid (and .gw-nav)
     alone; the percentage in min-height then resolves against that settled
     row height and stretches the panel back over it. align-self: stretch
     cannot do this — a stretched item still contributes its content height to
     the row, which is the 38-row list this rule exists to ignore.

     The min() keeps the viewport cap the panel has always had: the cards can
     run far past one screen, and matching them literally would leave a sticky
     panel taller than the window it is pinned inside — the inner list, not the
     panel, is what absorbs the overflow (.gw-nav__list--scroll below). The cap
     is the viewport less the sticky nav above it and the same gap on both
     sides, so the panel's bottom edge lands where its top offset implies it
     should. It is a min-height rather than a max-height because min-height
     wins over max-height when the two disagree, which would have unpinned the
     cap the moment the cards grew past it.

     Both are reset at the 900px breakpoint below, where each panel is its own
     single-column row: a row sized by an item contributing 0 would be 0 tall,
     and 100% of that would collapse the panel outright. */
  height: 0;
  min-height: min(
    100%,
    calc(100vh - var(--app-header-height) - (var(--space-6) * 2))
  );
  /* Third column of .matchup-shell__main, which is now a single-row grid —
     the season strip is a sibling of that grid, not a second row of it, so
     this panel simply ends level with the cards beside it. It used to carry
     grid-row: 1 / 3 to stretch down past them, alongside a strip that
     occupied only columns 1-2; see .matchup-shell for why that arrangement
     could not survive the strip going full width.

     Auto-placement would put it here anyway (it is the third item in source
     order), but the column stays pinned: it is what the layout means, and it
     keeps the panel on the right regardless of what else joins the row. */
  grid-column: 3;
}

/* Reset the column once the row collapses to a single one
   (.matchup-shell__main's own 900px override, MATCHUP SHELL section top of
   file). This has to sit AFTER the unconditional rule above, not merely
   inside a media query positioned earlier in the file — a media condition
   being true does not move a rule later in the cascade; two rules of equal
   specificity still resolve by source order, so a mobile override written
   above its unconditional counterpart loses to it. Left unreset, grid-column:
   3 would ask for a 3rd column that no longer exists in the collapsed
   1-column grid, growing a phantom implicit one to satisfy it and quietly
   reintroducing a side-by-side layout under the stack this breakpoint exists
   to produce. */
@media (max-width: 900px) {
  .team-nav {
    grid-column: auto;
  }
}

.gw-nav__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.gw-nav__title {
  flex: 1;
  text-align: center;
  font-size: var(--font-size-sm);
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--color-text);
}

/* Team navigator's header title: same slot as .gw-nav__title above, just
   with the team's badge spaced before its name instead of plain text. */
.gw-nav__title--team {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
}

/* Hidden outright (not merely disabled) at the ends of the navigable list —
   see buildNavPanel() in matchup.js, which sets the `hidden` attribute
   ([hidden]{display:none!important} in base.css) rather than `disabled`. */
.gw-nav__arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  flex: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  font-size: var(--font-size-md);
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}

.gw-nav__arrow:hover {
  background: var(--color-surface-raised);
  color: var(--color-text);
  border-color: var(--color-muted);
}

.gw-nav__arrow:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.gw-nav__list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  list-style: none;
}

/* Chip-reset boundary — the hairline between GW19 and GW20 in the team
   navigator. FPL reissues every chip after GW19, so fixtures either side of
   this line cannot be planned against the same chips; without the rule the
   list reads as one uninterrupted run.

   A rule, not a row: zero height, no padding and no hit area, so it separates
   the rows without ever looking like one of them. The margin is what gives it
   room to read as deliberate against the list's own 2px row gap. */
.gw-nav__chip-reset {
  height: 0;
  border-top: 1px solid var(--color-border);
  margin: var(--space-2) var(--space-1);
}

/* The team navigator's full-season list. Only this variant scrolls — see the
   .team-nav note above for why the GW navigator does not. */
.gw-nav__list--scroll {
  overflow-y: auto;
  /* A flex child's default min-height:auto refuses to shrink below its content,
     which would push the panel past its max-height and never produce a
     scrollbar at all. This is what actually makes the cap take effect. */
  min-height: 0;
  /* Reaching the end of the list must not start scrolling the page underneath
     it — the panel is sticky, so a chained scroll moves content the reader was
     not pointing at. */
  overscroll-behavior: contain;
  /* Row height plus the list's gap, so a row activated by keyboard is never
     left half-clipped against either edge. */
  scroll-padding: var(--space-4) 0;
  scrollbar-width: thin;
  scrollbar-color: var(--color-border) transparent;
  /* Breathing room for the scrollbar so it does not sit on top of the rows'
     trailing GW label. */
  padding-right: var(--space-1);
}

.gw-nav__list--scroll::-webkit-scrollbar {
  width: 8px;
}

.gw-nav__list--scroll::-webkit-scrollbar-track {
  background: transparent;
}

.gw-nav__list--scroll::-webkit-scrollbar-thumb {
  background: var(--color-border);
  border-radius: var(--radius-pill);
}

.gw-nav__list--scroll::-webkit-scrollbar-thumb:hover {
  background: var(--color-muted);
}

/* Score chip / home / dash / away / score chip as an auto 1fr auto 1fr auto
   grid, home right-aligned and away left-aligned — the dash sits on one
   shared centre axis across every row regardless of how the two shortnames'
   widths differ, and the two outer score chips sit flush at the row edges. */
.gw-nav__fixture {
  display: grid;
  grid-template-columns: auto 1fr auto 1fr auto;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background 0.12s, border-color 0.12s;
}

/* Each side's own CompositeScore for this fixture — reuses .score-chip
   (band-coloured pill, same markup as the counter-matchup pairing chips) so
   it reads identically wherever a Gaffer IQ score is shown. */
.gw-nav__score {
  justify-self: center;
}

/* The nav row is already tight on width and the chip's dashed border
   (.score-chip--estimated above) already signals low confidence, so the ~
   prefix is redundant here — drop it to save space. Scoped to this class
   only; every other .score-chip--estimated usage (dashboard, ranker,
   planner) keeps the ~. */
.gw-nav__score.score-chip--estimated::before {
  content: none;
}

.gw-nav__fixture:hover {
  background: var(--color-surface-raised);
}

.gw-nav__fixture.is-selected {
  background: var(--color-accent-muted);
  border-color: var(--color-accent);
}

.gw-nav__fixture:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.gw-nav__team {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.gw-nav__team--home { justify-content: flex-end; }
.gw-nav__team--away { justify-content: flex-start; }

.gw-nav__badge {
  width: 20px;
  height: 20px;
  object-fit: contain;
  border-radius: 4px;
  flex: none;
}

.gw-nav__short {
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text);
}

.gw-nav__fixture.is-selected .gw-nav__short {
  color: var(--color-accent);
}

.gw-nav__dash {
  color: var(--color-muted);
  font-size: var(--font-size-xs);
}

/* Team navigator's row layout: score chip, opponent, H/A letter, GW label —
   4 columns instead of the GW nav's 5 (no second team, no second chip). All
   four are auto (content-hugging), not a stretched 1fr — a 1fr opponent
   column left the short opponent code flush left in a wide column, stranding
   H/A + GW at the far right edge and reading as two separate clusters with a
   dead gap between. Auto columns keep the whole row as one tight group, with
   any leftover row width landing as trailing space past GW instead of in the
   middle. See buildTeamFixtureRow in matchup.js. */
.gw-nav__fixture--team {
  grid-template-columns: auto auto auto auto;
  justify-content: start;
}

/* Single-letter H/A indicator. Reuses the app's existing green/red status
   tokens — same pair .squad-import-status--success/error use below — rather
   than introducing new colours. Deliberately not bold: same weight as the
   row's other small text (.gw-nav__dash). */
.gw-nav__venue {
  font-size: var(--font-size-xs);
  font-weight: 400;
  text-align: center;
}

.gw-nav__venue--home { color: var(--band-green); }
/* Primitives, not band tokens: an away fixture is not a 21-35 rating, it is
   just away. These two used to borrow --band-great/--band-brutal (the same
   pair .squad-import-status--success/error uses), and --band-brutal has since
   been re-pointed twice — red, then orange, then back to red — while this
   letter never meant anything but "away". See base.css. */
.gw-nav__venue--away { color: var(--band-red); }

/* ═══════════════════════════════════════════════════════════════════════════
   MATCHUP GRID — two-column layout for home/away cards
   ═══════════════════════════════════════════════════════════════════════════ */

.matchup-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-6);
  align-items: start;
}

@media (max-width: 800px) {
  .matchup-grid {
    grid-template-columns: 1fr;
  }
}

.matchup-status {
  grid-column: 1 / -1;
  text-align: center;
  color: var(--color-muted);
  padding: var(--space-8) 0;
  font-size: var(--font-size-md);
}

/* ═══════════════════════════════════════════════════════════════════════════
   MATCHUP CARD
   ═══════════════════════════════════════════════════════════════════════════ */

.matchup-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  /* Band-specific left accent applied by modifier below */
  border-left-width: 3px;
  border-left-style: solid;
  border-left-color: var(--color-border);
}

.matchup-card--great   { border-left-color: var(--band-great); }
.matchup-card--good    { border-left-color: var(--band-good); }
.matchup-card--neutral { border-left-color: var(--band-neutral); }
.matchup-card--tough   { border-left-color: var(--band-tough); }
.matchup-card--brutal  { border-left-color: var(--band-brutal); }
/* The two tiers the seven-tier scale added at the ends of the old five.
   The bottom one takes its INK, not its chrome: this stripe is a bare 3px
   mark with the card behind it, and --band-extreme is now black, which on a
   #161b22 card is a stripe you cannot see. Same reason the breakdown bars
   below read the ink. */
.matchup-card--excellent { border-left-color: var(--band-excellent); }
.matchup-card--extreme   { border-left-color: var(--band-extreme-ink); }

/* Low-confidence cards get a hatched overlay so the provisional nature is
   immediately visible without hiding the data — see ARCHITECTURE.md §11. */
.matchup-card--provisional {
  background-image: repeating-linear-gradient(
    45deg,
    transparent,
    transparent 6px,
    rgba(255, 255, 255, 0.02) 6px,
    rgba(255, 255, 255, 0.02) 12px
  );
}

.matchup-card__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-6) var(--space-3);
  border-bottom: 1px solid var(--color-border-subtle);
}

.matchup-card__team {
  display: flex;
  align-items: center;
  font-size: var(--font-size-lg);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.2;
}

/* Crest pulled from the FPL bootstrap CDN (team.badgeUrl, normalise.js).
   onerror on the <img> itself hides it silently on a 404/missing badge —
   no fallback box, no layout shift, the team name alone still reads fine. */
.matchup-card__badge {
  width: 32px;
  height: 32px;
  margin-right: var(--space-2);
  border: none;
  border-radius: 6px;
  object-fit: contain;
  flex: none;
}

.matchup-card__venue {
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-muted);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 2px var(--space-2);
}

/* ─── Score row: pill + FDR + confidence ─────────────────────────────────── */

.matchup-card__score-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-6);
  border-bottom: 1px solid var(--color-border-subtle);
}

/* ═══════════════════════════════════════════════════════════════════════════
   SCORE PILL — main composite score display
   Band modifiers key off CompositeScore.band (CONVENTIONS.md §5.2).
   ═══════════════════════════════════════════════════════════════════════════ */

.score-pill {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  min-width: 76px;
  flex-shrink: 0;
}

.score-pill--great   { background: var(--band-great-bg);   border-color: var(--band-great);   color: var(--band-great); }
.score-pill--good    { background: var(--band-good-bg);    border-color: var(--band-good);    color: var(--band-good); }
.score-pill--neutral { background: var(--band-neutral-bg); border-color: var(--band-neutral); color: var(--band-neutral); }
.score-pill--tough   { background: var(--band-tough-bg);   border-color: var(--band-tough);   color: var(--band-tough); }
.score-pill--brutal  { background: var(--band-brutal-bg);  border-color: var(--band-brutal);  color: var(--band-brutal); }
/* Top tier carries a glow the other six do not — the pill is the card's
   headline, and 80+ is the one reading the page exists to spot. Gold chrome,
   silver ink: see --band-excellent-ink in base.css for why this tier alone
   splits the two. The text glow is warm-white rather than pure white so the
   digits read as lit BY the gold around them; a cold white halo made the
   number look pasted on. */
.score-pill--excellent {
  background: var(--band-excellent-bg);
  border-color: var(--band-excellent);
  color: var(--band-excellent-ink);
  box-shadow: 0 0 12px color-mix(in srgb, var(--band-excellent) 35%, transparent);
  text-shadow:
    0 0 10px color-mix(in srgb, var(--band-excellent-ink) 60%, transparent),
    0 0 20px color-mix(in srgb, var(--band-excellent) 45%, transparent);
}
/* The bottom tier is the top tier's mirror: a fill that names it, an ink that
   is not the fill, and an edge where the middle five have none. Black where
   gold has gold, the darkest red in the palette where gold has silver — and
   the border takes that same ink, since a black border on a black fill is no
   border at all and this tier has to read as bounded rather than as a hole in
   the card. No glow, unlike the top tier: gold glows because 80+ is the thing
   the reader is hunting for, and this is the opposite errand. */
.score-pill--extreme {
  background: var(--band-extreme-bg);
  border-color: var(--band-extreme-ink);
  color: var(--band-extreme-ink);
}

/* Provisional: muted + hatched — score still shown, never hidden */
.score-pill--provisional {
  opacity: 0.6;
  background-image: repeating-linear-gradient(
    45deg,
    transparent,
    transparent 4px,
    rgba(255, 255, 255, 0.05) 4px,
    rgba(255, 255, 255, 0.05) 8px
  );
}

/* Estimated: low-confidence score — dashed border, never hidden. Combined
   with a band modifier: score-pill--good score-pill--estimated. No separate
   "~" marker — the dashed border + reduced opacity already carry the signal
   (matchup page's no-symbol convention). See ARCHITECTURE.md §11,
   FEATURE_ENGINE.md §8.3. */
.score-pill--estimated {
  border-style: var(--estimated-border-style);
  opacity: 0.82;
}

.score-pill__value {
  font-size: var(--font-size-2xl);
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.score-pill__band {
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin-top: 2px;
}

/* ═══════════════════════════════════════════════════════════════════════════
   SCORE CHIP — compact inline score used in counter-matchup pairings
   Same band modifiers, no double-line layout.
   ═══════════════════════════════════════════════════════════════════════════ */

.score-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 2px var(--space-2);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-sm);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.4;
  min-width: 32px;
  flex-shrink: 0;
}

.score-chip--great   { background: var(--band-great-bg);   color: var(--band-great); }
.score-chip--good    { background: var(--band-good-bg);    color: var(--band-good); }
.score-chip--neutral { background: var(--band-neutral-bg); color: var(--band-neutral); }
.score-chip--tough   { background: var(--band-tough-bg);   color: var(--band-tough); }
.score-chip--brutal  { background: var(--band-brutal-bg);  color: var(--band-brutal); }
/* Same gold-chrome/silver-ink split as the pill, at chip scale — where it
   matters more, not less: these digits are 11px and were the hardest gold
   text on the page to read. Glow radii scale down with the type.
   The gold edge is an INSET ring, not a border: .score-chip has none, and a
   real 1px border would grow this tier's chips by 2px in each axis — enough
   to jog the row it sits in, and only for the rows that happen to be 80+. The
   ring follows --radius-sm on its own, and the outer glow still reads as the
   pill's does because both come off the same box-shadow. */
.score-chip--excellent {
  background: var(--band-excellent-bg);
  color: var(--band-excellent-ink);
  box-shadow:
    inset 0 0 0 1px var(--band-excellent),
    0 0 6px color-mix(in srgb, var(--band-excellent) 45%, transparent);
  text-shadow:
    0 0 6px color-mix(in srgb, var(--band-excellent-ink) 55%, transparent),
    0 0 12px color-mix(in srgb, var(--band-excellent) 50%, transparent);
}
/* The mirror of .score-chip--excellent above: black fill, dark red ink, and a
   ring in that same ink — INSET for the same reason gold's is, since
   .score-chip has no border and a real 1px one would grow only this tier's
   chips by 2px in each axis and jog the rows they sit in. The ring is what
   makes the tier visible here at all: a black fill on a dark card is
   otherwise just a gap in the row. No glow — see .score-pill--extreme. */
.score-chip--extreme {
  background: var(--band-extreme-bg);
  color: var(--band-extreme-ink);
  box-shadow: inset 0 0 0 1px var(--band-extreme-ink);
}

/* Rank-relative tiers (pool-percentile, NOT the 0-100 band above — see
   FEATURE_ENGINE.md §13). Rendered ALONGSIDE a .score-chip--{band} class, never
   instead of it — these rules come after the seven band rules above so they win
   the cascade at equal specificity, without needing !important or JS to strip
   the band class. Every player now gets exactly one of these six (calcRankTier
   no longer returns null except for a malformed/empty pool), so in practice
   this block fully determines colour, not just for a handful of standouts.
   rank-neutral/rank-yellow are their OWN classes (not a reuse of .score-chip--
   neutral/.score-chip--tough directly) specifically so their cascade position
   here — after ALL SEVEN band classes, not just some — is guaranteed regardless
   of which band a given player's own absolute score happens to carry.

   These are named after their COLOUR, not their tier, so they read the
   primitives directly: this axis is deliberately not on the 0-100 scale and
   must not move when that scale is re-pointed.

   The blue was light green until the seven-tier scale landed. Two greens one
   step apart is a distinction the eye has to work for, and the column was
   asking it to do that ~700 rows deep; against the dark green above it, blue
   separates at a glance and costs nothing, since nothing else in this table
   is blue. It reads --band-light-blue, the same primitive the 'good' band
   reads: these two axes are independent and must be free to move apart, but
   "the blue one" has to be ONE blue app-wide, or a reader carrying a colour
   from the Matchup page to this table finds a second, darker blue here and
   has to ask whether the difference means something. The deep #1f6feb this
   used to be lost that argument for the same reason it lost it as a band —
   at 11px on this ground it is the one hue that has to be hunted for. */
.score-chip--rank-green       { background: var(--band-dark-green-bg); color: var(--band-dark-green); }
.score-chip--rank-light-green { background: var(--band-light-blue-bg); color: var(--band-light-blue); }
.score-chip--rank-red         { background: var(--band-red-bg);        color: var(--band-red); }
.score-chip--rank-neutral     { background: var(--band-neutral-bg);    color: var(--band-neutral); }
.score-chip--rank-yellow      { background: var(--band-amber-bg);      color: var(--band-amber); }

/* Rank 1 of a position — the gold the 0-100 scale gives its top tier, on the
   one rank tier scarce enough to earn it: exactly four chips on the whole
   table, one per position, however the pool moves (calcRankTier,
   'positionBest'). Same gold-chrome/silver-ink split and the same glow as
   .score-chip--excellent, so "best there is" reads identically whether it was
   earned on the absolute scale or the relative one. Last in the block, so it
   beats rank-green for the player who is both. */
.score-chip--rank-gold {
  background: var(--band-excellent-bg);
  color: var(--band-excellent-ink);
  box-shadow:
    inset 0 0 0 1px var(--band-excellent),
    0 0 6px color-mix(in srgb, var(--band-excellent) 45%, transparent);
  text-shadow:
    0 0 6px color-mix(in srgb, var(--band-excellent-ink) 55%, transparent),
    0 0 12px color-mix(in srgb, var(--band-excellent) 50%, transparent);
}

/* Estimated compact chip — dashed border in the band's own colour, ~ prefix. */
.score-chip--estimated {
  border: 1px var(--estimated-border-style) currentColor;
  opacity: 0.82;
}
.score-chip--estimated::before {
  content: '~';
  font-size: 0.7em;
  opacity: 0.7;
  margin-right: 1px;
}

/* ═══════════════════════════════════════════════════════════════════════════
   FDR COMPARISON — official FPL 1–5 rating shown alongside Gaffer IQ score
   FDR direction: 1 = very easy, 5 = very hard (opposite to Gaffer IQ).
   ═══════════════════════════════════════════════════════════════════════════ */

.fdr-comparison {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-width: 52px;
  max-width: 90px;
}

.fdr-comparison__label {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  text-align: center;
  line-height: 1.2;
}

.fdr-comparison__value {
  font-size: var(--font-size-xl);
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  /* FDR 1=easy→great colour, 5=hard→brutal colour (inverted vs Gaffer IQ) */
}

.fdr-comparison__value[data-fdr="1"] { color: var(--band-dark-green); }
.fdr-comparison__value[data-fdr="2"] { color: var(--band-green); }
.fdr-comparison__value[data-fdr="3"] { color: var(--band-neutral); }
.fdr-comparison__value[data-fdr="4"] { color: var(--band-amber); }
.fdr-comparison__value[data-fdr="5"] { color: var(--band-red); }

.fdr-comparison__scale {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  line-height: 1;
}

/* ═══════════════════════════════════════════════════════════════════════════
   CONFIDENCE INDICATOR
   ═══════════════════════════════════════════════════════════════════════════ */

.confidence-indicator {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-width: 60px;
  margin-left: auto;
}

.confidence-indicator__label {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  white-space: nowrap;
}

.confidence-indicator__value {
  font-size: var(--font-size-lg);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
}

/* Low confidence: provisional indicator in caution colour */
.confidence-indicator--low .confidence-indicator__value {
  color: var(--band-amber);
}

.confidence-indicator--low .confidence-indicator__label::after {
  content: ' ⚠';
  font-style: normal;
}

/* ═══════════════════════════════════════════════════════════════════════════
   SCORE BREAKDOWN SECTION
   ═══════════════════════════════════════════════════════════════════════════ */

.matchup-card__breakdown,
.matchup-card__counter,
.matchup-card__horizon {
  padding: var(--space-4) var(--space-6);
  border-bottom: 1px solid var(--color-border-subtle);
}

/* Last visible section has no bottom border */
.matchup-card__counter:last-child,
.matchup-card__horizon:last-child {
  border-bottom: none;
}

.matchup-card__section-title {
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--color-muted);
  margin-bottom: var(--space-3);
}

/* ─── Breakdown row: label | bar | value | weight | est? ─────────────────── */

/* Standalone row grid — label | bar | value | weight | est marker. This is
   dashboard.js's layout (its rows are siblings of other content, not of each
   other). matchup.js overrides it below via .breakdown-rows. */
.breakdown-row {
  display: grid;
  grid-template-columns: 9.5rem 1fr 2.4rem 2.4rem auto;
  gap: var(--space-2);
  align-items: center;
  padding: var(--space-1) 0;
}

/* matchup.js's Score Breakdown: label | maturity counter | bar | value | weight.
   ONE grid for the whole block, with each row `display: contents`, so the
   counter column is a single shared track sized by the widest counter present.
   Six independent row grids would each size their own `auto` track instead,
   and the bars would step in and out by the counter's width from row to row.
   Row padding moves to row-gap, since a contents row has no box to pad. */

/* Both flexible tracks are minmax()'d rather than left at their defaults: an
   `auto` minimum makes a track refuse to shrink below its content, and at the
   card widths this grid actually gets on a 1280px screen the row then
   overflowed the card — which has overflow:hidden, so the rightmost column
   was silently cut off rather than visibly wrapping. The label already carries
   text-overflow:ellipsis for exactly this case; it just never got the chance
   to use it. The bar keeps a floor of 0 so it yields before the label does. */
.breakdown-rows {
  display: grid;
  grid-template-columns:
    minmax(4rem, 9.5rem) auto minmax(0, 1fr) 2.4rem 2.4rem auto;
  column-gap: var(--space-2);
  row-gap: var(--space-2);
  align-items: center;
}

.breakdown-rows > .breakdown-row { display: contents; }

/* How far a ramping metric is toward carrying its full weight (n/N). Mono and
   muted: it is a progress reading, not a score, and must not compete with the
   value or the weight either side of the bar. */
.breakdown-row__maturity {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  font-variant-numeric: tabular-nums;
  color: var(--color-muted);
  white-space: nowrap;
  cursor: help;
  text-align: right;
}

/* Rows that don't ramp still emit the cell, to keep the shared grid in phase.
   When NO row in the block has a counter the track collapses to zero width,
   and this cancels the column gap it would otherwise leave behind. */
.breakdown-rows:not(:has(.breakdown-row__maturity:not(:empty))) .breakdown-row__maturity {
  margin-right: calc(var(--space-2) * -1);
}

.breakdown-row__label {
  font-size: var(--font-size-sm);
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.breakdown-row__bar-wrap {
  background: var(--color-surface-raised);
  border-radius: var(--radius-sm);
  height: 6px;
  overflow: hidden;
}

.breakdown-row__bar {
  height: 100%;
  border-radius: var(--radius-sm);
  transition: width 0.3s ease;
  /* Colour keyed by band modifier below */
  background: var(--color-muted);
}

.breakdown-row__bar--great   { background: var(--band-great); }
.breakdown-row__bar--good    { background: var(--band-good); }
.breakdown-row__bar--neutral { background: var(--band-neutral); }
.breakdown-row__bar--tough   { background: var(--band-tough); }
.breakdown-row__bar--brutal  { background: var(--band-brutal); }
.breakdown-row__bar--excellent { background: var(--band-excellent); }
/* Ink, not chrome: a bar is a bare mark on a track, and black would paint
   nothing. Same call as .matchup-card--extreme's stripe above. */
.breakdown-row__bar--extreme   { background: var(--band-extreme-ink); }

/* Estimated sub-metric bar: reduced opacity + striped overlay (the "dashed" fill). */
.breakdown-row__bar--estimated {
  opacity: 0.55;
  background-image: repeating-linear-gradient(
    90deg,
    rgba(255, 255, 255, 0)    0px,
    rgba(255, 255, 255, 0)    4px,
    rgba(255, 255, 255, 0.2)  4px,
    rgba(255, 255, 255, 0.2)  8px
  );
}

.breakdown-row__value {
  font-size: var(--font-size-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
  text-align: right;
}

.breakdown-row__weight {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Estimated rows: label muted, slight visual distinction */
.breakdown-row--estimated .breakdown-row__label {
  color: var(--color-muted);
}

/* Still used by dashboard.js's own breakdown rows (its "~" convention is out
   of scope for the matchup-page no-symbol change — matchup.js no longer
   emits this class, so it simply never appears there). */
.breakdown-row__est {
  font-size: var(--font-size-xs);
  color: var(--band-amber);
  font-weight: 700;
  cursor: default;
  width: 12px;
  text-align: center;
}

/* ─── Metric "i" popup ─ what a breakdown row actually measures ──────── */
/* Same round "i" affordance as .counter-pairing-info__btn on the pairings
   below, in its own column at the right edge so all six line up. The panel it
   opens differs in ONE respect: it overlays instead of expanding. A pairing's
   panel is the content the reader came for and can afford to push the rows
   apart; this one is a one-sentence footnote, and pushing five rows down to
   show it would shift every number the reader was mid-comparison on.

   The <summary> is the button itself, so the affordance is what takes focus
   and the popup is reachable from the keyboard. Dismissal on an outside click
   is wired in matchup.js (closeMetricInfo) — <details> has no native
   light-dismiss. */
.breakdown-row__info {
  position: relative;
  justify-self: end;
}

.breakdown-row__info-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background: var(--color-surface-raised);
  color: var(--color-muted);
  font-size: var(--font-size-xs);
  font-weight: 700;
  font-style: italic;
  line-height: 1;
  cursor: pointer;
  list-style: none;
  transition: color 0.1s, border-color 0.1s;
}

.breakdown-row__info-btn::-webkit-details-marker {
  display: none;
}

.breakdown-row__info-btn:hover,
.breakdown-row__info[open] .breakdown-row__info-btn {
  color: var(--color-accent);
  border-color: var(--color-accent);
}

.breakdown-row__info-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Overlay panel, matched to .squad-import-help__panel so every dropdown on the
   site reads alike. Anchored right so it opens inward from the card edge. */
.breakdown-row__info-pop {
  position: absolute;
  top: calc(100% + var(--space-1));
  right: 0;
  z-index: 50;
  width: 15rem;
  max-width: calc(100vw - var(--space-4) * 2);
  padding: var(--space-3);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  text-align: left;
  /* The row above is a grid of nowrap numbers; the popup is prose. */
  white-space: normal;
  font-weight: 400;
}

.breakdown-row__info-title {
  display: block;
  margin-bottom: var(--space-1);
  font-size: var(--font-size-xs);
  font-weight: 700;
  color: var(--color-text);
}

.breakdown-row__info-text {
  margin: 0;
  font-size: var(--font-size-xs);
  line-height: 1.5;
  color: var(--color-muted);
}

/* ═══════════════════════════════════════════════════════════════════════════
   COUNTER-MATCHUP PAIRINGS
   ═══════════════════════════════════════════════════════════════════════════ */

.counter-pairing {
  display: grid;
  grid-template-columns: 8rem auto 1fr auto;
  gap: var(--space-2);
  align-items: center;
  padding: var(--space-2) 0;
}

.counter-pairing + .counter-pairing {
  border-top: 1px solid var(--color-border-subtle);
}

.counter-pairing__label {
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text);
  white-space: nowrap;
}

/* .counter-pairing__score uses .score-chip classes */

.counter-pairing__detail {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  font-variant-numeric: tabular-nums;
  padding-left: var(--space-2);
}

/* Estimated pairings: slightly muted */
.counter-pairing--estimated .counter-pairing__label {
  color: var(--color-muted);
}

/* ─── Pairing info popup — reveals the named players behind a pairing ─── */
/* The "i" at the row's right edge is a <details> of its own, exactly as the
   breakdown rows above are: the <summary> IS the button, so the affordance is
   what takes focus, and the panel it opens OVERLAYS the card instead of
   expanding inside it.

   It used to be the whole row that was the <summary>, with the panel in flow
   below it — opening one pushed every pairing under it down the card, which
   moved the numbers the reader was mid-comparison on. Same reason the metric
   popups overlay; both are now the same interaction.

   Light-dismiss (outside click, Escape) is wired in matchup.js alongside the
   metric popups — <details> has neither natively. */

/* The row-separator border lives on the row wrapper: the .counter-pairing
   elements are each inside their own flex row beside the "i". */
.counter-pairing-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  /* The popup anchors to the ROW, not to the 18px "i" beside it: a pairing
     panel carries an explanation and a player list, which is wider than the
     card at any fixed width. Spanning the row keeps it inside the card. */
  position: relative;
}

.counter-pairing-row + .counter-pairing-row {
  border-top: 1px solid var(--color-border-subtle);
}

/* The pairing keeps its own grid; it just has to fill the row now. */
.counter-pairing-row .counter-pairing {
  flex: 1;
  min-width: 0;
}

.counter-pairing-info {
  flex-shrink: 0;
}

.counter-pairing-info__btn {
  flex-shrink: 0;
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background: var(--color-surface-raised);
  color: var(--color-muted);
  font-size: var(--font-size-xs);
  font-weight: 700;
  font-style: italic;
  line-height: 1;
  transition: color 0.1s, border-color 0.1s;
}

.counter-pairing-info__btn::-webkit-details-marker {
  display: none;
}

.counter-pairing-info__btn:hover,
.counter-pairing-info[open] .counter-pairing-info__btn {
  color: var(--color-accent);
  border-color: var(--color-accent);
}

.counter-pairing-info__btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Overlay panel, matched to .breakdown-row__info-pop above — same chrome, but
   spanning the row rather than sized to the "i", because this one carries an
   explanation and a player list. Capped in height so a long duel list scrolls
   inside the popup instead of running off the card. */
.counter-pairing-info__panel {
  position: absolute;
  top: calc(100% + var(--space-1));
  left: 0;
  right: 0;
  z-index: 50;
  max-height: min(60vh, 26rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-3);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  text-align: left;
  white-space: normal;
  font-weight: 400;
  cursor: default;
}

/* Explanation block at the top of a pairing's "i" panel — what the score and
   the two percentages mean, above the named players. Separated from the player
   rows by a rule so the two halves of the panel read as distinct answers. */
.counter-pairing-info__explain {
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-3);
  border-bottom: 1px solid var(--color-border-subtle);
}

/* No bottom border when the panel has no player rows under it — otherwise the
   explanation appears to underline an empty space. */
.counter-pairing-info__explain:last-child {
  padding-bottom: 0;
  margin-bottom: 0;
  border-bottom: none;
}

.counter-pairing-info__note {
  margin: 0;
  font-size: var(--font-size-xs);
  line-height: 1.5;
  color: var(--color-muted);
}

.counter-pairing-info__terms {
  list-style: none;
  margin: var(--space-2) 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.counter-pairing-info__terms li {
  display: grid;
  /* Fixed first column so the two definitions align regardless of percentage
     width ("Atk 6%" vs "Atk 97%"). */
  grid-template-columns: 5.5rem 1fr;
  gap: var(--space-2);
  align-items: baseline;
  font-size: var(--font-size-xs);
  line-height: 1.5;
  color: var(--color-muted);
}

.counter-pairing-info__term {
  font-weight: 600;
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 767px) {
  /* Stack the term above its definition rather than letting a 5.5rem column
     squeeze the text into a narrow ribbon. */
  .counter-pairing-info__terms li {
    grid-template-columns: 1fr;
    gap: 0;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   INDIVIDUAL DUELS — player-vs-player rows
   Rendered inside the pairing info panels above, which name the players behind
   one pairing. The standalone "Individual Duels" disclosure these were shared
   with has been removed, so this is now their only caller.
   ═══════════════════════════════════════════════════════════════════════════ */

.individual-duel {
  display: grid;
  grid-template-columns: 1fr auto 1fr auto;
  gap: var(--space-2);
  align-items: center;
  padding: var(--space-1) 0;
  font-size: var(--font-size-xs);
}

.individual-duel + .individual-duel {
  border-top: 1px solid var(--color-border-subtle);
}

.individual-duel__attacker,
.individual-duel__defender {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  min-width: 0;
}

.individual-duel__defender {
  justify-content: flex-end;
  text-align: right;
}

/* Role tag (ST / CB / WM …) — quiet, so the player name stays dominant. */
.individual-duel__role {
  flex-shrink: 0;
  font-size: 0.65rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--color-muted);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  padding: 0 3px;
}

.individual-duel__form {
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
  color: var(--color-muted);
}

.individual-duel__vs {
  flex-shrink: 0;
  color: var(--color-muted);
  font-style: italic;
}

/* ═══════════════════════════════════════════════════════════════════════════
   HORIZON OUTLOOK SECTION — scoreOverHorizon perGw strip + aggregate
   Appears in the matchup card when the active horizon is GW3 or GW6.
   ═══════════════════════════════════════════════════════════════════════════ */

.horizon-summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.horizon-summary__score {
  font-size: var(--font-size-md);
  min-width: 36px;
}

.horizon-summary__label {
  font-size: var(--font-size-sm);
  color: var(--color-muted);
  text-transform: capitalize;
}

/* ─── perGw fixture strip ─────────────────────────────────────────────────── */

.pgw-strip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  /* Children are .pgw-slot column stacks (cells + a gameweek label) and, when a
     team has postponements, a .pgw-pending pill that has no label and is
     therefore shorter. Aligning to the start keeps every slot's cell row on the
     same line; the default `stretch` would distort the pill instead. */
  align-items: flex-start;
}

.pgw-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 28px;
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  cursor: default;
  border: 1px solid transparent;
}

.pgw-cell--great   { background: var(--band-great-bg);   color: var(--band-great);   border-color: var(--band-great); }
.pgw-cell--good    { background: var(--band-good-bg);    color: var(--band-good);    border-color: var(--band-good); }
.pgw-cell--neutral { background: var(--band-neutral-bg); color: var(--band-neutral); border-color: var(--band-neutral); }
.pgw-cell--tough   { background: var(--band-tough-bg);   color: var(--band-tough);   border-color: var(--band-tough); }
.pgw-cell--brutal  { background: var(--band-brutal-bg);  color: var(--band-brutal);  border-color: var(--band-brutal); }
.pgw-cell--excellent {
  background: var(--band-excellent-bg);
  color: var(--band-excellent-ink);
  border-color: var(--band-excellent);
  text-shadow:
    0 0 6px color-mix(in srgb, var(--band-excellent-ink) 55%, transparent),
    0 0 12px color-mix(in srgb, var(--band-excellent) 50%, transparent);
}
/* Black cell, dark red border and digits — the same treatment as
   .score-chip--extreme, on the surface where these cells sit beside the
   excellent cell's gold. The cell already reserves a 1px border, so this one
   is a real border rather than an inset ring. */
.pgw-cell--extreme {
  background: var(--band-extreme-bg);
  color: var(--band-extreme-ink);
  border-color: var(--band-extreme-ink);
}

/* Estimated fixture cell — dashed border, reduced opacity. */
.pgw-cell--estimated {
  border-style: var(--estimated-border-style);
  opacity: 0.7;
}

/* ─── Gameweek slots — schedule irregularities ──────────────────────────────
   One slot per GAMEWEEK, holding zero, one or two fixture cells.

   The governing rule: schedule structure is carried by GEOMETRY, fixture
   quality by COLOUR, and the two never overlap. Colour was already fully
   committed — the five band colours mean fixture quality in all four modules
   (CONVENTIONS §5.2) — and so was the dashed border, which means low MODEL
   confidence (--estimated-border-style, just above). A double against brutal
   opposition must therefore read as red-and-grouped rather than reaching for a
   third colour that means neither thing.
   See FEATURE_ENGINE.md §9.1. */

.pgw-slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.pgw-slot__cells {
  display: flex;
  gap: 2px;
}

.pgw-slot__label {
  font-size: 0.5625rem;
  line-height: 1;
  color: var(--color-muted);
  font-variant-numeric: tabular-nums;
}

/* Double: the two cells are bound into one week by an accent-tinted group.
   --color-accent is the app's structural-emphasis colour and is NOT one of the
   five band colours, so this can never be misread as a quality signal. */
.pgw-slot--double .pgw-slot__cells {
  padding: 2px;
  border-radius: var(--radius-sm);
  background: var(--color-accent-muted);
  outline: 1px solid color-mix(in srgb, var(--color-accent) 35%, transparent);
}

.pgw-slot--double .pgw-slot__label {
  color: var(--color-accent);
}

/* Blank: hatched, so a known-empty week is visibly different from a failed
   load. The bare "–" this replaces was indistinguishable from missing data. */
.pgw-cell--blank {
  background: repeating-linear-gradient(
    -45deg,
    transparent 0 3px,
    color-mix(in srgb, var(--band-neutral) 28%, transparent) 3px 5px
  );
  border: 1px solid var(--color-border);
  color: var(--color-muted);
}

/* Kickoff TBC: a corner dot. Deliberately NOT a dashed border — that already
   means low model confidence, and this is SCHEDULE confidence, a different
   axis that has to stay legible alongside it rather than collide with it. */
.pgw-cell--tbc {
  position: relative;
}

.pgw-cell--tbc::after {
  content: '';
  position: absolute;
  top: 2px;
  right: 2px;
  width: 4px;
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--band-amber);
}

/* Postponed: lives OFF the strip, because it has no gameweek to sit in. */
.pgw-pending {
  display: inline-flex;
  align-items: center;
  height: 28px;
  padding: 0 var(--space-2);
  margin-left: var(--space-1);
  border-radius: var(--radius-sm);
  border: 1px dashed var(--color-border);
  background: color-mix(in srgb, var(--band-neutral) 6%, transparent);
  font-size: 0.625rem;
  font-weight: 700;
  color: var(--color-muted);
  white-space: nowrap;
}

/* ─── Matchup Analyser strip: hover-expand cells ────────────────────────────
   Scoped to .pgw-strip .pgw-cell only — the Ranker's own fixture strip
   (.ranker-fixtures .pgw-cell, further below) reuses the base .pgw-cell /
   .pgw-cell--<band> rules above but renders plain <span> cells with no
   .pgw-cell__extra child, so it's untouched by any of this.

   Collapsed state must stay pixel-identical to the plain .pgw-cell above
   (36×28, single centred score).

   Hover/focus grows ONLY the hovered cell — width and height together, by
   the same amount, so it stays square rather than turning into a tall
   rectangle. This is a deliberate exception to "don't animate layout
   properties": a transform-based scale would visually overlap neighbouring
   cells instead of pushing them, and this strip specifically needs the
   cells to its right to shift and make room (ordinary flex reflow — no JS),
   which only a real width/height change produces. Every other :hover rule
   in this file uses transform/opacity/box-shadow instead; this is the one
   place a genuine box-size transition is the actual requirement.

   The score's own vertical position must NOT move during that growth — only
   the box grows (downward, to make room for the reveal below) and the score
   recentres horizontally as the box widens. That's why padding-top is a
   fixed 6px in BOTH the base rule and the :hover rule below (never toggled,
   so it's never part of what's animating) rather than relying on
   justify-content: center, whose centred offset would itself grow as the
   box got taller and drag the score down with it. .pgw-cell__score is given
   an explicit pixel height/line-height for the same reason — a deterministic
   box to measure the padding against, not a font-metric guess. */
.pgw-strip .pgw-cell {
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
  width: 36px;
  height: 28px;
  padding-top: 6px;
  cursor: pointer;
  overflow: hidden;
  transition: width 200ms ease-out, height 200ms ease-out, box-shadow 200ms ease-out;
}

.pgw-strip .pgw-cell--static {
  cursor: default;
}

.pgw-strip .pgw-cell__score {
  height: 16px;
  line-height: 16px;
  flex: none;
}

.pgw-strip .pgw-cell__extra {
  display: grid;
  grid-template-rows: 0fr;
  width: 100%;
  transition: grid-template-rows 200ms ease-out;
}

/* This element's own padding-top/bottom is declared ONLY on the
   :hover/:focus-visible variant below: it sits inside .pgw-cell__extra's
   grid row, which is collapsed to 0fr at rest, and a stretched grid item's
   own padding still occupies box-model space against a nominally-zero
   track in some engines — declaring it here left residual space in the
   resting state before. Its job is the gap between the score and the
   opponent/venue block, plus clearance above the bottom border, once
   expanded — it has no effect on the score's own position above it. */
.pgw-strip .pgw-cell__extra-inner {
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 0;
  opacity: 0;
  /* Same 200ms ease-out as the box's width/height and the extra row's
     grid-template-rows below — everything settling at a different moment
     is what read as the animation "stopping twice". */
  transition: opacity 200ms ease-out, padding 200ms ease-out;
}

.pgw-strip .pgw-cell__opponent {
  font-size: 10px;
  line-height: 12px;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.pgw-strip .pgw-cell__venue {
  font-size: 10px;
  line-height: 12px;
  font-weight: 600;
  color: var(--color-muted);
}

/* 60×60, square. padding-top stays the fixed 6px from the base rule above
   (unlisted here — nothing to override) so the score never shifts; all of
   the growth appears as new space below it: 6 (top, unchanged) + 16 (score)
   + 4 (extra-inner top gap) + 12 (opponent) + 2 (gap) + 12 (venue) + 8
   (extra-inner bottom clearance) = 60, so the venue line clears the bottom
   border by the same 8px the score's top clears the top border by 6px. */
.pgw-strip .pgw-cell:hover,
.pgw-strip .pgw-cell:focus-visible {
  width: 60px;
  height: 60px;
  box-shadow: 0 0 0 1px currentColor inset;
}

.pgw-strip .pgw-cell--static:hover,
.pgw-strip .pgw-cell--static:focus-visible {
  box-shadow: none;
}

.pgw-strip .pgw-cell:hover .pgw-cell__extra,
.pgw-strip .pgw-cell:focus-visible .pgw-cell__extra {
  grid-template-rows: 1fr;
}

.pgw-strip .pgw-cell:hover .pgw-cell__extra-inner,
.pgw-strip .pgw-cell:focus-visible .pgw-cell__extra-inner {
  opacity: 1;
  padding-top: 4px;
  padding-bottom: 8px;
}

@media (prefers-reduced-motion: reduce) {
  .pgw-strip .pgw-cell,
  .pgw-strip .pgw-cell__extra,
  .pgw-strip .pgw-cell__extra-inner {
    transition: none;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   PLAYER RANKER — controls, table, badges, fixture strip
   BEM-lite per CONVENTIONS.md §5.1. All colours via var(--…).
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─── Two-column shell: filters (left) + table (right) ─────────────────────
   Same shape as .dash-shell / .planner-shell, but deliberately not their fixed
   300px sidebar: those panels hold squad cards that need the room, whereas the
   Ranker's sidebar holds five narrow controls and its table is the widest
   thing in the app. The track is content-sized instead — see below. */
.ranker-shell {
  display: grid;
  /* The sidebar takes the width of its widest control and no more. That
     control is the price <select>: browsers size a <select> to its widest
     option, and "£10.0m and below" is the longest string either dropdown
     can show — so the column stays correct on its own if PRICE_FILTER_MAX
     ever moves, with no measured constant to keep in sync. The old fixed
     300px was budgeted for four filter pills sitting side by side; those are
     stacked now (.ranker-filter-group), and every pixel the sidebar gives up
     goes to the table. */
  grid-template-columns: max-content 1fr;
  gap: var(--space-6);
  padding: 0 var(--space-4) var(--space-8);
  align-items: start;
  /* Break out of .app-main's max-width:1400px + margin:0 auto centering
     (layout.css) — that cap, minus its own padding and this shell's 300px
     sidebar, left well under 1028px for the table even before subtracting
     anything else. No table min-width (below) can fix a problem that's
     actually in the ancestor container. width:100vw + these negative
     margins is the standard technique for escaping a centered ancestor's
     max-width, letting the Ranker (and only the Ranker) use the full
     browser width when the table needs it. */
  width: 100vw;
  max-width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
}

@media (max-width: 900px) {
  .ranker-shell {
    grid-template-columns: 1fr;
  }
}

.ranker-filters {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.ranker-main {
  min-width: 0; /* allow the table wrapper to shrink/scroll inside the grid track */
}

/* ─── Filter button groups ─────────────────────────────────────────────────
   One button per row. Stacking is what lets .ranker-shell's max-content track
   collapse to the width of the dropdowns — a wrapping strip of four pills
   needed roughly twice that, so the table was paying for the sidebar's
   layout. Nothing is hidden or shrunk to get the width back. */
.ranker-filter-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/* Hairline above the second group so Position and Playtime read as two filter
   axes rather than one run of eight buttons. --color-border-subtle, not
   --color-border: this separates two things, it doesn't enclose either. */
.ranker-filter-group--divided {
  border-top: 1px solid var(--color-border-subtle);
  padding-top: var(--space-4);
}

.ranker-pos-btn,
.ranker-minsec-btn {
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-muted);
  font-size: var(--font-size-xs);
  font-weight: 700;
  font-family: var(--font-sans);
  letter-spacing: 0.06em;
  /* Square padding: these are full-width rows now, not pills sized to their
     text, so the horizontal half no longer does anything and 4px of vertical
     padding read as cramped once each button spanned the sidebar. */
  padding: var(--space-2);
  cursor: pointer;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
  text-transform: uppercase;
}

.ranker-pos-btn.is-active,
.ranker-minsec-btn.is-active {
  background: var(--color-accent-muted);
  border-color: var(--color-accent);
  color: var(--color-accent);
}

.ranker-pos-btn:hover:not(.is-active),
.ranker-minsec-btn:hover:not(.is-active) {
  border-color: var(--color-muted);
  color: var(--color-text);
}

/* Select wrapper: label stacked above the select */
.ranker-controls__select-wrap {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.ranker-controls__label,
.ranker-filters__heading {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  /* A label must never be the thing that sets the sidebar's width — that job
     belongs to the control it describes. A definite width makes its
     max-content contribution zero; the percentage min-width is ignored during
     that same intrinsic sizing pass but resolves to the full column at layout
     time. Net effect: renders full width, wraps if it has to, and "Avg Pts/GW
     source" can never out-measure the dropdown below it. */
  width: 0;
  min-width: 100%;
}

/* Section headings for the two button groups. Same look as the <label>s above
   the selects, since they do the same job one row up. */
.ranker-filters__heading {
  margin: 0 0 2px;
}

.ranker-select {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  font-size: var(--font-size-sm);
  font-family: var(--font-sans);
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
  transition: border-color 0.15s;
}

.ranker-select:hover { border-color: var(--color-muted); }
.ranker-select:focus {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  border-color: var(--color-accent);
}

/* ─── Avg Pts/GW source toggle — mirrors .planner-hit-toggle's pattern ───── */

.ranker-avgpts-wrap {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.ranker-avgpts-toggle {
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-muted);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  font-weight: 700;
  cursor: pointer;
  transition: background 0.1s, color 0.1s, border-color 0.1s;
}

/* Active = "Last Season" mode. Accent (not a band colour) — this is a data
   SOURCE switch, not a good/bad signal, so it shouldn't borrow band meaning. */
.ranker-avgpts-toggle.is-active {
  background: var(--color-accent-muted);
  border-color: var(--color-accent);
  color: var(--color-accent);
}

.ranker-avgpts-toggle:hover:not(.is-active) {
  border-color: var(--color-muted);
  color: var(--color-text);
}

/* ─── Loading state ─────────────────────────────────────────────────────── */

.ranker-loading {
  display: none;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-3) 0;
  color: var(--color-muted);
  font-size: var(--font-size-sm);
}

.ranker-loading.is-visible {
  display: flex;
}

/* Simple animated ellipsis via CSS so no JS is needed */
.ranker-loading__text::after {
  content: '';
  animation: ranker-dots 1.2s steps(4, end) infinite;
}

@keyframes ranker-dots {
  0%   { content: ''; }
  25%  { content: '.'; }
  50%  { content: '..'; }
  75%  { content: '...'; }
  100% { content: ''; }
}

/* ─── Table wrapper ─────────────────────────────────────────────────────── */

.ranker-table-wrap {
  overflow-x: auto;
  /* Subtle shadow on the right edge hints at horizontal scroll when needed */
  -webkit-overflow-scrolling: touch;
}

/* ─── Table ─────────────────────────────────────────────────────────────── */

.ranker-table {
  width: 100%;
  /* Floor beneath which nothing shrinks. Sized for the only horizon that now
     exists: the switcher was removed and the horizon is fixed at 6 GWs, so
     the fixtures column always carries 6 cells and the old narrower 1150px
     floor (plus its [data-horizon="GW6"] override) had no remaining purpose.
     Sized to clear a 1080p viewport without a horizontal scrollbar — the first
     pass at 1560px overflowed one by roughly a third of the last column.
     Below this, .ranker-table-wrap's overflow-x:auto takes over, same as any
     ordinary responsive table. */
  min-width: 1540px;
  border-collapse: collapse;
  font-size: var(--font-size-sm);
  table-layout: fixed;
}

/* Explicit per-column widths (percentages sum to 100) so proportions stay
   consistent as the table grows past its min-width above — table-layout:fixed
   makes the browser respect these regardless of cell content, wrapping or
   clipping instead of silently reshuffling column widths. Applied via the
   same data-sort/data-col attributes thead already renders, so no new markup
   is needed. Keyed on the <th> row specifically: fixed layout takes column
   widths from the FIRST row's cells. */
/* Budgeted against the widest real content each column carries, not split
   evenly: Player holds full names up to about "Gabriel Fernando de Jesus",
   Fixtures holds six pgw cells plus their gaps, and the numeric columns hold
   at most five glyphs ("£10.5m", "128", "5.3"), so they are deliberately
   narrow.

   Player went from web_name to the full name and needs roughly 60px more to
   carry it. That 60px comes from the table's floor, not from the other
   columns: min-width rose 1480 -> 1540px and the twelve other percentages
   were rescaled by 1480/1540, which leaves every one of them at the pixel
   width it had before. So the only column that changed size is the one that
   had to. The floor is affordable because the filters sidebar gave up about
   135px when it stopped being a fixed 300px track (.ranker-shell).

   If you retune these, the invariant to keep is that they still sum to 100 --
   table-layout:fixed will happily distribute a shortfall for you and quietly
   undo the budgeting. */
.ranker-table__th[data-sort="name"]              { width: 11.60%; }
.ranker-table__th[data-sort="team"]              { width: 5.60%; }
.ranker-table__th[data-col="pos"]                { width: 4.00%; }
.ranker-table__th[data-sort="price"]             { width: 5.60%; }
.ranker-table__th[data-sort="priceChange"]       { width: 6.00%; }
.ranker-table__th[data-sort="costPerPoint"]      { width: 6.80%; }
.ranker-table__th[data-sort="avgPointsPerGw"]    { width: 6.40%; }
.ranker-table__th[data-sort="totalPoints"]       { width: 4.80%; }
.ranker-table__th[data-sort="fplForm"]           { width: 4.80%; }
.ranker-table__th[data-sort="transfersInEvent"]  { width: 6.00%; }
.ranker-table__th[data-sort="transfersOutEvent"] { width: 6.00%; }
.ranker-table__th[data-sort="value"]             { width: 5.20%; }
.ranker-table__th[data-sort="nextFixtureScore"]  { width: 7.00%; }
.ranker-table__th[data-col="fixtures"]           { width: 13.00%; }
.ranker-table__th[data-sort="playtime"]          { width: 7.20%; }

/* Header cells */
.ranker-table__th {
  padding: var(--space-2) var(--space-3);
  text-align: left;
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--color-muted);
  border-bottom: 2px solid var(--color-border);
  white-space: nowrap;
  user-select: none;
}

.ranker-table__th--sortable {
  cursor: pointer;
}

.ranker-table__th--sortable:hover {
  color: var(--color-text);
}

.ranker-table__th.is-sorted {
  color: var(--color-accent);
}

/* Sort direction arrow */
.sort-icon {
  font-size: var(--font-size-xs);
  color: var(--color-accent);
  margin-left: 2px;
}

/* Body cells */
.ranker-table__td {
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--color-border-subtle);
  vertical-align: middle;
}

/* Column-specific styling; the widths themselves are on the <th> row above. */
.ranker-table__td--name {
  font-weight: 500;
  /* Clip rather than wrap. The column is budgeted for the full names that
     actually occur, but the tail of that distribution has no natural end
     ("Emerson Palmieri dos Santos"), and a name that wrapped would grow its
     row taller than every other and break the eye's run down the column.
     Whatever is clipped stays reachable: the cell carries the full string as
     its title, and the row's aria-label reads it out. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ranker-table__td--team          { white-space: nowrap; color: var(--color-muted); gap: var(--space-2); }

/* Club crest ahead of the abbreviation. Small on purpose — it is a glance
   aid, not a logo. alt="" because the three-letter code beside it already
   names the club, so announcing it twice only adds noise for screen readers.
   A crest that fails to load collapses to nothing and leaves the code intact,
   which is why the code was kept rather than replaced. */
.ranker-team-badge {
  width: 16px;
  height: 16px;
  object-fit: contain;
  vertical-align: text-bottom;
  margin-right: var(--space-1);
  flex-shrink: 0;
}

/* Early-season playtime figures still lean on the price-implied prior rather
   than the observed record. Same treatment as the Value column's estimated
   chip (.score-chip--estimated) — 1px, in the badge's own colour. Setting only
   border-style, as this first did, left border-width at its `medium` initial
   value and drew a 3px slab around the badge. */
.ranker-min-badge--estimated {
  border: 1px var(--estimated-border-style) currentColor;
}
.ranker-table__td--pos           { white-space: nowrap; }
.ranker-table__td--price         { font-variant-numeric: tabular-nums; white-space: nowrap; }
.ranker-table__td--value         { white-space: nowrap; }
.ranker-table__td--cost-per-point{ font-variant-numeric: tabular-nums; white-space: nowrap; }
.ranker-table__td--avg-pts       { font-variant-numeric: tabular-nums; white-space: nowrap; }
.ranker-table__td--total-pts     { font-variant-numeric: tabular-nums; white-space: nowrap; }
.ranker-table__td--form          { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Tabular figures so the thousands separators line up down the column and a
   6-figure count is instantly distinguishable from a 4-figure one. Left
   alignment, matching every other numeric column here and the left-aligned
   <th> above them — not right, which would detach the digits from the header. */
.ranker-table__td--tsfr-in,
.ranker-table__td--tsfr-out      { font-variant-numeric: tabular-nums; white-space: nowrap; }
.ranker-table__td--next-fixture  { white-space: nowrap; }

/* Value + Next Fixture chips drop the "~" prefix — the dashed border
   (.score-chip--estimated above) already carries the low-confidence signal,
   and a "~" repeated down every row of two columns reads as noise at table
   scale. Scoped to these two columns only; every other .score-chip--estimated
   usage on this page (Avg Pts/GW's .ranker-est-mark, THIS GW's fixture
   strip) is untouched. */
.ranker-table__td--value .score-chip--estimated::before,
.ranker-table__td--next-fixture .score-chip--estimated::before {
  content: none;
}
/* The fixture strip's cell count scales with the active horizon (1/3/6 GWs);
   .ranker-fixtures/.pgw-cell (below) handle fitting that into this column's
   fixed width by shrinking cells together on a 6-GW horizon, so this td
   itself needs no special overflow handling. */
.ranker-table__td--fixtures      { white-space: nowrap; }
.ranker-table__td--min           { white-space: nowrap; }

/* Estimated marker for Avg Pts/GW — mirrors .breakdown-row__est (dashboard.js)
   for the same "~" convention. Matchup page dropped its own "~" markers in
   favour of the dashed-border/muted-label treatment alone. */
.ranker-est-mark {
  font-size: var(--font-size-xs);
  color: var(--band-amber);
  font-weight: 700;
  cursor: default;
}

/* Inside a rank chip the "~" takes the chip's own colour. Its amber is a
   THIRD-party hue in there — it neither belongs to the tier nor reads as the
   marker it does against plain body text, and at gold it actively muddied the
   glow. Ranked cells are wider than the Value chip's digits, so these chips
   size to their content rather than sitting on the 32px floor. */
.score-chip .ranker-est-mark { color: inherit; }

.ranker-table__td--avg-pts .score-chip,
.ranker-table__td--cost-per-point .score-chip {
  min-width: 0;
}

/* Next-fixture rank tag, shown beside its score chip */
.ranker-rank-tag {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  margin-left: var(--space-1);
}

/* Player rows — interactive */
.ranker-table__row {
  cursor: pointer;
  transition: background 0.1s;
}

.ranker-table__row:hover {
  background: var(--color-surface-raised);
}

.ranker-table__row:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

/* Loading state: spinner-style muted row */
.ranker-table__row.is-loading {
  opacity: 0.5;
  cursor: wait;
  pointer-events: none;
}

/* Empty / status message spanning full width */
.ranker-table__empty {
  padding: var(--space-8) var(--space-3);
  text-align: center;
  color: var(--color-muted);
  font-size: var(--font-size-md);
}

/* ─── Position badge ─────────────────────────────────────────────────────── */

.ranker-pos-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  border-radius: var(--radius-sm);
  padding: 1px 5px;
  /* Colour set by position modifier below */
}

.ranker-pos-badge--gkp { background: rgba(240, 192, 64,  0.15); color: var(--pos-gkp); }
.ranker-pos-badge--def { background: rgba(88,  166, 255, 0.12); color: var(--pos-def); }
.ranker-pos-badge--mid { background: rgba(163, 113, 247, 0.12); color: var(--pos-mid); }
.ranker-pos-badge--fwd { background: rgba(255, 159, 91,  0.12); color: var(--pos-fwd); }

/* ─── Status badge (injured / doubtful) ──────────────────────────────────── */

.ranker-status-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-xs);
  font-weight: 800;
  color: var(--band-red);
  background: var(--band-red-bg);
  border-radius: 50%;
  width: 14px;
  height: 14px;
  margin-left: var(--space-1);
  cursor: default;
  vertical-align: middle;
  line-height: 1;
}

/* ─── Minutes security badge ─────────────────────────────────────────────── */

.ranker-min-badge {
  display: inline-block;
  font-size: var(--font-size-xs);
  font-weight: 700;
  border-radius: var(--radius-sm);
  padding: 1px var(--space-2);
  white-space: nowrap;
}

/* Band modifiers mirror the score band colour convention (CONVENTIONS.md §5.2) */
.ranker-min-badge--great   { background: var(--band-dark-green-bg);   color: var(--band-dark-green); }
.ranker-min-badge--good    { background: var(--band-green-bg);    color: var(--band-green); }
.ranker-min-badge--neutral { background: var(--band-neutral-bg); color: var(--band-neutral); }
.ranker-min-badge--tough   { background: var(--band-amber-bg);   color: var(--band-amber); }

/* ─── Fixture strip inside table cell ────────────────────────────────────── */

/* Reuses .pgw-cell / .pgw-cell--<band> from the matchup card; the container
   here is inline so it sits naturally inside a <td>. Cells never shrink below
   their natural size (see .pgw-cell below) — legibility over fitting a fixed
   box, per explicit product decision. The table's min-width (above) is sized
   with this column's worst case (6 GWs) already accounted for, and the
   surrounding .ranker-table-wrap scrolls the whole table on narrower windows
   rather than compressing this strip illegible. */
.ranker-fixtures {
  display: inline-flex;
  flex-wrap: nowrap;
  gap: 2px;
  /* Was `center`. Children are now .pgw-slot column stacks of differing height —
     the .pgw-pending pill carries no gameweek label — and centring them would
     float the pill to the middle of the row instead of level with the cells. */
  align-items: flex-start;
}

/* Compact sizing for table context — narrower than the matchup card strip.
   Fixed width, no flex-shrink: an earlier version let these shrink to fit a
   6-GW horizon, which squeezed the team codes down to illegible/overlapping
   text. Full size, always. */
.ranker-fixtures .pgw-cell {
  width: 28px;
  height: 20px;
  font-size: 0.65rem;
}

.ranker-no-fixtures {
  color: var(--color-muted);
  font-size: var(--font-size-xs);
}

/* ═══════════════════════════════════════════════════════════════════════════
   DASHBOARD — GW Decision Dashboard (Phase 2C)
   Squad builder (left) + decisions panel (right).
   BEM-lite per CONVENTIONS.md §5.1. Band colours reuse score-chip modifiers.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─── Controls bar ───────────────────────────────────────────────────────── */

.dash-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-4) var(--space-4);
  border-bottom: 1px solid var(--color-border-subtle);
  margin-bottom: var(--space-4);
}

.dash-search-wrap {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.dash-search-label {
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-muted);
}

.dash-search-combo {
  position: relative;
}

.dash-search-input {
  width: 260px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
}

.dash-search-input:focus {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
  border-color: var(--color-accent);
}

.dash-search-input::placeholder {
  color: var(--color-muted);
}

/* ─── Search results dropdown ────────────────────────────────────────────── */

.dash-search-results {
  display: none; /* shown by JS adding .is-open */
  position: absolute;
  top: calc(100% + var(--space-1));
  left: 0;
  min-width: 100%;
  width: max-content;
  max-width: 380px;
  max-height: 280px;
  overflow-y: auto;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  z-index: 200;
  list-style: none;
  padding: var(--space-1) 0;
}

.dash-search-results.is-open {
  display: block;
}

.dash-search-results__item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  font-size: var(--font-size-sm);
  user-select: none;
  transition: background 0.08s;
}

.dash-search-results__item:hover {
  background: var(--color-surface-raised);
}

.dash-search-results__item--disabled {
  opacity: 0.35;
  cursor: not-allowed;
  pointer-events: none;
}

.dash-search-results__name {
  flex: 1;
  font-weight: 500;
}

.dash-search-results__meta {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  white-space: nowrap;
}

.dash-search-results__empty {
  padding: var(--space-3) var(--space-4);
  color: var(--color-muted);
  font-size: var(--font-size-sm);
  font-style: italic;
  list-style: none;
}

/* ─── Position filter pills ──────────────────────────────────────────────── */

.dash-pos-filter {
  display: flex;
  gap: var(--space-1);
}

.dash-pos-btn {
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-muted);
  font-family: var(--font-sans);
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  cursor: pointer;
  transition: background 0.1s, color 0.1s, border-color 0.1s;
}

.dash-pos-btn.is-active {
  background: var(--color-accent-muted);
  border-color: var(--color-accent);
  color: var(--color-accent);
}

/* ─── Squad tally ────────────────────────────────────────────────────────── */

.dash-squad-tally {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  white-space: nowrap;
  margin: 0;
  align-self: flex-end;
}

/* ─── Main two-column shell ──────────────────────────────────────────────── */

.dash-shell {
  display: grid;
  grid-template-columns: 300px 1fr;
  gap: var(--space-6);
  padding: 0 var(--space-4) var(--space-8);
  align-items: start;
}

@media (max-width: 900px) {
  .dash-shell {
    grid-template-columns: 1fr;
  }
}

/* ─── Squad panel (left column) ──────────────────────────────────────────── */

.dash-squad-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.dash-squad-group__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-muted);
  padding: var(--space-2) 0 var(--space-1);
  border-bottom: 1px solid var(--color-border-subtle);
  margin-bottom: var(--space-1);
}

.dash-squad-slot {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-sm);
  min-height: 34px;
}

.dash-squad-slot--empty {
  border: 1px dashed var(--color-border);
  color: var(--color-muted);
  font-style: italic;
  font-size: var(--font-size-xs);
}

.dash-squad-slot--filled {
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border-subtle);
}

.dash-squad-slot__name {
  flex: 1;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

.dash-squad-slot__team {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  white-space: nowrap;
  flex-shrink: 0;
}

.dash-squad-slot__price {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  white-space: nowrap;
  flex-shrink: 0;
}

.dash-squad-slot__remove {
  flex-shrink: 0;
  background: none;
  border: none;
  color: var(--color-muted);
  cursor: pointer;
  font-size: var(--font-size-md);
  line-height: 1;
  padding: 0 var(--space-1);
  border-radius: var(--radius-sm);
  transition: color 0.1s, background 0.1s;
}

.dash-squad-slot__remove:hover {
  color: var(--band-red);
  background: var(--band-red-bg);
}

/* ─── Decisions panel (right column) ─────────────────────────────────────── */

.dash-decisions {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.dash-decisions__hint {
  color: var(--color-muted);
  text-align: center;
  padding: var(--space-8) var(--space-4);
  font-size: var(--font-size-sm);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-md);
  margin: 0;
}

/* ─── Captain recommendation block ──────────────────────────────────────── */

.dash-captain {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-accent);
  border-radius: var(--radius-md);
  padding: var(--space-4);
}

.dash-captain__header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

.dash-captain__badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--color-accent);
  color: #000;
  font-weight: 800;
  font-size: var(--font-size-sm);
  flex-shrink: 0;
}

.dash-captain__title {
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-muted);
}

.dash-captain__name {
  font-size: var(--font-size-md);
  font-weight: 700;
  line-height: 1.3;
}

.dash-captain__team {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  font-weight: 400;
  margin-left: var(--space-1);
}

.dash-captain__price {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  font-weight: 400;
  margin-left: var(--space-1);
}

.dash-captain__predicted {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  font-weight: 400;
  margin-left: var(--space-1);
}

/* ─── Starting XI and Bench lists ────────────────────────────────────────── */

.dash-xi,
.dash-bench {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.dash-xi__header,
.dash-bench__header {
  display: flex;
  justify-content: space-between;
  padding: var(--space-2) var(--space-4);
  border-bottom: 1px solid var(--color-border);
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-muted);
  background: var(--color-surface-raised);
}

/* ─── Player row (shared by XI and bench) ────────────────────────────────── */

.dash-player-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-bottom: 1px solid var(--color-border-subtle);
  font-size: var(--font-size-sm);
}

.dash-player-row:last-child {
  border-bottom: none;
}

.dash-player-row--captain {
  background: var(--color-accent-muted);
}

.dash-player-row__pos-badge {
  flex-shrink: 0;
}

.dash-player-row__name {
  flex: 1;
  font-weight: 500;
  min-width: 0;
}

.dash-player-row__captain-mark {
  font-weight: 800;
  color: var(--color-accent);
  font-size: var(--font-size-xs);
}

.dash-player-row__team {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  flex-shrink: 0;
}

.dash-player-row__price {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  flex-shrink: 0;
}

/* display:contents so .dash-breakdown and the live-pts span become direct
   flex children of .dash-player-row itself — needed so .dash-breakdown[open]
   (flex-basis: 100%) breaks onto its own full-width line within the ROW,
   not just within this wrapper's own intrinsic width. */
.dash-player-row__score {
  display: contents;
}

/* ─── Score breakdown disclosure (Phase 6) ───────────────────────────────── */
/* The score chip is the <summary> — clicking it reveals the form/fixture/
   counter breakdown. Row markup inside .dash-breakdown__panel reuses
   matchup.js's .breakdown-row* classes (bar + value + weight%) for visual
   consistency; only this toggle chrome is new. Squad slots, XI/bench rows,
   and the captain header all use flex-wrap so the panel can drop onto its
   own full-width line on open — same technique as .dash-player-row__flags. */
.dash-breakdown {
  flex-shrink: 0;
  /* No-op wherever a flex:1 sibling (e.g. the name column) already claims the
     remaining row space; in the captain header (no such sibling) this is what
     pushes the chip to the row's right edge, replacing the margin-left:auto
     that used to live on the now display:contents .dash-captain__score-wrap. */
  margin-left: auto;
}

.dash-breakdown[open] {
  flex-basis: 100%;
}

.dash-breakdown__summary {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
}

.dash-breakdown__panel {
  width: 100%;
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px dashed var(--color-border);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.dash-breakdown__panel-title {
  font-size: var(--font-size-xs);
  font-weight: 700;
  color: var(--color-muted);
}

/* Risk flags: full row below name/score on wrap */
.dash-player-row__flags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  width: 100%;
  /* Indent to align with the player name (past the pos badge). */
  padding-left: calc(36px + var(--space-2));
}

/* ─── GW state badge (Phase 3C-5) ───────────────────────────────────────── */

/* Container: sits at the top of the decisions panel at all times. */
.dash-gw-state {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

/* Base badge: pill shape, tight uppercase label. */
.gw-state-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 3px var(--space-3);
  border-radius: 999px;
  font-size: var(--font-size-xs);
  font-weight: 800;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  white-space: nowrap;
  line-height: 1.5;
}

/* GW LIVE — green with a gentle pulse to signal active data. */
.gw-state-badge--live {
  background: rgba(63, 185, 80, 0.13);
  color: var(--band-dark-green);
  border: 1px solid var(--band-dark-green);
  animation: gw-badge-pulse 1.8s ease-in-out infinite;
}

/* PRE-DEADLINE / OFF SEASON — quiet muted badge, no animation. */
.gw-state-badge--pre,
.gw-state-badge--done {
  background: var(--color-surface-raised);
  color: var(--color-muted);
  border: 1px solid var(--color-border);
}

/* Subtle opacity pulse on the live badge only. */
@keyframes gw-badge-pulse {
  0%,  100% { opacity: 1;    }
  50%        { opacity: 0.55; }
}

/* "· data may be delayed" note shown next to the badge when data is stale. */
.gw-state-badge__stale {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  font-style: italic;
}

/* ─── Captain score wrap — projected chip + live pts side by side ─────────── */

/* display:contents so .dash-breakdown and the live-pts span become direct
   flex children of .dash-captain__header (flex-wrap: wrap row) — needed so
   .dash-breakdown[open]'s flex-basis:100% breaks onto a full-width line
   within the HEADER, not just within this wrapper's own column context
   (a flex-basis in a column-direction container affects height, not width). */
.dash-captain__score-wrap {
  display: contents;
}

/* ─── Live points display (Phase 3C-5) ───────────────────────────────────── */

/* Base: small accent-coloured text shown alongside the projected score chip. */
.dash-live-pts {
  display: inline-block;
  font-size: var(--font-size-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--band-green);
  white-space: nowrap;
  line-height: 1.4;
  /* Always follows a .dash-breakdown chip; both former wrappers that used to
     supply this gap (.dash-captain__score-wrap, .dash-player-row__score) are
     now display:contents so the chip and this span can wrap independently. */
  margin-left: var(--space-1);
}

/* Stale: last known data but fetch failed — muted and italic. */
.dash-live-pts--stale {
  color: var(--color-muted);
  font-style: italic;
  opacity: 0.8;
}

/* Captain: accent colour to highlight the doubled-points pick. */
.dash-live-pts--captain {
  color: var(--color-accent);
}

/* Stale captain: override captain colour with muted, keep italic. */
.dash-live-pts--captain.dash-live-pts--stale {
  color: var(--color-muted);
}

/* ─── Risk flag chips ────────────────────────────────────────────────────── */

.dash-flag {
  display: inline-block;
  padding: 1px var(--space-2);
  border-radius: 999px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  white-space: nowrap;
  line-height: 1.6;
}

/* Rotation Risk — amber/tough band */
.dash-flag--rotation {
  background: var(--band-amber-bg);
  color: var(--band-amber);
}

/* Tough Fixture — red/brutal band */
.dash-flag--fixture {
  background: var(--band-red-bg);
  color: var(--band-red);
}

/* Low Confidence — uses --color-estimated so all estimated indicators share one token */
.dash-flag--confidence {
  background: rgba(122, 131, 144, 0.12);
  color: var(--color-estimated);
}

/* Availability Doubt — red/brutal band */
.dash-flag--availability {
  background: var(--band-red-bg);
  color: var(--band-red);
}

/* ═══════════════════════════════════════════════════════════════════════════
   TRANSFER PLANNER — Phase 2D
   Squad builder (left) + transfer recommendations (right).
   BEM-lite per CONVENTIONS.md §5.1. Band colours reuse existing tokens.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─── Controls bar ───────────────────────────────────────────────────────── */

.planner-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-4);
  padding: var(--space-4);
  border-bottom: 1px solid var(--color-border-subtle);
  margin-bottom: var(--space-4);
}

/* ─── Budget input ───────────────────────────────────────────────────────── */

.planner-budget-wrap {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.planner-label {
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-muted);
}

.planner-budget-input {
  width: 90px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  font-variant-numeric: tabular-nums;
  transition: border-color 0.15s;
}

.planner-budget-input:focus {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
  border-color: var(--color-accent);
}

/* ─── Free transfers toggle ──────────────────────────────────────────────── */

.planner-ft-wrap {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.planner-ft-btns {
  display: flex;
  gap: var(--space-1);
}

.planner-ft-btn {
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-muted);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  font-weight: 700;
  cursor: pointer;
  transition: background 0.1s, color 0.1s, border-color 0.1s;
}

.planner-ft-btn.is-active {
  background: var(--color-accent-muted);
  border-color: var(--color-accent);
  color: var(--color-accent);
}

.planner-ft-btn:hover:not(.is-active) {
  border-color: var(--color-muted);
  color: var(--color-text);
}

/* ─── Hit toggle ─────────────────────────────────────────────────────────── */

.planner-hit-wrap {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.planner-hit-toggle {
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-muted);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  font-weight: 700;
  cursor: pointer;
  transition: background 0.1s, color 0.1s, border-color 0.1s;
}

.planner-hit-toggle.is-active {
  background: var(--band-amber-bg);
  border-color: var(--band-amber);
  color: var(--band-amber);
}

.planner-hit-toggle:hover:not(.is-active) {
  border-color: var(--color-muted);
  color: var(--color-text);
}

/* ─── Position filter — reuses .dash-pos-btn; the planner variant shares the same
       visual style but scoped to .planner-pos-btn for JS targeting ─────────── */

.planner-pos-btn {
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-muted);
  font-family: var(--font-sans);
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  cursor: pointer;
  transition: background 0.1s, color 0.1s, border-color 0.1s;
}

.planner-pos-btn.is-active {
  background: var(--color-accent-muted);
  border-color: var(--color-accent);
  color: var(--color-accent);
}

/* ─── Main two-column shell ──────────────────────────────────────────────── */

.planner-shell {
  display: grid;
  grid-template-columns: 300px 1fr;
  gap: var(--space-6);
  padding: 0 var(--space-4) var(--space-8);
  align-items: start;
}

@media (max-width: 900px) {
  .planner-shell {
    grid-template-columns: 1fr;
  }
}

/* ─── Right column wrapper (chips + recommendations stack) ───────────────── */

.planner-right-col {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  min-width: 0;
}

/* ─── Chip planner panel (Phase 4-3) ─────────────────────────────────────── */

.planner-chips {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.planner-chips__hd {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-border-subtle);
}

.planner-chips__title {
  font-size: var(--font-size-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-text);
}

.planner-chips__meta {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  margin-left: auto;
}

.planner-chips__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-3);
}

.planner-chip-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-accent);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.planner-chip-card--used {
  opacity: 0.45;
  border-left-color: var(--color-border);
}

.planner-chip-card__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.planner-chip-card__name {
  font-weight: 700;
  font-size: var(--font-size-sm);
  color: var(--color-text);
}

.planner-chip-card__rec {
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-accent);
  margin-left: auto;
}

.planner-chip-card--used .planner-chip-card__rec {
  color: var(--color-muted);
  text-decoration: line-through;
}

.planner-chip-card__why {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  line-height: 1.4;
  margin: 0;
}

.planner-chip-card__foot {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-1);
}

.planner-chip-card__used-toggle {
  font-size: var(--font-size-xs);
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 2px var(--space-2);
  color: var(--color-muted);
  cursor: pointer;
}

.planner-chip-card__used-toggle[aria-pressed="true"] {
  background: var(--color-surface-raised);
  color: var(--color-text);
  border-color: var(--color-border-subtle);
}

.planner-chip-card__hint {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  font-style: italic;
}

/* ─── Recommendations panel (right column) ───────────────────────────────── */

.planner-recommendations {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}

/* Empty / loading state */
.planner-hint {
  color: var(--color-muted);
  text-align: center;
  padding: var(--space-8) var(--space-4);
  font-size: var(--font-size-sm);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-md);
  margin: 0;
}

/* ─── Section headers ────────────────────────────────────────────────────── */

.planner-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.planner-section__hd {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-border-subtle);
}

.planner-section__title {
  font-size: var(--font-size-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-text);
}

.planner-section__meta {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  margin-left: auto;
}

.planner-section-label {
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-muted);
}

/* ─── Transfer recommendation card ──────────────────────────────────────── */

.planner-transfer-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.planner-transfer-card--double {
  border-left: 3px solid var(--color-accent);
}

.planner-transfer-card__header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface-raised);
  border-bottom: 1px solid var(--color-border-subtle);
  flex-wrap: wrap;
}

.planner-transfer-card__header--double {
  background: var(--color-accent-muted);
}

/* Out→In player row */
.planner-transfer-card__body {
  display: flex;
  align-items: stretch;
  padding: var(--space-3) var(--space-4);
  gap: var(--space-3);
  flex-wrap: wrap;
}

/* Arrow between out and in */
.planner-transfer-card__arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-lg);
  color: var(--color-muted);
  flex-shrink: 0;
  align-self: center;
}

/* Double-swap container */
.planner-transfer-card__double-swaps {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.planner-transfer-card__swap-label {
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--color-muted);
  padding: var(--space-2) var(--space-4) 0;
}

.planner-transfer-card__swap-meta {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-1) var(--space-4) var(--space-2);
}

.planner-transfer-card__divider {
  border: none;
  border-top: 1px solid var(--color-border-subtle);
  margin: var(--space-2) 0 0;
}

/* ─── Delta indicator ────────────────────────────────────────────────────── */

.planner-delta {
  font-size: var(--font-size-lg);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.planner-delta--gain { color: var(--band-green); }
.planner-delta--loss { color: var(--band-red); }

/* Estimated delta: underlying projections have low confidence. */
.planner-delta--estimated {
  opacity: 0.7;
}
.planner-delta--estimated::before {
  content: '~';
  font-size: 0.75em;
  opacity: 0.7;
  margin-right: 1px;
}

.planner-delta--sm {
  font-size: var(--font-size-sm);
  font-weight: 700;
}

/* ─── Cost difference ────────────────────────────────────────────────────── */

.planner-cost-diff {
  font-size: var(--font-size-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--color-muted);
}

.planner-cost-diff--sm {
  font-size: var(--font-size-xs);
}

/* ─── Budget remaining ───────────────────────────────────────────────────── */

.planner-budget-remaining {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  margin-left: auto;
  font-variant-numeric: tabular-nums;
}

/* ─── Hit warning badge ──────────────────────────────────────────────────── */

.planner-hit-badge {
  display: inline-block;
  padding: 2px var(--space-2);
  border-radius: 999px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  white-space: nowrap;
  line-height: 1.6;
  background: var(--band-amber-bg);
  color: var(--band-amber);
  border: 1px solid var(--band-amber);
}

/* ─── Player projection block ────────────────────────────────────────────── */

.planner-player {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  flex: 1;
  min-width: 0;
}

.planner-player__dir {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 20px;
  border-radius: var(--radius-sm);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.05em;
  flex-shrink: 0;
  margin-top: 2px;
}

.planner-player__dir--out {
  background: var(--band-red-bg);
  color: var(--band-red);
}

.planner-player__dir--in {
  background: var(--band-green-bg);
  color: var(--band-green);
}

.planner-player__info {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  flex: 1;
  min-width: 0;
}

.planner-player__name {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-1);
  font-size: var(--font-size-sm);
  font-weight: 600;
  line-height: 1.3;
}

.planner-player__team-inline {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  font-weight: 400;
}

.planner-player__price {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}

/* ─── Mini projection breakdown bars ────────────────────────────────────── */

.planner-breakdown {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.planner-breakdown__bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 10px;
}

.planner-breakdown__lbl {
  width: 40px;
  color: var(--color-muted);
  font-weight: 600;
  flex-shrink: 0;
}

.planner-breakdown__track {
  flex: 1;
  height: 4px;
  background: var(--color-border);
  border-radius: 999px;
  overflow: hidden;
  min-width: 40px;
}

.planner-breakdown__fill {
  height: 100%;
  background: var(--color-accent);
  border-radius: 999px;
  transition: width 0.3s ease;
}

.planner-breakdown__val {
  width: 20px;
  text-align: right;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
  flex-shrink: 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   CALIBRATION VIEW  (#calibration — developer tool)
   ═══════════════════════════════════════════════════════════════════════════ */

.calib-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--color-border-subtle);
}

.calib-title {
  font-size: var(--font-size-lg);
  font-weight: 600;
  color: var(--color-text);
}

.calib-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.calib-accuracy {
  font-size: var(--font-size-sm);
  color: var(--color-muted);
  font-variant-numeric: tabular-nums;
}

.calib-btn {
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface-raised);
  color: var(--color-text);
  font-size: var(--font-size-sm);
  cursor: pointer;
  transition: background 0.15s;
}

.calib-btn:hover {
  background: var(--color-border);
}

.calib-btn--clear {
  color: var(--band-red);
  border-color: var(--band-red);
}

.calib-empty {
  color: var(--color-muted);
  font-size: var(--font-size-sm);
  padding: var(--space-8) 0;
}

.calib-table-wrap {
  overflow-x: auto;
}

.calib-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-sm);
  font-variant-numeric: tabular-nums;
}

.calib-table th,
.calib-table td {
  padding: var(--space-2) var(--space-3);
  text-align: left;
  border-bottom: 1px solid var(--color-border-subtle);
  white-space: nowrap;
}

.calib-table th {
  color: var(--color-muted);
  font-weight: 600;
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  background: var(--color-surface);
  position: sticky;
  top: 0;
}

.calib-table tbody tr:hover {
  background: var(--color-surface-raised);
}

.calib-fixture {
  white-space: nowrap;
}

.calib-call--correct {
  color: var(--band-green);
  font-weight: 700;
}

.calib-call--wrong {
  color: var(--band-red);
  font-weight: 700;
}

/* ═══════════════════════════════════════════════════════════════════════════
   SQUAD IMPORT  (Phase 4-1)
   ═══════════════════════════════════════════════════════════════════════════ */

/* Wrap is `position: relative` so the panel can be positioned absolutely below
   the button without disrupting the flex controls bar layout. */
.squad-import-wrap {
  position: relative;
  display: flex;
  flex-direction: column;
  align-self: flex-end;
}

.squad-import-btn {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-muted);
  font-size: var(--font-size-sm);
  font-family: var(--font-sans);
  font-weight: 700;
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  white-space: nowrap;
  transition: border-color 0.1s, color 0.1s, background 0.1s;
}

.squad-import-btn:hover {
  border-color: var(--color-muted);
  color: var(--color-text);
}

.squad-import-btn.is-open {
  background: var(--color-accent-muted);
  border-color: var(--color-accent);
  color: var(--color-accent);
}

.squad-import-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Panel drops down from the wrap as an overlay — does not affect controls bar height. */
.squad-import-panel {
  position: absolute;
  top: calc(100% + var(--space-1));
  right: 0;
  z-index: 50;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  min-width: 280px;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}

/* Row inside the panel: label + input + action buttons side by side */
.squad-import-panel__row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.squad-import-label {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  white-space: nowrap;
}

.squad-import-input {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  font-size: var(--font-size-sm);
  font-family: var(--font-sans);
  padding: var(--space-2) var(--space-3);
  width: 130px;
  transition: border-color 0.15s;
  /* Hide the number-input spinners — they add no value here */
  -moz-appearance: textfield;
}

.squad-import-input::-webkit-inner-spin-button,
.squad-import-input::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.squad-import-input:focus {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  border-color: var(--color-accent);
}

.squad-import-go {
  background: var(--color-accent);
  border: none;
  border-radius: var(--radius-sm);
  color: #fff;
  font-size: var(--font-size-sm);
  font-family: var(--font-sans);
  font-weight: 700;
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  transition: opacity 0.15s;
}

.squad-import-go:hover  { opacity: 0.85; }
.squad-import-go:active { opacity: 0.7;  }

.squad-import-go:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.squad-import-cancel {
  background: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-muted);
  font-size: var(--font-size-sm);
  font-family: var(--font-sans);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}

.squad-import-cancel:hover {
  border-color: var(--color-muted);
  color: var(--color-text);
}

.squad-import-cancel:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Status line — full-width row below the controls */
.squad-import-status {
  width: 100%;
  font-size: var(--font-size-xs);
  min-height: 1.2em;
}

.squad-import-status--loading { color: var(--color-muted); font-style: italic; }
.squad-import-status--success { color: var(--band-dark-green);  font-weight: 600; }
.squad-import-status--error   { color: var(--band-red); font-weight: 600; }
.squad-import-status--idle    { color: var(--color-muted); }

/* Team info line: "Team Name · Manager · Overall rank: N" */
.squad-import-info {
  width: 100%;
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  min-height: 1.2em;
}

/* ─── Team ID help disclosure ────────────────────────────────────── */
/* A "?" beside the Import button explaining where the FPL Team ID lives.
   Same <details>/<summary> pattern and round-affordance styling as the
   matchup page's .counter-pairing-info__btn — no new interaction model,
   and no JS wiring. */

/* The button and the "?" sit on one row; the import panel still drops out of
   .squad-import-wrap below them. */
.squad-import-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Relative so the help panel can overlay rather than push the controls bar. */
.squad-import-help {
  position: relative;
  flex-shrink: 0;
}

.squad-import-help__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background: var(--color-surface-raised);
  color: var(--color-muted);
  font-size: var(--font-size-xs);
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  list-style: none;
  transition: color 0.1s, border-color 0.1s;
}

.squad-import-help__btn::-webkit-details-marker {
  display: none;
}

.squad-import-help__btn:hover,
.squad-import-help[open] .squad-import-help__btn {
  color: var(--color-accent);
  border-color: var(--color-accent);
}

.squad-import-help__btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Overlay panel, matched to .squad-import-panel so both dropdowns read alike. */
.squad-import-help__panel {
  position: absolute;
  top: calc(100% + var(--space-1));
  right: 0;
  z-index: 50;
  width: 320px;
  max-width: calc(100vw - var(--space-4) * 2);
  padding: var(--space-3);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  font-size: var(--font-size-xs);
  color: var(--color-text);
  /* The steps are prose, not the controls bar's nowrap chrome. */
  white-space: normal;
  text-align: left;
  font-weight: 400;
}

.squad-import-help__title {
  margin: 0 0 var(--space-2);
  font-size: var(--font-size-sm);
  font-weight: 700;
  color: var(--color-text);
}

.squad-import-help__steps {
  margin: 0;
  padding-left: 1.2em;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  line-height: 1.45;
  color: var(--color-muted);
}

.squad-import-help__steps strong {
  color: var(--color-text);
  font-weight: 600;
}

.squad-import-help__panel kbd {
  display: inline-block;
  padding: 0 4px;
  border: 1px solid var(--color-border);
  border-radius: 3px;
  background: var(--color-surface);
  color: var(--color-text);
  font-family: var(--font-mono, monospace);
  font-size: 0.9em;
  line-height: 1.4;
}

.squad-import-help__panel code {
  font-family: var(--font-mono, monospace);
  font-size: 0.95em;
  color: var(--color-accent);
  word-break: break-all;
}

.squad-import-help__aside {
  margin: var(--space-3) 0 0;
  padding-top: var(--space-2);
  border-top: 1px solid var(--color-border-subtle);
  line-height: 1.45;
  color: var(--color-muted);
}

/* ═══════════════════════════════════════════════════════════════════════════
   PHASE 4-4  PRICE CHANGE INDICATORS
   Ranker column + Transfer Planner warnings.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Ranker: price change column cell ──────────────────────────────────────── */

/* NOTE: the Ranker's column now shows the season-to-date change actually
   banked (engine/prices.js → calcSeasonPriceChange), not the transfer-flow
   forecast it originally carried — that prediction column was removed from the
   Ranker and survives only in the Transfer Planner (.planner-price-warning--*
   below). The rise/fall/stable colour vocabulary is shared deliberately: green
   up, red down, muted flat means the same thing in both places. */
.ranker-table__td--price-change {
  white-space: nowrap;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.ranker-price-change {
  display: inline-block;
  font-size: var(--font-size-xs);
  font-weight: 600;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  cursor: default;
}

.ranker-price-change--rise {
  color: var(--band-dark-green);
  background: color-mix(in srgb, var(--band-dark-green) 12%, transparent);
}

.ranker-price-change--fall {
  color: var(--band-red);
  background: color-mix(in srgb, var(--band-red) 12%, transparent);
}

.ranker-price-change--stable {
  color: var(--color-muted);
}

/* ── Transfer Planner: price change footer on transfer cards ───────────────── */

.planner-transfer-card__price-footer {
  padding: var(--space-2) var(--space-4);
  border-top: 1px solid var(--color-border-subtle);
}

.planner-price-warning {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-xs);
  font-weight: 600;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  cursor: default;
}

.planner-price-warning--buy-now {
  color: var(--band-dark-green);
  background: color-mix(in srgb, var(--band-dark-green) 15%, transparent);
  border: 1px solid color-mix(in srgb, var(--band-dark-green) 35%, transparent);
}

.planner-price-warning--rise {
  color: var(--band-green);
  background: color-mix(in srgb, var(--band-green) 12%, transparent);
}

.planner-price-warning--fall {
  color: var(--band-red);
  background: color-mix(in srgb, var(--band-red) 12%, transparent);
}

/* ═══════════════════════════════════════════════════════════════════════════
   MOBILE COMPONENTS  ≤ 768px — Phase 4-5
   Purely additive overrides. Desktop rules are completely untouched.
   No new hardcoded values — all tokens from base.css.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 768px) {

  /* ─── Matchup Analyser ──────────────────────────────────────────────────── */

  /* The matchup grid already collapses to single-column at 800px.
     Below 768px we tighten both breakdown grids so the "Counter-Matchup"
     label (9.5rem on desktop) doesn't overflow a 375px screen. The two are
     tightened in step: .breakdown-row is dashboard.js's standalone layout,
     .breakdown-rows is matchup.js's shared one with the maturity column. */
  .breakdown-row {
    grid-template-columns: 7rem 1fr 2rem 2rem auto;
  }

  .breakdown-rows {
    grid-template-columns: minmax(3.5rem, 7rem) auto minmax(0, 1fr) 2rem 2rem auto;
  }

  .breakdown-row__label {
    font-size: var(--font-size-xs);
  }

  /* Counter-pairing: narrower label column on small screens */
  .counter-pairing {
    grid-template-columns: 6rem auto 1fr auto;
  }

  .counter-pairing__label {
    font-size: var(--font-size-xs);
  }

  /* Tighten card padding */
  .matchup-card__header,
  .matchup-card__score-row,
  .matchup-card__breakdown,
  .matchup-card__counter,
  .matchup-card__horizon {
    padding-left: var(--space-3);
    padding-right: var(--space-3);
  }

  /* Team name: step down one level so it fits beside the venue badge */
  .matchup-card__team {
    font-size: var(--font-size-md);
  }

  /* Score pill value: one step smaller so the pill fits in the card header */
  .score-pill__value {
    font-size: var(--font-size-xl);
  }

  /* GW/team navigators: stacked above/below the cards at this width
     (.matchup-shell collapses to 1fr at 900px) — drop the sticky positioning
     so they scroll away with the page instead of pinning on a small screen. */
  .gw-nav,
  .team-nav {
    position: static;
  }

  /* With the panel no longer pinned, a viewport-height cap would trap the
     season list in a small inner scroller in the middle of a page that already
     scrolls — two scrollbars for one list. Let it run at full length here and
     scroll with the page, which is the natural gesture on a narrow screen.

     height/min-height undo the fill-the-row pair from the same rule: stacked,
     this panel IS its own row, so a 0 contribution would size that row to 0
     and 100% of 0 would collapse the panel to nothing. */
  .team-nav {
    height: auto;
    min-height: 0;
    max-height: none;
  }

  .gw-nav__list--scroll {
    overflow-y: visible;
  }

  /* ─── Player Ranker — table → card list ──────────────────────────────────
     Each <tr> becomes a flex card. Specific <td> columns are hidden or
     repositioned to show only the key stats: pos · name · price · value ·
     fixture strip. The <thead> is visually hidden but kept for screen readers.
     ─────────────────────────────────────────────────────────────────────── */

  /* Unwrap the table from table layout so rows can be flex cards */
  .ranker-table,
  .ranker-table tbody {
    display: block;
    width: 100%;
  }

  /* Visually hide the column headers — screen readers still read them */
  .ranker-table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  /* Each row becomes a card */
  .ranker-table__row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--space-2) var(--space-3);
    margin-bottom: var(--space-2);
    min-height: 44px;
    /* Reset the desktop hover background so it doesn't fight the border */
    transition: border-color 0.1s;
  }

  .ranker-table__row:hover {
    background: var(--color-surface);
    border-color: var(--color-muted);
  }

  /* All <td> reset from table-cell to inline-flex items */
  .ranker-table__td {
    display: inline-flex;
    align-items: center;
    padding: 0;
    border-bottom: none;
    vertical-align: unset;
  }

  /* Position badge: first in visual order */
  .ranker-table__td--pos {
    order: -1;
    flex-shrink: 0;
  }

  /* Name: takes all remaining space on the first line */
  .ranker-table__td--name {
    flex: 1;
    min-width: 0;
    font-size: var(--font-size-sm);
    font-weight: 600;
    /* Truncate very long names gracefully */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Team abbreviation: hidden — redundant with the fixture strip */
  .ranker-table__td--team {
    display: none;
  }

  /* Price: small muted text, stays on the first line */
  .ranker-table__td--price {
    font-size: var(--font-size-xs);
    color: var(--color-muted);
    white-space: nowrap;
  }

  /* Value chip: stays on first line */
  .ranker-table__td--value {
    flex-shrink: 0;
    white-space: nowrap;
  }

  /* Fixture strip: wraps onto its own full-width second line */
  .ranker-table__td--fixtures {
    flex-basis: 100%;
    order: 10;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    /* Remove the whitespace:nowrap set on desktop */
    white-space: normal;
  }

  /* Minutes security badge: hidden on mobile — too much noise */
  .ranker-table__td--min {
    display: none;
  }

  /* Cost/Pt, Avg Pts/GW, Total Pts, Form, Tsfr In, Tsfr Out, Next Fixture:
     hidden on mobile, same reasoning as Team/Min above — the compact card keeps
     only Value + fixture strip. The transfer counts are the widest numbers in
     the table and the least useful at a glance, so they go first. */
  .ranker-table__td--cost-per-point,
  .ranker-table__td--avg-pts,
  .ranker-table__td--total-pts,
  .ranker-table__td--form,
  .ranker-table__td--tsfr-in,
  .ranker-table__td--tsfr-out,
  .ranker-table__td--next-fixture {
    display: none;
  }

  /* Price-change cell: one of the few kept on the compact card. It now sits
     immediately after Price in DOM order (it moved from last to column 5), so
     on mobile it reads as "£7.5m  ↑ +£0.3m" rather than trailing the value
     chip as it used to. */
  .ranker-table__td--price-change {
    flex-shrink: 0;
    white-space: nowrap;
    text-align: left;
  }

  /* Empty / loading state: block so colspan has no effect */
  .ranker-table__td.ranker-table__empty,
  .ranker-table__empty {
    display: block;
    width: 100%;
    text-align: center;
    padding: var(--space-6) var(--space-3);
  }

  /* Filters panel: tighten gaps once it's stacked full-width above the table
     (the .ranker-shell media query above already collapses to 1fr). */
  .ranker-filters {
    gap: var(--space-3);
  }

  /* Select wrappers: let them grow to fill available width */
  .ranker-controls__select-wrap {
    flex: 1;
    min-width: 0;
  }

  .ranker-select {
    width: 100%;
  }

  /* Filter buttons: 44px tap targets */
  .ranker-pos-btn,
  .ranker-minsec-btn {
    min-height: 44px;
    padding: var(--space-2) var(--space-2);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* ─── GW Dashboard ────────────────────────────────────────────────────── */

  /* Controls: stack vertically so each control is full width */
  .dash-controls {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-3);
    padding: var(--space-3);
  }

  /* Search combo and input: full width */
  .dash-search-combo {
    width: 100%;
  }

  .dash-search-input {
    width: 100%;
    box-sizing: border-box;
  }

  /* Search results dropdown: match the now full-width input */
  .dash-search-results {
    max-width: 100%;
  }

  /* Position filter: 44px tap targets */
  .dash-pos-btn {
    min-height: 44px;
    padding: var(--space-2) var(--space-3);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* Import button: full width so it's easy to tap */
  .squad-import-btn {
    min-height: 44px;
    width: 100%;
  }

  /* Import panel: full width below the button */
  .squad-import-wrap {
    width: 100%;
  }

  .squad-import-panel {
    left: 0;
    right: 0;
    min-width: 0;
    width: 100%;
  }

  /* Squad tally: centred */
  .dash-squad-tally {
    text-align: center;
    align-self: center;
  }

  /* dash-shell is already single-column at 900px; reduce padding */
  .dash-shell {
    padding: 0 0 var(--space-8);
    gap: var(--space-4);
  }

  /* Squad slots: taller to be comfortably tappable */
  .dash-squad-slot {
    min-height: 44px;
  }

  /* Captain card: less horizontal padding on a small screen */
  .dash-captain {
    padding: var(--space-3);
  }

  /* Player rows: 44px minimum tap height */
  .dash-player-row {
    min-height: 44px;
    padding: var(--space-2) var(--space-3);
  }

  /* Risk flags: remove the desktop indent that assumes a wide name column */
  .dash-player-row__flags {
    padding-left: 0;
  }

  /* ─── Transfer Planner ────────────────────────────────────────────────── */

  /* Controls: stack vertically */
  .planner-controls {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-3);
    padding: var(--space-3);
  }

  /* Budget input: full width */
  .planner-budget-wrap {
    width: 100%;
  }

  .planner-budget-input {
    width: 100%;
    box-sizing: border-box;
  }

  /* Free-transfer buttons: 44px tap targets */
  .planner-ft-btn {
    min-height: 44px;
    flex: 1;
    justify-content: center;
  }

  /* Hit toggle: full width, 44px */
  .planner-hit-toggle {
    min-height: 44px;
    width: 100%;
  }

  /* Position filter: 44px */
  .planner-pos-btn {
    min-height: 44px;
    padding: var(--space-2) var(--space-3);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* planner-shell is already single-column at 900px; reduce padding */
  .planner-shell {
    padding: 0 0 var(--space-8);
    gap: var(--space-4);
  }

  /* Chip grid: single column */
  .planner-chips__grid {
    grid-template-columns: 1fr;
  }

  /* Transfer card body: OUT stacks above IN instead of left/right.
     The arrow element is rotated 90° so "→" becomes "↓" without HTML changes. */
  .planner-transfer-card__body {
    flex-direction: column;
    align-items: stretch;
    padding: var(--space-2) var(--space-3);
    gap: var(--space-2);
  }

  .planner-transfer-card__arrow {
    align-self: center;
    /* Rotate the → arrow clockwise so it points downward */
    transform: rotate(90deg);
  }

  /* Player projection blocks: full width when stacked */
  .planner-player {
    width: 100%;
  }

  /* Price change footer: reduce padding */
  .planner-transfer-card__price-footer {
    padding: var(--space-2) var(--space-3);
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   FIXTURES — gameweek grid, league table, per-team season, head-to-head history
   All four views run on live data. Prefixed fx- throughout. BEM-lite per
   CONVENTIONS.md §5.1, tokens only.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─── Controls bar ───────────────────────────────────────────────────────── */

.fx-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  padding-bottom: var(--space-4);
  margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--color-border);
}

/* Mode switch. Same visual language as .horizon-switcher, but scoped here so
   a change to the global switcher cannot silently restyle this module. */
.fx-modes,
.fx-scope {
  display: flex;
  gap: var(--space-1);
}

.fx-modes__btn,
.fx-scope__btn {
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}

.fx-modes__btn:hover,
.fx-scope__btn:hover {
  background: var(--color-surface-raised);
  color: var(--color-text);
  border-color: var(--color-muted);
}

.fx-modes__btn.is-active,
.fx-scope__btn.is-active {
  background: var(--color-accent-muted);
  border-color: var(--color-accent);
  color: var(--color-accent);
}

.fx-picker {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.fx-label {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.fx-select {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  font-size: var(--font-size-sm);
  font-family: var(--font-sans);
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
  transition: border-color 0.15s;
}

.fx-select:hover { border-color: var(--color-muted); }

.fx-select:focus {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  border-color: var(--color-accent);
}

/* GW stepper — arrows, label, and a "back to now" escape hatch. */
.fx-step,
.fx-step__now,
.fx-swap {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  line-height: 1;
  padding: var(--space-1) var(--space-2);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}

.fx-step:hover,
.fx-step__now:hover,
.fx-swap:hover {
  background: var(--color-surface-raised);
  border-color: var(--color-muted);
}

.fx-step__now { color: var(--color-muted); }

.fx-step__label {
  font-size: var(--font-size-sm);
  font-weight: 600;
  min-width: 12ch;
  text-align: center;
}

/* ─── Panes ──────────────────────────────────────────────────────────────── */

/* Mirrors the .module-view show/hide contract one level down: exactly one
   pane carries .is-active at a time (see setMode() in fixtures.js). */
.fx-pane { display: none; }
.fx-pane.is-active { display: block; }

.fx-pane__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
}

.fx-pane__title {
  font-size: var(--font-size-xl);
  font-weight: 700;
  letter-spacing: -0.01em;
}

.fx-pane__sub {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  margin-top: var(--space-1);
  font-size: var(--font-size-sm);
  color: var(--color-muted);
}

.fx-pane__sub > span {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* ─── Status legend ──────────────────────────────────────────────────────── */

.fx-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.fx-legend__item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* ─── Status chip ────────────────────────────────────────────────────────── */

.fx-status {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 4.5ch;
  padding: 1px var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--band-neutral-bg);
  color: var(--color-muted);
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.03em;
  flex-shrink: 0;
}

.fx-status--live {
  background: var(--band-green-bg);
  border-color: var(--band-green);
  color: var(--band-green);
}

.fx-status--upcoming {
  background: var(--color-accent-muted);
  border-color: var(--color-accent);
  color: var(--color-accent);
}

/* ─── Day grouping ───────────────────────────────────────────────────────── */

.fx-daygroup { margin-bottom: var(--space-6); }

.fx-daygroup__title {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding-bottom: var(--space-2);
  margin-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-border-subtle);
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text);
}

.fx-daygroup__day {
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

/* ─── Fixture list ───────────────────────────────────────────────────────── */

.fx-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.fx-item { list-style: none; }

.fx-fixture {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  overflow: hidden;
}

.fx-fixture[open] { border-color: var(--color-muted); }

.fx-fixture__summary {
  display: grid;
  grid-template-columns: 5.5ch 1fr 11ch 1fr 2ch;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
  list-style: none;
  transition: background 0.15s;
}

.fx-fixture__summary::-webkit-details-marker { display: none; }
.fx-fixture__summary:hover { background: var(--color-surface-raised); }

.fx-side {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

/* The away side mirrors so both team names sit against the centre score. */
.fx-side--away { flex-direction: row-reverse; }

/* Full club name, not the abbreviation. The crest and result box either side
   both set flex-shrink: 0, so this is the only part of a side that can give
   way — it ellipsizes rather than pushing the scoreline off centre. The title
   attribute carries the untruncated name for the narrow widths where that
   actually bites (longest in the league is "Nottingham Forest"). */
.fx-side__name {
  font-size: var(--font-size-sm);
  font-weight: 600;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Crest slot — an empty ring until real badges land. */
.fx-crest {
  display: inline-block;
  vertical-align: middle;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1px var(--estimated-border-style) var(--color-border);
  background: var(--color-surface-raised);
}

.fx-crest--sm { width: 14px; height: 14px; }
.fx-crest--lg { width: 44px; height: 44px; border-radius: var(--radius-md); }

.fx-fixture__centre {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.fx-fixture__score {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--font-size-md);
  font-weight: 700;
}

.fx-fixture__score em {
  font-style: normal;
  color: var(--color-muted);
}

.fx-fixture__ko {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.fx-fixture__chev {
  color: var(--color-muted);
  font-size: var(--font-size-sm);
  transition: transform 0.15s;
}

.fx-fixture[open] .fx-fixture__chev { transform: rotate(180deg); }

/* ─── Fixture detail disclosure ──────────────────────────────────────────── */

.fx-detail {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-4);
  border-top: 1px solid var(--color-border);
  background: var(--color-bg);
}

.fx-detail__block {
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  padding: var(--space-3);
}

.fx-detail__title {
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-muted);
  margin-bottom: var(--space-3);
}

.fx-detail__cols {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
}

.fx-detail__note {
  margin-top: var(--space-2);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  font-style: italic;
}

/* ─── Match events ───────────────────────────────────────────────────────── */

.fx-events {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.fx-event {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-sm);
}

/* Away events read right-to-left so the two feeds mirror the pitch. */
.fx-events--away .fx-event {
  flex-direction: row-reverse;
  text-align: right;
}

.fx-event__min {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  flex-shrink: 0;
}

.fx-event__icon {
  font-size: var(--font-size-sm);
  line-height: 1;
}

.fx-event__type {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

/* ─── Lineups ────────────────────────────────────────────────────────────── */

.fx-lineup__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding-bottom: var(--space-2);
  margin-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-border-subtle);
  font-size: var(--font-size-sm);
  font-weight: 600;
}

.fx-lineup__formation {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.fx-lineup__subhead {
  margin: var(--space-3) 0 var(--space-2);
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-muted);
}

.fx-lineup__list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.fx-lineup__list--bench { opacity: 0.7; }

.fx-lineup__row {
  display: grid;
  grid-template-columns: 3ch 1fr 5ch;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-sm);
}

.fx-lineup__num {
  font-family: var(--font-mono);
  color: var(--color-muted);
}

.fx-lineup__pos {
  text-align: right;
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

/* ─── H2H peek inside a fixture ──────────────────────────────────────────── */

.fx-h2h-mini {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  font-size: var(--font-size-sm);
}

.fx-h2h-mini__tally,
.fx-h2h-mini__trend {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.fx-h2h-mini__tally em,
.fx-h2h-mini__trend {
  font-style: normal;
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.fx-link-btn {
  margin-top: var(--space-3);
  background: none;
  border: none;
  padding: 0;
  color: var(--color-accent);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  cursor: pointer;
}

.fx-link-btn:hover { text-decoration: underline; }

/* ─── Form / result pips ─────────────────────────────────────────────────── */

.fx-pips {
  display: inline-flex;
  gap: 3px;
}

.fx-pip {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  background: var(--band-neutral);
}

.fx-pip--w { background: var(--band-green); }
.fx-pip--d { background: var(--band-neutral); }
.fx-pip--l { background: var(--band-red); }

/* ─── Team pane ──────────────────────────────────────────────────────────── */

.fx-pane__head--team { align-items: center; }

.fx-teamhead {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.fx-stat-row {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.fx-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-width: 6ch;
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
}

.fx-stat__label {
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-muted);
}

.fx-stat__value {
  font-family: var(--font-mono);
  font-weight: 700;
}

/* Two-column results/upcoming split — same 1fr 1fr rhythm as .fx-detail__cols.
   The bottom margin matters on the head-to-head pane, where a full-width table
   follows: the two columns are uneven by nature (one a fixed two-row table, the
   other a list), so whichever ends lower would otherwise touch it. */
.fx-two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-6);
  margin-bottom: var(--space-6);
}

.fx-col__title {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding-bottom: var(--space-2);
  margin-bottom: var(--space-3);
  border-bottom: 1px solid var(--color-border);
  font-size: var(--font-size-md);
  font-weight: 600;
}

.fx-col__count {
  font-size: var(--font-size-xs);
  font-weight: 400;
  color: var(--color-muted);
}

.fx-list--compact { gap: var(--space-1); }

/* Six columns: gameweek, date, venue, opponent, score-or-kickoff, and a tail
   that is the result box on a played fixture and the FPL difficulty on an
   upcoming one. Results and upcoming fixtures share the shape deliberately, so
   the eye reads straight across the two-column split without re-learning it.
   Every row must emit all six cells — an omitted one shifts the rest left. */
.fx-row {
  display: grid;
  grid-template-columns: 5ch 11ch 2ch 1fr 7ch 3ch;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  font-size: var(--font-size-sm);
}

.fx-row:hover {
  border-color: var(--color-border);
  background: var(--color-surface-raised);
}

.fx-row__date {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

/* The gameweek a fixture belongs to — mono so the column reads as a scale
   rather than as prose, and quiet enough not to compete with the opponent. */
.fx-row__gw {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.fx-venue {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.fx-row__opp {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.fx-row__value {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  font-family: var(--font-mono);
  font-weight: 700;
}

.fx-row__value em {
  font-style: normal;
  color: var(--color-muted);
}

/* An unplayed fixture shows its kickoff where a score would go — same slot,
   lighter, so scanning the column separates played from pending instantly.
   Mirrors what .fx-fixture__score--time does on the gameweek grid. */
.fx-row__value--time {
  font-weight: 400;
  color: var(--color-muted);
}

.fx-row__tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 1px var(--space-1);
  border: 1px var(--estimated-border-style) var(--color-border);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
}

/* The home/away split table, between the header strip and the two columns.
   Its own block rather than part of .fx-two-col because it spans the width —
   the two venues are rows of one table, not two independent lists. */
.fx-vsplit { margin-bottom: var(--space-6); }

.fx-vsplit .fx-table th[scope="row"] {
  font-weight: 600;
  white-space: nowrap;
}

/* ─── Head-to-head pane ──────────────────────────────────────────────────── */

.fx-h2h-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.fx-h2h-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  text-align: center;
}

/* The three result tiles borrow band colour so the split reads at a glance:
   team A wins, draws, team B wins (CONVENTIONS.md §5.2 colours).

   Green / grey / RED, which is exactly what .fx-pip--w/--d/--l paints in the
   run of results a few lines below these tiles. "Man City wins" and a red pip
   are the same event described twice on the same screen, and while the third
   tile was amber the page said so in two colours — leaving the reader to work
   out whether amber was a softer kind of loss. It is not: these are wins for
   the other side, and the app's colour for that is red everywhere else. */
.fx-h2h-tile--a { border-color: var(--band-green);   background: var(--band-green-bg); }
.fx-h2h-tile--d { border-color: var(--band-neutral); background: var(--band-neutral-bg); }
.fx-h2h-tile--b { border-color: var(--band-red);     background: var(--band-red-bg); }

.fx-h2h-tile__value {
  font-family: var(--font-mono);
  font-size: var(--font-size-xl);
  font-weight: 700;
}

.fx-h2h-tile__label {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.fx-h2h-trend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  margin-bottom: var(--space-4);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  font-size: var(--font-size-sm);
}

.fx-h2h-trend__label { font-weight: 600; }

.fx-h2h-trend__note {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

/* The aggregate-goals tile carries a scoreline, not a single number, so its
   separator is dimmed the same way every other score in the tab is. */
.fx-h2h-tile__value em {
  font-style: normal;
  color: var(--color-muted);
}

/* The meeting date now carries its year, so it needs room to stay on one line
   — a wrapped date would make its row taller than its neighbours. */
.fx-h2h-date { white-space: nowrap; }

/* A club cell in the meeting table: crest then short name, kept on one line so
   the Home and Away columns stay the same height as the score between them. */
.fx-h2h-club {
  white-space: nowrap;
}

.fx-h2h-club .fx-crest {
  margin-right: var(--space-2);
  vertical-align: middle;
}

/* Prose facts about the pairing that no column of the table states directly —
   the current run, each side's widest win, the points split. Label left,
   value right, so the labels form a readable spine. */
.fx-notable {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.fx-notable__item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  font-size: var(--font-size-sm);
}

.fx-notable__label {
  flex-shrink: 0;
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-muted);
}

.fx-notable__value {
  text-align: right;
  font-weight: 600;
}

.fx-notable__value em {
  font-style: normal;
  font-weight: 400;
  color: var(--color-muted);
}

/* .fx-pane__sub normally sits inside .fx-pane__head; on the head-to-head pane
   it follows the header as its own line, so it needs the margin the header
   would otherwise have supplied. */
.fx-pane__sub--standalone { margin-bottom: var(--space-4); }

.fx-table-wrap {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow-x: auto;
}

.fx-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-sm);
}

.fx-table th {
  position: sticky;
  top: 0;
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface-raised);
  border-bottom: 1px solid var(--color-border);
  text-align: left;
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-muted);
  white-space: nowrap;
}

.fx-table td {
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--color-border-subtle);
}

.fx-table tbody tr:last-child td { border-bottom: none; }
.fx-table tbody tr:hover { background: var(--color-surface); }

.fx-table__score {
  font-family: var(--font-mono);
  font-weight: 700;
  white-space: nowrap;
}

.fx-table__score em {
  font-style: normal;
  color: var(--color-muted);
}

/* ─── League table ───────────────────────────────────────────────────────── */

/* Zone key in the legend. Same four colours as the row stripes below, and both
   are driven off LEAGUE_ZONES in fixtures.js so they cannot fall out of step. */
.fx-zone-key {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  background: var(--band-neutral);
}

/* One colour per competition, not a shade of one colour per finishing
   position — these four zones are different destinations, not a ranking.
   Deep blue and orange are the competitions' own colours; Conference takes
   the same green .fx-pip--w uses for a win. See base.css. */
.fx-zone-key--ucl  { background: var(--band-deep-blue); }
.fx-zone-key--uel  { background: var(--band-orange); }
.fx-zone-key--uecl { background: var(--band-green); }
.fx-zone-key--rel  { background: var(--band-red); }

/* Below this width .fx-table-wrap scrolls rather than crushing 13 columns. */
.fx-league { min-width: 820px; }

.fx-league__num {
  text-align: right;
  font-family: var(--font-mono);
  white-space: nowrap;
}

/* Pl through GD are the workings; PTS is the answer. Eight columns of
   identically-weighted figures gave the eye nothing to aim at, so everything
   feeding into the total drops to --color-muted and .fx-league__pts keeps
   --color-text plus its 700 weight — the contrast is what locates the column,
   not the position on the row.

   Scoped to .fx-league deliberately, for the same reason th.fx-league__num
   above is NOT: the By team split and the head-to-head venue split reuse this
   modifier on plain .fx-table markup and have no points column to set off, so
   greying them there would just dim an entire table for no gain.

   :not() rather than leaning on source order — the points cell carries both
   classes, so without the exclusion this rule's higher specificity would grey
   the one figure it exists to make stand out. */
.fx-league td.fx-league__num:not(.fx-league__pts) { color: var(--color-muted); }

/* Right-align the HEADER of a numeric column too. Deliberately NOT scoped to
   .fx-league: the By team split and the head-to-head venue split use the same
   modifier on plain .fx-table markup, and a left-aligned label sitting over
   right-aligned figures reads as two staggered columns. `th` alone is enough
   specificity to beat .fx-table th's own text-align. */
th.fx-league__num { text-align: right; }

.fx-league__pos {
  width: 4ch;
  color: var(--color-muted);
}

/* Zone stripe rides on the position cell as an inset shadow — a border-left on
   a border-collapse table is unreliable across the first-cell boundary. */
.fx-league__row--ucl  .fx-league__pos { box-shadow: inset 3px 0 0 var(--band-deep-blue); }
.fx-league__row--uel  .fx-league__pos { box-shadow: inset 3px 0 0 var(--band-orange); }
.fx-league__row--uecl .fx-league__pos { box-shadow: inset 3px 0 0 var(--band-green); }
.fx-league__row--rel  .fx-league__pos { box-shadow: inset 3px 0 0 var(--band-red); }

.fx-league__move {
  width: 3ch;
  text-align: center;
  font-size: var(--font-size-xs);
}

.fx-league__move--up   { color: var(--band-green); }
.fx-league__move--flat { color: var(--color-muted); }
.fx-league__move--down { color: var(--band-red); }

.fx-league__team {
  min-width: 18ch;
  white-space: nowrap;
}

.fx-league__team .fx-crest,
.fx-league__next .fx-crest { margin-right: var(--space-2); }

/* The team name is the cross-link into the By team view, so it reads as body
   text until hovered rather than as a row of blue links. */
.fx-league__team .fx-link-btn {
  margin-top: 0;
  vertical-align: middle;
  font-weight: 600;
  color: var(--color-text);
}

.fx-league__team .fx-link-btn:hover { color: var(--color-accent); }

.fx-league__pts {
  font-weight: 700;
  color: var(--color-text);
}

.fx-league__form,
.fx-league__next { white-space: nowrap; }

.fx-league__next .fx-row__tag {
  margin-left: var(--space-2);
  vertical-align: middle;
}

/* ─── Crests, empty states, badges ───────────────────────────────────────── */

/* A real badge replaces the empty ring .fx-crest draws: no outline, no fill. */
img.fx-crest {
  border: none;
  background: none;
  border-radius: 0;
  object-fit: contain;
}

/* "Nothing to show here" — used for empty gameweeks, unplayed fixtures, and
   an unreachable live endpoint. Quiet by design; these are normal states. */
.fx-empty {
  padding: var(--space-4);
  color: var(--color-muted);
  font-size: var(--font-size-sm);
  text-align: center;
}

/* Current / Next gameweek badge beside the pane title. */
.fx-tag {
  display: inline-block;
  vertical-align: middle;
  padding: 1px var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface-raised);
  color: var(--color-muted);
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.fx-tag--now {
  border-color: var(--color-accent);
  background: var(--color-accent-muted);
  color: var(--color-accent);
}

/* An unplayed fixture shows its kickoff time where a score would go — same
   slot, but lighter, so a scan down the column separates the two instantly. */
.fx-fixture__score--time {
  font-weight: 400;
  color: var(--color-muted);
}

/* A repeated event (a brace, a hat-trick) collapses to one line with a count. */
.fx-event__count {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-accent);
}

.fx-event__player { font-weight: 600; }

/* A team with no completed fixtures yet has no form to show. */
.fx-pips--empty { color: var(--color-muted); }

.fx-league__none { color: var(--color-muted); }

/* Official FPL 1–5 difficulty on the Next column. Direction is opposite to the
   Gaffer IQ score — 1 is easiest — so the band mapping is reversed here
   (CONVENTIONS.md §5.2, and see .fdr-chip which does the same job elsewhere). */
.fx-fdr--1 { border-color: var(--band-dark-green);   color: var(--band-dark-green);   }
.fx-fdr--2 { border-color: var(--band-green);    color: var(--band-green);    }
.fx-fdr--3 { border-color: var(--band-neutral); color: var(--band-neutral); }
.fx-fdr--4 { border-color: var(--band-amber);   color: var(--band-amber);   }
.fx-fdr--5 { border-color: var(--band-red);  color: var(--band-red);  }

/* Result box beside each team on a completed fixture. Sits on the inside edge
   of each side so the pair mirrors around the scoreline (.fx-side--away is
   row-reverse, so identical markup lands mirrored). Same three-part treatment
   as .fx-status: tinted ground, matching border, matching text. */
.fx-result {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  font-weight: 700;
  line-height: 1;
}

.fx-result--w { background: var(--band-green-bg);    border-color: var(--band-green);    color: var(--band-green);    }
.fx-result--d { background: var(--band-neutral-bg); border-color: var(--band-neutral); color: var(--color-muted);  }
.fx-result--l { background: var(--band-red-bg);  border-color: var(--band-red);  color: var(--band-red);  }

/* ─── Match timeline (Understat) ─────────────────────────────────────────────
   Two sides split by a central spine: home events left of it, away events
   right, minute in the middle. Which team an event belongs to is carried by
   POSITION rather than by a letter at the end of a long single column, which
   is what this replaced.

   Both sides render from identical markup — the home row is mirrored in CSS
   (row-reverse + right alignment) so its glyph and text hug the spine, the
   same technique .fx-side--away uses on the fixture row above. */

.fx-tl {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  position: relative;
}

/* The spine. Columns are symmetric (1fr | minute | 1fr), so the grid's centre
   and the minute column's centre are the same line. Sits behind the minute
   pills, which paint their own background over it. */
.fx-tl::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1px;
  background: var(--color-border);
}

.fx-tl__item {
  display: grid;
  grid-template-columns: 1fr 6ch 1fr;
  align-items: center;
  font-size: var(--font-size-sm);
}

.fx-tl__event {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
}

/* Both cells are pinned to row 1. Without this the AWAY row breaks: its event
   is placed in column 3, and sparse auto-placement then refuses to put the
   minute back in column 2 of the same row, pushing it onto a second row — the
   away rows rendered 47px tall against the home rows' 30px, with every away
   event floating 23px above its own minute. */
.fx-tl__event,
.fx-tl__minute { grid-row: 1; }

/* Home: first column, mirrored so the glyph lands against the spine. */
.fx-tl__item--home .fx-tl__event {
  grid-column: 1;
  justify-self: end;
  flex-direction: row-reverse;
  text-align: right;
}

/* Away: third column, natural order — glyph already sits against the spine. */
.fx-tl__item--away .fx-tl__event {
  grid-column: 3;
  justify-self: start;
}

.fx-tl__item:hover .fx-tl__event { background: var(--color-surface); }

.fx-tl__minute {
  grid-column: 2;
  justify-self: center;
  z-index: 1;
  padding: 0 var(--space-1);
  background: var(--color-bg);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  text-align: center;
}

.fx-tl__icon { line-height: 1; }

.fx-tl__body {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
}

/* The body is deliberately NOT reversed. Only the outer .fx-tl__event mirrors,
   which is what puts the glyph against the spine; reversing the body as well
   made a home row read "1 - 0  assist X  Kai Havertz" and — far worse — turned
   a substitution into "Zubimendi → Rice", naming the wrong man as the one
   coming off. Player order carries meaning, so it stays left-to-right. */

.fx-tl__player { font-weight: 600; }

/* The assister rides on the scorer's line — one moment, one row. */
.fx-tl__assist {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.fx-tl__score {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  font-weight: 700;
  padding: 0 var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface-raised);
}

.fx-tl__player--off { color: var(--band-red); font-weight: 400; }
.fx-tl__player--on  { color: var(--band-green); }
.fx-tl__arrow       { color: var(--color-muted); }

/* Available to any content that must exist for assistive tech but not on
   screen — here, the home/away label the split conveys visually. */
.fx-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ─── Teamsheet (Understat rosters) ──────────────────────────────────────────
   A real starting XI with a derived formation, replacing the flat appearance
   list FPL's data forced. Same two-column frame as the old block so the
   fallback and the real thing sit identically in the panel. */

.fx-xi__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding-bottom: var(--space-2);
  margin-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-border-subtle);
  font-size: var(--font-size-sm);
  font-weight: 600;
}

.fx-xi__formation {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-accent);
  letter-spacing: 0.04em;
}

.fx-xi__subhead {
  margin: var(--space-3) 0 var(--space-2);
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-muted);
}

/* The LIST owns the columns, not each row. Each .fx-xi__row used to be its
   own grid, so an `auto` track sized to that row's content alone and the
   minutes column landed at a different x on every line — a substituted player
   and an ever-present one never lined up. `display: contents` lifts the rows'
   cells into this single grid, so all five tracks are shared and the columns
   run straight down the list. */
.fx-xi__list {
  list-style: none;
  display: grid;
  grid-template-columns: 4.5ch 1fr auto auto 4ch;
  align-items: center;
  column-gap: var(--space-2);
  row-gap: var(--space-1);
}

/* Substitutes read as secondary to the XI above them. */
.fx-xi__list--subs { opacity: 0.85; }

.fx-xi__row {
  display: contents;
  font-size: var(--font-size-sm);
}

/* The Understat position code (GK, DC, AMR…) — the thing that makes this a
   teamsheet rather than a list, so it gets its own quiet column. */
.fx-xi__pos {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  text-transform: uppercase;
}

.fx-xi__name {
  font-size: var(--font-size-sm);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fx-xi__marks {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.fx-xi__mark {
  display: inline-flex;
  align-items: center;
  gap: 1px;
  line-height: 1;
  font-size: var(--font-size-xs);
}

.fx-xi__markn {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-accent);
}

/* An own goal is still a goal glyph — desaturate it so it does not read as a
   credit on the scorer's line. */
.fx-xi__mark--own { opacity: 0.55; }

.fx-xi__swap {
  font-size: var(--font-size-xs);
  white-space: nowrap;
}

.fx-xi__swap--off { color: var(--band-red); }
.fx-xi__swap--on  { color: var(--band-green); }

.fx-xi__mins {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  text-align: right;
}

/* ─── Fixtures: mobile ───────────────────────────────────────────────────── */

@media (max-width: 768px) {

  .fx-controls { gap: var(--space-3); }

  .fx-modes,
  .fx-scope { width: 100%; }
  .fx-modes__btn,
  .fx-scope__btn { flex: 1; min-height: 44px; }

  .fx-picker { width: 100%; }
  .fx-select { flex: 1; min-width: 0; }

  .fx-step,
  .fx-step__now,
  .fx-swap { min-height: 44px; min-width: 44px; }

  /* Fixture row drops the venue column and stacks status above the pairing. */
  .fx-fixture__summary {
    grid-template-columns: 1fr 9ch 1fr;
    grid-template-areas:
      "status status status"
      "home   centre away";
    row-gap: var(--space-2);
    padding: var(--space-3);
  }

  .fx-fixture__summary > .fx-status { grid-area: status; justify-self: start; }
  .fx-side--home      { grid-area: home; }
  .fx-fixture__centre { grid-area: centre; }
  .fx-side--away      { grid-area: away; }

  .fx-fixture__chev { display: none; }

  /* Both two-column blocks collapse to one. */
  .fx-detail__cols,
  .fx-two-col { grid-template-columns: 1fr; }

  .fx-xi__list { grid-template-columns: 4.5ch 1fr auto 4ch; }
  .fx-xi__swap { display: none; }

  .fx-tl__item { grid-template-columns: 1fr 4.5ch 1fr; font-size: var(--font-size-xs); }
  .fx-tl__event { padding: var(--space-1); gap: var(--space-1); }
  .fx-tl__body { gap: var(--space-1); }

  /* The gameweek is the one column that drops — it is the only cell whose
     information the date beside it already implies. Everything else stays, so
     the row keeps five children and five tracks; hiding a cell that is present
     on some rows and absent on others would put the grid out of phase. */
  .fx-row { grid-template-columns: 10ch 2ch 1fr 6ch 3ch; }
  .fx-row__gw { display: none; }

  /* Both head-to-head tables scroll inside .fx-table-wrap rather than
     squeezing; the notable list below them is already single-column. */
  .fx-notable__item { flex-direction: column; align-items: flex-start; gap: var(--space-1); }
  .fx-notable__value { text-align: left; }

  .fx-pane__head { flex-direction: column; align-items: flex-start; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   LANDING — the front page at /projects/apps/gaffer-iq/
   Hero-only composition: eyebrow → headline → subhead → a cluster of floating
   sample cards with the primary CTA nested inside it. Presentational; the only
   live value is the gameweek in the eyebrow (js/modules/landing.js).
   ═══════════════════════════════════════════════════════════════════════════ */

/* Overrides .module-view.is-active's `display: block` (equal specificity, later
   in the cascade — components.css loads after layout.css). The section is the
   vertical centring context for the whole page. */
.landing.is-active {
  display: flex;
  align-items: center;
  min-height: 100vh;
  /* The backdrop is deliberately wider than the content column (100vw) — clip
     the overhang here rather than letting it scroll the document sideways.
     `clip`, not `hidden`: hidden on one axis forces the other axis to compute
     as `auto`, which would turn this section into its own scroll container and
     put a second scrollbar inside the page once the content outgrows 100vh. */
  overflow-x: clip;
}

.landing__inner {
  position: relative;
  width: 100%;
  max-width: 1280px;
  margin: 0 auto;
  padding: var(--space-20) var(--space-10);
}

/* ─── Backdrop: pulsing accent glow + masked grid ────────────────────────── */

/* Pulled up above the content column and stretched to the full viewport so the
   glow reads as page-level lighting rather than a box behind the text. */
.landing__backdrop {
  position: absolute;
  top: -160px;
  left: 50%;
  transform: translateX(-50%);
  width: 100vw;
  height: max(900px, 80vh);
  overflow: hidden;
  pointer-events: none;
}

.landing__glow {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: min(1400px, 90vw);
  height: min(900px, 80vh);
  background: radial-gradient(ellipse at center,
    color-mix(in srgb, var(--color-accent) 16%, transparent) 0%,
    color-mix(in srgb, var(--color-accent)  5%, transparent) 45%,
    transparent 70%);
  animation: landing-pulse 6s ease-in-out infinite;
}

/* Faint engineering-grid texture. The white alphas here are raw rgba() with no
   token behind them — same as the existing "provisional" hatch overlays, which
   are the only other place the system tints with white rather than a palette
   colour. */
.landing__grid {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
  background-size: 64px 64px;
  /* Fades the grid out before it reaches the edges, so it never reads as a
     hard-edged panel. */
  -webkit-mask-image: radial-gradient(ellipse min(700px, 45vw) min(400px, 40vh) at 50% 18%, black, transparent);
  mask-image: radial-gradient(ellipse min(700px, 45vw) min(400px, 40vh) at 50% 18%, black, transparent);
}

/* ─── Intro: eyebrow, headline, subhead ──────────────────────────────────── */

.landing__intro {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-6);
}

.landing__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  border-radius: var(--radius-pill);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.landing__eyebrow-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--band-green);
  flex-shrink: 0;
}

/* The one place in the app that does not use --font-sans. Anton is a
   single-weight display face, so this stays at 400 — the tight tracking and
   negative letter-spacing the sans headline needed are wrong for it, hence the
   positive 0.005em and the sub-1 line-height instead. */
.landing__title {
  font-family: var(--font-display);
  font-size: var(--font-size-hero);
  font-weight: 400;
  letter-spacing: 0.005em;
  line-height: 0.98;
  text-transform: uppercase;
  max-width: 960px;
}

.landing__title-accent {
  background: linear-gradient(90deg, var(--color-accent), var(--pos-mid));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.landing__subhead {
  font-size: var(--font-size-lg);
  color: var(--color-muted);
  max-width: 620px;
  line-height: 1.6;
}

/* ─── Floating card cluster ──────────────────────────────────────────────── */

/* Fixed height because every child is absolutely positioned — the cluster has
   no flow content of its own to size it. */
.landing__cluster {
  position: relative;
  /* 40px, half the reference's 96px. The satellite cards top out well above
     the matchup card's own top edge, so the measured gap and the optical one
     are not the same thing — at the reference value the hero read as two
     disconnected blocks rather than one composition. */
  margin-top: var(--space-10);
  height: 340px;
}

.landing-card {
  position: absolute;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-float);
}

/* Centre card — a full matchup card in the brutal band, left-bordered by band
   colour exactly as .matchup-card does. */
.landing-card--matchup {
  left: 50%;
  top: 10px;
  transform: translateX(-50%);
  width: min(560px, 88vw);
  padding: var(--space-7);
  border-left: 3px solid var(--band-red);
  box-shadow: var(--shadow-raised);
  z-index: 3;
}

.landing-card__head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--space-5);
}

.landing-card__teams {
  font-weight: 700;
  font-size: var(--font-size-md);
  letter-spacing: -0.01em;
}

.landing-card__vs {
  color: var(--color-muted);
  font-weight: 400;
}

.landing-card__meta {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.landing-card__body {
  display: flex;
  gap: var(--space-4);
  align-items: center;
}

/* Score pill — same {tint, border, text} trio as .score-pill, keyed off a band. */
.landing-pill {
  display: flex;
  flex-direction: column;
  align-items: center;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
}

.landing-pill--brutal {
  background: var(--band-red-bg);
  border-color: var(--band-red);
  color: var(--band-red);
  padding: var(--space-3) var(--space-5);
  min-width: 76px;
}

.landing-pill--great {
  background: var(--band-dark-green-bg);
  border-color: var(--band-dark-green);
  color: var(--band-dark-green);
  padding: var(--space-2) var(--space-3);
  min-width: 52px;
}

.landing-pill__value {
  font-size: var(--font-size-3xl);
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.landing-pill--great .landing-pill__value {
  font-size: var(--font-size-xl);
}

.landing-pill__band {
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin-top: var(--space-1);
}

/* Metric bars — the .bar-track / .bar-fill recipe at the matchup card's size. */
.landing-metrics {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.landing-metric {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.landing-metric__label {
  width: 76px;
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  flex-shrink: 0;
}

.landing-metric__track {
  flex: 1;
  height: 6px;
  background: var(--color-surface-raised);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.landing-metric__fill {
  height: 100%;
  border-radius: var(--radius-sm);
  background: var(--band-red);
}

/* Fixed widths, not computed ones — this card is a worked example, so the fill
   percentages belong in CSS rather than as inline styles set by JS (the
   exemption in CONVENTIONS.md §5.3 is for genuinely dynamic values). */
.landing-metric__fill--28 { width: 28%; }
.landing-metric__fill--18 { width: 18%; }

/* ─── Satellite cards ────────────────────────────────────────────────────── */

.landing-card--score {
  left: calc(50% - 460px);
  top: 60px;
  width: 180px;
  padding: var(--space-4);
  animation: landing-float-1 5.5s ease-in-out infinite;
  z-index: 2;
}

.landing-card--player {
  right: calc(50% - 480px);
  top: 110px;
  width: 200px;
  padding: var(--space-4);
  animation: landing-float-2 6.5s ease-in-out infinite;
  z-index: 2;
}

.landing-card--estimated {
  left: calc(50% - 400px);
  bottom: 0;
  width: 150px;
  padding: var(--space-3);
  border-style: var(--estimated-border-style);
  border-color: var(--band-amber);
  box-shadow: var(--shadow-float-soft);
  opacity: 0.92;
  animation: landing-float-3 7s ease-in-out infinite;
  z-index: 1;
}

.landing-card--position {
  right: calc(50% - 410px);
  bottom: 20px;
  width: 150px;
  padding: var(--space-3);
  border-color: var(--pos-mid);
  box-shadow: var(--shadow-float-soft);
  /* Reversed so the cluster never drifts in sync — same keyframes as --score,
     a different duration and direction. */
  animation: landing-float-1 6s ease-in-out infinite reverse;
  z-index: 1;
}

.landing-card__row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.landing-card__name {
  font-size: var(--font-size-xs);
  font-weight: 600;
  /* Lets a long name shrink and ellipsise rather than shoving the score chip
     out of the card. Without min-width:0 a flex item refuses to shrink below
     its content width. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The chip keeps its size whatever the name does. */
.landing-card__split .landing-chip {
  flex-shrink: 0;
}

.landing-card__sub {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.landing-card__split {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--space-2);
}

/* Inline score chip — .score-chip's shape at badge size. */
.landing-chip {
  display: inline-flex;
  padding: 2px var(--space-2);
  border-radius: var(--radius-sm);
  font-weight: 700;
  font-size: var(--font-size-xs);
}

.landing-chip--good {
  background: var(--band-green-bg);
  color: var(--band-green);
}

/* Dashed + amber, matching how the app flags a score below the confidence
   floor everywhere else. */
.landing-chip--estimated {
  border: 1px solid var(--band-amber);
  border-style: var(--estimated-border-style);
  color: var(--band-amber);
}

.landing-chip--mid {
  background: color-mix(in srgb, var(--pos-mid) 12%, transparent);
  color: var(--pos-mid);
}

.landing-card__caption {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.landing-card__meta-line {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  margin-top: var(--space-2);
}

/* Player progress bar — the planner's thin pill-radius variant. */
.landing-progress {
  height: 4px;
  background: var(--color-border);
  border-radius: var(--radius-pill);
  overflow: hidden;
}

.landing-progress__fill {
  height: 100%;
  background: var(--color-accent);
  border-radius: var(--radius-pill);
}

.landing-progress__fill--68 { width: 68%; }

/* ─── CTA ────────────────────────────────────────────────────────────────── */

/* TWO NESTED ELEMENTS ON PURPOSE. A CSS animation replaces the whole
   `transform` property while it runs, so putting the float animation on the
   same element as the centring translateX(-50%) drops the centring the moment
   the animation starts. Outer element centres; inner element animates.
   Do not merge these. */
.landing__cta {
  position: absolute;
  left: 50%;
  bottom: var(--space-10);
  transform: translateX(-50%);
  z-index: 4;
}

.landing__cta-float {
  animation: landing-float-3 5.8s ease-in-out infinite;
}

/* Solid-fill primary action — the .squad-import-go recipe (opacity-only
   hover/active, #fff label) at hero size. */
.landing__cta-btn {
  display: inline-block;
  background: var(--color-accent);
  border: none;
  border-radius: var(--radius-sm);
  color: #fff;
  font-family: var(--font-sans);
  font-size: var(--font-size-md);
  font-weight: 700;
  padding: var(--space-3) var(--space-8);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: var(--shadow-accent);
  transition: opacity 0.15s;
}

.landing__cta-btn:hover  { opacity: 0.85; }
.landing__cta-btn:active { opacity: 0.70; }

/* ─── Scroll reveal ──────────────────────────────────────────────────────── */

/* Applied to each top-level block. js/modules/landing.js adds .is-visible,
   staggering the batch through --reveal-delay. */
.landing-reveal {
  opacity: 0;
  transform: translateY(10px);
  transition:
    opacity   620ms cubic-bezier(.2, 0, 0, 1),
    transform 620ms cubic-bezier(.2, 0, 0, 1);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}

.landing-reveal.is-visible {
  opacity: 1;
  transform: none;
}

/* ─── Motion ─────────────────────────────────────────────────────────────── */

@keyframes landing-pulse {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 0.90; }
}

@keyframes landing-float-1 {
  0%, 100% { transform: translateY(0)     rotate(-2deg); }
  50%      { transform: translateY(-14px) rotate(-1deg); }
}

@keyframes landing-float-2 {
  0%, 100% { transform: translateY(0)     rotate(2deg); }
  50%      { transform: translateY(-20px) rotate(3deg); }
}

@keyframes landing-float-3 {
  0%, 100% { transform: translateY(0)     rotate(-1deg); }
  50%      { transform: translateY(-10px) rotate(0deg); }
}

/* The reference only silenced the reveal transition. Everything decorative
   goes quiet here: an infinite drift loop is exactly what this media query
   exists to stop. The cards keep their resting rotation, so the composition
   still reads as a scattered cluster rather than a flat stack. */
@media (prefers-reduced-motion: reduce) {
  .landing-reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .landing__glow,
  .landing__cta-float {
    animation: none;
  }

  .landing-card--score     { animation: none; transform: rotate(-2deg); }
  .landing-card--player    { animation: none; transform: rotate(2deg); }
  .landing-card--estimated { animation: none; transform: rotate(-1deg); }
  .landing-card--position  { animation: none; transform: rotate(-2deg); }
}

/* ─── Narrow viewports ───────────────────────────────────────────────────── */

/* The satellite cards are positioned from the centre line at fixed pixel
   offsets (±400–460px), so below roughly 1000px they slide off-canvas and get
   clipped. Rather than let them half-disappear, drop them and let the matchup
   card carry the composition on its own, with the CTA below it in flow. */
@media (max-width: 1000px) {
  .landing-card--score,
  .landing-card--player,
  .landing-card--estimated,
  .landing-card--position {
    display: none;
  }

  .landing__cluster {
    height: auto;
    /* margin-top is inherited from the desktop rule — it is already --space-10. */
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-8);
  }

  .landing-card--matchup {
    position: static;
    transform: none;
    width: 100%;
    max-width: 560px;
  }

  .landing__cta {
    position: static;
    transform: none;
  }
}

@media (max-width: 768px) {
  .landing__inner {
    padding: var(--space-10) var(--space-4);
  }

  .landing-card--matchup {
    padding: var(--space-5);
  }

  /* Pill above the bars rather than beside them — at phone width the 76px
     labels plus the pill leave the tracks too short to read. */
  .landing-card__body {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-3);
  }

  .landing-pill--brutal {
    flex-direction: row;
    justify-content: center;
    align-items: baseline;
    gap: var(--space-2);
  }

  .landing-pill--brutal .landing-pill__band {
    margin-top: 0;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   SCHEDULE CONTEXT BAR
   App-wide, above every module view. Carries the shape of the schedule to the
   three tabs that render no fixture strip (Dashboard, Planner, Fixtures).
   Uses --color-accent, the structural-emphasis colour, for the same reason the
   double-gameweek slot does: it is not one of the five band colours, so it
   cannot be misread as a fixture-quality signal. See FEATURE_ENGINE.md §9.1.
   ═══════════════════════════════════════════════════════════════════════════ */

.schedule-bar {
  padding: var(--space-2) var(--space-4) 0;
}

.schedule-bar__inner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--color-accent-muted);
  border: 1px solid color-mix(in srgb, var(--color-accent) 35%, transparent);
  font-size: var(--font-size-xs);
  color: var(--color-text);
}

.schedule-bar__item {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  white-space: nowrap;
}

.schedule-bar__gw {
  color: var(--color-accent);
  font-weight: 700;
}

.schedule-bar__detail {
  color: var(--color-muted);
}

.schedule-bar__sep {
  width: 1px;
  height: 12px;
  background: var(--color-border);
  flex: none;
}

/* Schedule marker on Planner transfer cards. Shown on both sides of a swap:
   transferring OUT of a double or INTO a blank is the mistake it exists to
   catch. Same vocabulary as the fixture strip — accent for a double, muted for
   a blank — so the two read as one system. */
.planner-schedule-mark {
  margin-left: var(--space-1);
  font-size: 0.625rem;
  font-weight: 700;
}

.planner-schedule-mark--double { color: var(--color-accent); }
.planner-schedule-mark--blank  { color: var(--color-muted); }

/* Postponed fixtures on the Fixtures gameweek pane. Dashed, muted, and set
   apart from the day groups — these belong to no day and no gameweek, and the
   styling should not imply otherwise. Same dashed-pill vocabulary as
   .pgw-pending on the fixture strips. */
.fx-pending {
  border-top: 1px dashed var(--color-border);
  padding-top: var(--space-3);
}

.fx-pending__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.fx-pending__item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--band-neutral) 6%, transparent);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.fx-pending__team { font-weight: 700; color: var(--color-text); }
.fx-pending__v    { color: var(--color-muted); }

/* ─── Transfer Planner: verdict banner ─────────────────────────────────────── */

.planner-verdict {
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-left: 4px solid var(--band-neutral);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  margin-bottom: var(--space-3);
}

.planner-verdict--dominant { border-left-color: var(--band-dark-green); }
.planner-verdict--clear    { border-left-color: var(--band-green); }
.planner-verdict--close    { border-left-color: var(--band-neutral); }
.planner-verdict--estimated { border-left-style: dashed; }

/* A hard trigger jumped this lane ahead of the arithmetic leader — flag it
   with a distinct border colour so it never reads as an ordinary win. Comes
   after the confidence modifiers so it wins the cascade regardless of which
   confidence class also applied. */
.planner-verdict--promoted { border-left-color: var(--band-orange); }

.planner-verdict__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--space-1);
}

.planner-verdict__lane {
  font-weight: 600;
  text-transform: capitalize;
  font-size: var(--font-size-lg);
}

.planner-verdict__confidence {
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-muted);
}

.planner-verdict__promoted-mark {
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--band-orange);
  border: 1px solid var(--band-orange);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-1);
}

.planner-verdict__score { margin-left: auto; font-variant-numeric: tabular-nums; }
.planner-verdict__margin { font-size: var(--font-size-xs); color: var(--color-muted); }
.planner-verdict__headline { margin: 0; line-height: 1.5; }
.planner-verdict__alts { margin: var(--space-1) 0 0; font-size: var(--font-size-sm); color: var(--color-muted); }
.planner-verdict__triggers { list-style: none; margin: var(--space-2) 0 0; padding: 0; }

/* Which gameweek this plan is FOR. Sits above the headline because it changes
   how everything below it should be read — a plan for the round after the one
   on the scoreboard is a different claim. */
.planner-verdict__timing {
  margin: 0 0 var(--space-1);
  font-size: var(--font-size-sm);
  color: var(--color-muted);
  line-height: 1.5;
}

/* What acting on the winning lane commits you to. */
.planner-verdict__direction {
  margin: var(--space-2) 0 0;
  font-size: var(--font-size-sm);
  color: var(--color-muted);
  line-height: 1.5;
}

/* Dismiss. Default margin-left:auto right-aligns it on a 'roll' verdict, which
   renders no score; when a score IS present that element takes the auto margin
   instead and this one only needs a gap, or the free space would be split
   between the two and strand the score mid-row. */
.planner-verdict__close {
  flex: none;
  margin-left: auto;
  background: none;
  border: none;
  color: var(--color-muted);
  font-size: var(--font-size-lg);
  line-height: 1;
  cursor: pointer;
  padding: 0 var(--space-1);
}

.planner-verdict__score ~ .planner-verdict__close { margin-left: var(--space-2); }
.planner-verdict__close:hover { color: var(--color-text); }

/* Dismissed: one line, still naming the call and offering a way back. */
.planner-verdict--collapsed {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
}

.planner-verdict__gw { font-size: var(--font-size-xs); color: var(--color-muted); }

.planner-verdict__reopen {
  margin-left: auto;
  background: none;
  border: none;
  color: var(--color-accent);
  font-size: var(--font-size-xs);
  cursor: pointer;
  padding: 0;
}

.planner-verdict__trigger {
  display: flex;
  gap: var(--space-1);
  font-size: var(--font-size-sm);
  padding: var(--space-1) 0;
}

.planner-verdict__trigger-mark { color: var(--band-red); font-weight: 700; }

/* ─── Transfer Planner: lens boards ────────────────────────────────────────── */

.planner-board-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  gap: var(--space-3);
}

.planner-board {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  padding: var(--space-2);
  min-width: 0;
}

/* Two stacked rows, not two side-by-side columns: title and unit share the top
   row, and the blurb gets the card's FULL width beneath them. Sitting the
   blurb beside the unit boxed it into roughly half the card and wrapped a
   two-line sentence over four. */
.planner-board__hd {
  margin-bottom: var(--space-2);
}

.planner-board__hd-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
}

.planner-board__title { margin: 0; font-size: var(--font-size-sm); font-weight: 600; }

/* What strategy this board actually represents. Deliberately quiet — it is
   read once and then ignored, so it must not compete with the rows. */
.planner-board__blurb {
  margin: var(--space-1) 0 0;
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  line-height: 1.45;
}

/* The unit of the middle column. Shares the title's row, so it only has to
   yield when the two together outrun the card: the title is short and fixed,
   and this is the half that may wrap. */
.planner-board__unit {
  flex: 0 1 auto;
  min-width: 0;
  text-align: right;
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  line-height: 1.3;
}

.planner-board__rows  { list-style: none; margin: 0; padding: 0; }
.planner-board__empty { margin: 0; font-size: var(--font-size-sm); color: var(--color-muted); }

.planner-board__more {
  margin-top: var(--space-2);
  background: none;
  border: none;
  color: var(--color-accent);
  font-size: var(--font-size-xs);
  cursor: pointer;
  padding: 0;
}

/* ─── Transfer Planner: compact swap row ───────────────────────────────────── */

.planner-swap-row { border-top: 1px solid var(--color-border); padding: var(--space-1) 0; }
.planner-swap-row:first-child { border-top: none; }

.planner-swap-row__line {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--font-size-sm);
  min-width: 0;
}

.planner-swap-row__names { display: flex; gap: var(--space-1); min-width: 0; flex: 1; }
.planner-swap-row__out,
.planner-swap-row__in { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.planner-swap-row__out { color: var(--color-muted); }
.planner-swap-row__arrow { color: var(--color-muted); flex: none; }
.planner-swap-row__value { font-variant-numeric: tabular-nums; font-weight: 600; flex: none; }

/* Estimated inputs. Carried on the number itself rather than by a separate
   "~" column, which cost every row horizontal space to annotate a few of them.
   Same border-style treatment as .score-pill--estimated — the house
   convention. See ARCHITECTURE.md §11, FEATURE_ENGINE.md §8.3. */
.planner-swap-row__value--estimated {
  border-bottom: 1px var(--estimated-border-style) var(--color-muted);
  opacity: 0.82;
}
.planner-swap-row__price { font-variant-numeric: tabular-nums; font-size: var(--font-size-xs);
                           color: var(--color-muted); flex: none; }

.planner-swap-row__badge {
  flex: none;
  font-size: var(--font-size-xs);
  padding: 0 var(--space-1);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
}

.planner-swap-row__badge--urgent    { color: var(--band-red); font-weight: 700; }
.planner-swap-row__badge--xi        { color: var(--band-green); }
/* --estimated removed: the estimated signal now sits on the value itself
   (.planner-swap-row__value--estimated) rather than in its own column. */

.planner-swap-row__why {
  flex: none;
  background: none;
  border: none;
  color: var(--color-accent);
  font-size: var(--font-size-xs);
  cursor: pointer;
  padding: 0 var(--space-1);
}

/* ─── Transfer Planner: why disclosure ─────────────────────────────────────── */

.planner-why {
  padding: var(--space-2) 0 var(--space-1);
  font-size: var(--font-size-xs);
}

.planner-why__reasoning { margin: 0 0 var(--space-1); line-height: 1.5; }
.planner-why__components { margin: 0; display: grid;
                           grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
                           gap: var(--space-1); }
.planner-why__row { display: flex; justify-content: space-between; gap: var(--space-1); }
.planner-why__key { color: var(--color-muted); }
.planner-why__val { font-variant-numeric: tabular-nums; margin: 0; }

/* Saved-XI diff markers: where the team you set differs from the model's pick. */
.dash-squad-slot__diff {
  font-size: var(--font-size-xs);
  padding: 0 var(--space-1);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
}

.dash-squad-slot__diff--benched { color: var(--band-green); }
.dash-squad-slot__diff--started { color: var(--band-amber); }
.dash-squad-slot__diff--armband { color: var(--band-orange); font-weight: 700; }

/* WHERE THIS LIVES, AND WHY IT IS LAST IN THE FILE.
   A skeleton is composed onto the real component's class
   (`class="breakdown-row__bar skeleton"`), and several of those components set
   `background` at the same specificity this block does — .breakdown-row__bar
   paints itself --color-muted, for one. Equal specificity means source order
   decides, so the skeleton fill only reliably wins from the bottom of the
   file. Keep this block last; moving it up silently returns those skeletons to
   whatever colour their component paints, with no error anywhere to say so. */

/* ═══════════════════════════════════════════════════════════════════════════
   SKELETON — placeholder for a value that is still settling
   ---------------------------------------------------------------------------
   WHAT IT IS FOR. A CompositeScore is a weighted blend, and one of its
   components (counter-matchup) cannot be computed until that team's Understat
   payload lands during the boot-time prefetch. Before this existed the score
   rendered anyway and then silently rewrote itself a second or two later — a
   number that looks final and is not. A skeleton says "not yet" for exactly as
   long as that is true, and is replaced by the settled number.

   HOW TO USE IT. Compose, don't replace: put `skeleton` ON the element that
   would have carried the value, alongside its normal structural class, and
   OMIT the band modifier.

       <span class="score-chip skeleton">…</span>     not  <span class="skeleton">
       <div class="score-pill skeleton">…</div>
       <div class="pgw-cell skeleton"></div>

   The structural class keeps the element's exact footprint — padding, min-width,
   line-height — so the settled value drops in with no layout shift, and there
   is no second set of sizes here to keep in step with the real components.
   Skeletons keep their placeholder text (rendered transparent) for the same
   reason: it is what gives a text-sized box its height.

   The band modifier is what must be left off. Band backgrounds and the
   skeleton fill are the same specificity, so emitting both would leave the
   winner decided by source order — an unsettled score would read as a real
   band-coloured chip. Omitting it makes that unrepresentable rather than
   merely discouraged.

   Skeletons are decorative and carry no information, so mark them
   `aria-hidden="true"` and put `aria-busy="true"` on the region that owns them.
   ═══════════════════════════════════════════════════════════════════════════ */

.skeleton {
  /* Transparent text, not empty markup: the placeholder characters size the
     box to the same line height the real value will occupy. */
  color: transparent;
  /* The placeholder text is a sizing device, not content — never let it be
     selected or copied as if it were a real value. */
  user-select: none;
  /* Deliberately NOT pointer-events:none. Some skeletons sit on elements that
     are navigation as well as value — a per-gameweek strip cell is a link to
     that fixture's card whether or not its score has settled — and disabling
     the element wholesale would take the navigation away for the few seconds
     the number is still arriving. Withhold the VALUE, not the affordance. */
  /* Deliberately no border rule: each composed element keeps its own, so a
     skeleton score pill still shows the pill's outline and a skeleton chip
     still has none. The one thing a skeleton must always win is the FILL. */
  background-color: var(--color-skeleton);
  background-image: linear-gradient(
    90deg,
    transparent 0%,
    var(--color-skeleton-highlight) 50%,
    transparent 100%
  );
  background-size: 200% 100%;
  background-repeat: no-repeat;
  animation: skeleton-sweep var(--skeleton-shimmer-duration) linear infinite;
}

/* Any nested text (a score pill's value/band spans) inherits the transparency
   rather than punching through with its own band colour. */
.skeleton * {
  color: transparent;
}

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

/* The shimmer is the only thing that distinguishes a skeleton from an empty
   box, so when motion is unwelcome it becomes a flat tone that still reads as
   a filled placeholder — not simply an animation switched off. */
@media (prefers-reduced-motion: reduce) {
  .skeleton {
    animation: none;
    background-image: none;
    background-color: var(--color-skeleton-highlight);
  }
}

/* A standalone placeholder line, for regions with no existing element to
   compose onto — a loading pane, a recommendation panel, a table row. Fills
   its container's width, so the container chooses how wide a line reads. */
.skeleton--text {
  display: block;
  width: 100%;
  height: 1.1rem;
  border-radius: var(--radius-sm);
}

/* Placeholder Ranker rows. The real row is a button (it opens that player in
   the Matchup Analyser), so the skeleton has to give up the affordances that
   promise — there is no player behind it to open. This is the exception to the
   "withhold the value, not the affordance" rule above: elsewhere the element
   still leads somewhere real. */
.ranker-table__row--skeleton {
  cursor: default;
}

.ranker-table__row--skeleton:hover {
  background: transparent;
}

/* Stack of placeholder lines with the rhythm of body copy. */
.skeleton-lines {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2) 0;
}

/* Successive lines shorten, so the block reads as a paragraph rather than a
   grey rectangle. */
.skeleton-lines .skeleton--text:last-child {
  width: 60%;
}

/* ═══════════════════════════════════════════════════════════════════════════
   FULL SEASON STRIP — GW1-38 planning ribbon (Matchup page)
   js/modules/fullSeason.js. Task 8 ships the collapsed ribbon only: the
   container chrome and the collapsed columns. .season-rail (chip windows)
   and .season-key (colour key) are empty containers here on purpose — Tasks
   9-11 add their markup and rules to this same section, and Task 10 adds the
   expand/collapse panel's own rules alongside .season-gw below.
   BEM-lite per CONVENTIONS.md §5.1. Band colours reuse existing tokens.
   See docs/superpowers/specs/2026-09-01-full-season-strip-design.md.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─── Container chrome ───────────────────────────────────────────────────── */

/* --season-t / --season-ease live on :root, NOT on .season-strip below, even
   though they are used nowhere outside this feature. expand() clones a
   column into a standalone .season-gw--float element and appends it to
   document.body (position: fixed, so it can escape .season-scroller's
   overflow clipping — see the float rules further down) — that element is a
   COUSIN of .season-strip in the DOM, not a descendant of it, so a custom
   property scoped to .season-strip does not inherit onto it. A transition
   whose var(--season-ease) fails to resolve is an INVALID declaration, which
   drops the whole rule silently — the float would simply never animate.
   --season-t is set by initFullSeason() from SEASON_PHASE_MS (style.setProperty
   on document.documentElement), so JS and CSS never disagree about the phase
   length. --season-ease has no config.js constant of its own (nothing outside
   CSS needs the control points) so it stays a plain literal — js/modules/
   fullSeason.js's ported bezier()/EASE use the SAME four numbers so a
   scripted scroll rides the identical curve as this CSS transition. */
:root {
  --season-ease: cubic-bezier(.32, .72, 0, 1);
}

.season-strip {
  /* --season-col-w / --season-col-wide are set by initFullSeason() from
     js/config.js's SEASON_COL_W / SEASON_COL_WIDE (style.setProperty), so the
     ribbon's pixel geometry has exactly one source of truth — see js/modules/
     fullSeason.js. These two have no config constant of their own (nothing
     outside CSS needs them) so they stay here as plain literals:
     --season-col-past is the played-week stub's width, --season-col-gap is
     the gap between ribbon columns (prototype's --col-past / --col-gap).
     They are scoped to .season-strip rather than :root (unlike --season-t /
     --season-ease above) because every rule that reads them — .season-gw,
     .season-gw--wide, .season-rail__cell — applies only to elements that stay
     inside .season-strip for their whole life; only the FLOAT ever leaves it,
     and the float never carries these two classes. */
  --season-col-past: 22px;
  --season-col-gap: 4px;

  /* The second box of .matchup-shell (components.css, MATCHUP SHELL above),
     stacked under the three-column row and stretching the shell's full
     width — the whole window, since the shell itself escapes app-main's
     max-width. 38 gameweeks is a lot of horizontal track: every pixel the
     strip gains is track the reader does not have to scroll for, and it was
     previously boxed out of the 260px team-nav column for no reason but that
     the sticky team panel used to stretch down beside it.

     No margin-top here: the shell's own `gap` (--space-6, the same value this
     margin used to add) already spaces it from the row above, and keeping both
     would have doubled the gap. */
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  overflow: hidden;
}

.season-strip__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border-subtle);
  background: var(--color-surface-raised);
}

.season-strip__title {
  font-size: var(--font-size-md);
  font-weight: 700;
}

.season-strip__body {
  padding: var(--space-3) var(--space-4) var(--space-4);
}

/* ─── Scroller — fixed-width columns; 38 weeks never share the page width ─── */

.season-scroller {
  overflow-x: auto;
  padding-bottom: var(--space-1);
  /* Same treatment as .gw-nav__list--scroll (MATCHUP SHELL above), mirrored
     for a horizontal track: that scrollbar sets `width` on its ::-webkit-
     scrollbar because it scrolls vertically, this one scrolls horizontally so
     the equivalent rule sets `height` instead — `width` there does nothing
     here. Firefox's two-property scrollbar-width/scrollbar-color pair needs
     no such swap; it styles whichever axis actually overflows. */
  scrollbar-width: thin;
  scrollbar-color: var(--color-border) transparent;
}

.season-scroller::-webkit-scrollbar {
  height: 8px;
}

.season-scroller::-webkit-scrollbar-track {
  background: transparent;
}

.season-scroller::-webkit-scrollbar-thumb {
  background: var(--color-border);
  border-radius: var(--radius-pill);
}

.season-scroller::-webkit-scrollbar-thumb:hover {
  background: var(--color-muted);
}

.season-track {
  min-width: max-content;
}

.season-ribbon {
  display: flex;
  gap: var(--season-col-gap);
  /* Each column keeps its own natural height instead of stretching to the
     tallest one — a played-week stub (number only) stays visibly shorter
     than a scored week (tiles + dots). */
  align-items: flex-start;
}

/* ─── Collapsed column ───────────────────────────────────────────────────── */

.season-gw {
  position: relative;
  width: var(--season-col-w);
  flex: none;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-surface-raised);
  padding: 5px 4px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  cursor: pointer;
  overflow: hidden;
  transition: border-color 0.16s, box-shadow 0.16s, background 0.16s;
}

.season-gw:hover {
  border-color: var(--color-muted);
}

.season-gw:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.season-gw--past {
  width: var(--season-col-past);
  opacity: 0.4;
  background: transparent;
  border-style: dashed;
  cursor: default;
}

/* Phase 2's width target on the collapsed column and its rail cell (below).
   The float itself has its own width — set directly on .season-gw--float,
   below — this is only what the space it VACATES/opens does. */
.season-gw--wide {
  width: var(--season-col-wide);
}

/* Hides the summary the float above it is covering. Its opacity is what
   .season-gw__summary's own transition (below) animates — this class only
   supplies the target value, at whatever moment expand()/collapse() toggle
   it. */
.season-gw--under .season-gw__summary {
  opacity: 0;
}

/* A "loaded" week (SEASON_LOADED_MIN_GREAT+ matchups at 75+ — see
   isLoadedWeek in engine/season.js, which is a fixed number and not the top
   band's foot) gets a green glow so it stands out while scrolling past,
   before any column is opened. */
/* Primitive green, not --band-good: "this week is loaded" is a property of the
   week, not a 57-64 rating, and --band-good is blue. Same reasoning for every
   primitive below (the top-five dots, the key's swatches for them) — see
   base.css, "Band colours: PRIMITIVES". */
.season-gw--hot {
  border-color: var(--band-green);
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--band-green) 30%, transparent),
    0 0 13px color-mix(in srgb, var(--band-green) 20%, transparent);
}

.season-gw__n {
  flex: none;
  text-align: center;
  font-size: 0.625rem;
  font-weight: 700;
  color: var(--color-muted);
  font-variant-numeric: tabular-nums;
  line-height: 1.4;
  transition: color 0.16s;
}

.season-gw--hot .season-gw__n {
  color: var(--band-green);
}

.season-gw__summary {
  display: flex;
  flex-direction: column;
  gap: 3px;
  transition: opacity var(--season-t) ease;
}

/* Once the float is open, the summary it clones (see expand()) is taken out
   of flow and hidden — the panel's own .season-gw__title/.season-fxrow/
   .season-plist content (in .season-gw__body) is what shows instead. Offsets
   match .season-gw's own padding (5px 4px) so the absolutely-positioned
   summary still lands exactly where it sat before it was pulled out of flow. */
.season-gw.is-open .season-gw__summary {
  position: absolute;
  top: 5px;
  left: 4px;
  right: 4px;
  opacity: 0;
  pointer-events: none;
}

/* One tile per top matchup (SEASON_TOP_MATCHUPS of them) — the band tint
   alone encodes one-sidedness. No club swatches: a normalised Team carries
   no club colour, only name/shortName/code/badgeUrl (see columnHTML). */
.season-gw__tile {
  height: 14px;
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  flex: none;
}

/* The gold glow used to live on --great, as a local emphasis making the old
   scale's top band read as strongest at 14px tall. The seven-tier scale gives
   that job a band of its own (--excellent, 80+), so the treatment moved up one
   rung and --great is now simply the scale's green. The glow stays soft (small
   blur, low opacity) — this is a 14px tile in a 38-column ribbon, not a hero
   element. A tile is banded off the FAVOURED side's score, which is never
   below 50, so the three bottom tiers below never appear on the ribbon; they
   are here because the expanded panel's fixture rows share this scale. */
.season-gw__tile--excellent {
  background: var(--band-excellent-bg);
  border: 1px solid var(--band-excellent);
  box-shadow: 0 0 5px color-mix(in srgb, var(--band-excellent) 60%, transparent);
}
.season-gw__tile--great   { background: var(--band-great-bg);   border: 1px solid var(--band-great); }
.season-gw__tile--good    { background: var(--band-good-bg);    border: 1px solid var(--band-good); }
.season-gw__tile--neutral { background: var(--band-neutral-bg); border: 1px solid var(--color-border); }
.season-gw__tile--tough   { background: var(--band-tough-bg);   border: 1px solid var(--band-tough); }
.season-gw__tile--brutal  { background: var(--band-brutal-bg);  border: 1px solid var(--band-brutal); }
/* The border reads the INK, not --band-extreme: the fill is black now, so a
   black border on it would draw nothing and a 14px tile would go missing from
   the ribbon entirely. */
.season-gw__tile--extreme { background: var(--band-extreme-bg); border: 1px solid var(--band-extreme-ink); }

/* Double gameweek: rides alongside a band modifier above (columnHTML emits
   both classes on the same tile), so this only adds the accent ring. */
.season-gw__tile--double {
  outline: 1px solid var(--color-accent);
  outline-offset: 1px;
}

/* Postponed: columnHTML emits this WITHOUT a band class (there is no score to
   band), so unlike --double it has to state its own neutral-ish fill rather
   than layering onto one. */
.season-gw__tile--postponed {
  background: var(--band-neutral-bg);
  border: 1px solid var(--color-border);
  outline: 1px solid var(--band-red);
  outline-offset: 1px;
  opacity: 0.75;
}

.season-gw__dots {
  display: flex;
  gap: 2px;
  justify-content: center;
  margin-top: 1px;
  min-height: 7px;
  flex: none;
}

/* Player projections are Task 11's job (g.players is null until then, so no
   dots render yet) — this rule just states what a dot looks like once they
   do. */
.season-gw__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--band-green);
}

/* Matches .season-key__swatch--standout's treatment so the ribbon's standout
   dots deliver on what the key promises. */
.season-gw__dot--standout {
  background: var(--band-light-green);
  box-shadow: 0 0 6px color-mix(in srgb, var(--band-light-green) 90%, transparent);
}

/* ─── Chip-reset divider ─────────────────────────────────────────────────── */

/* Sits between GW19 and GW20 (CHIP_RESET_AFTER_GW). align-self: stretch fills
   it to the ribbon row's own cross-axis size — the tallest column in that
   row — standing in for the prototype's JS-measured --col-h; .season-ribbon's
   align-items: flex-start only governs the columns, so this item still
   stretches on its own. The "chips reset" label is carried by the span's
   title/aria-label attributes (see fullSeason.js render()), not a visible
   ::after tag.

   The margin is what makes the break read, exactly as .gw-nav__chip-reset's
   does in the team navigator: a hairline sitting in the ribbon's own 4px
   column gap is just a thinner column edge, and the reader has to be told
   what it means rather than seeing it. --space-1 a side puts 12px of clear
   track either side of the rule against 4px between neighbouring columns —
   three times the rhythm, on 54px columns, which separates without opening a
   hole the eye has to jump. The navigator can afford its --space-2 because
   its rows are 2px apart; the same value here would cost a column's width. */
.season-split {
  width: 1px;
  flex: none;
  align-self: stretch;
  margin: 0 var(--space-1);
  background: var(--color-border);
}

/* ─── Chip rail ───────────────────────────────────────────────────────────── */

/* Sits below the ribbon (not overlapping it), so the prototype's floating
   expand/collapse panel — Task 10 — can grow upward without covering it.
   Mirrors .season-ribbon's flex sizing (width/gap) exactly: a chip band's
   left edge lands on its gameweek's left edge only because both rows share
   the same --season-col-w / --season-col-gap tokens. */
.season-rail {
  display: flex;
  gap: var(--season-col-gap);
  height: 7px;
  margin-top: 6px;
}

/* The cell itself carries no colour any more — a gameweek can be covered by
   MORE THAN ONE chip window at once (the busiest week and the best captain
   week can legitimately coincide), so colour lives on a .season-rail__bar
   child instead. Cell width/gap is untouched by this — it is still what
   locks the rail to the ribbon's columns. */
.season-rail__cell {
  position: relative;
  width: var(--season-col-w);
  flex: none;
  display: flex;
  flex-direction: column;
  /* JS only ever sets transition-delay on this rule (expand()/collapse()) —
     the property/duration/ease stay fixed here, the same split the ribbon
     column and the float use. */
  transition: width var(--season-t) var(--season-ease);
}

.season-rail__cell--past { width: var(--season-col-past); }

/* Mirrors .season-gw--wide exactly, so a chip band's cell widens in lockstep
   with its column above it. */
.season-rail__cell--wide { width: var(--season-col-wide); }

/* One bar per matched window. flex: 1 splits the cell's height evenly across
   however many windows cover this gameweek, so two colliding chips read as a
   two-tone bar instead of one hiding the other. */
.season-rail__bar {
  position: relative;
  flex: 1;
  min-height: 0;
}

.season-rail__bar--wildcard      { background: var(--band-deep-blue); }
.season-rail__bar--triplecaptain { background: var(--band-orange); }
.season-rail__bar--freehit       { background: var(--band-amber); }

/* Bridges a chip window's run across the ribbon's flex gap with an overlay,
   NOT a negative margin. A negative margin consumes layout width, which
   drifts every later cell leftward — 32px of accumulated error by GW38 in
   the prototype's first attempt — taking the bands off their gameweeks.
   Lives on the bar (not the cell) so `background: inherit` picks up THIS
   window's own colour even when it is one of several stacked bars. */
.season-rail__bar--bridge::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: 100%;
  width: var(--season-col-gap);
  background: inherit;
}

.season-rail__bar--head { border-top-left-radius: 3px; border-bottom-left-radius: 3px; }
.season-rail__bar--tail { border-top-right-radius: 3px; border-bottom-right-radius: 3px; }

/* Lines up under .season-split — no visible mark of its own here, the divider
   above already carries the label. The margin is not decoration: this row's
   cells line up with the ribbon's columns only because both rows are built
   from the same widths in the same order, so the split's own footprint has to
   match its twin above or every chip band from GW20 on slides 8px left of the
   gameweek it belongs to. */
.season-rail__split { width: 1px; flex: none; margin: 0 var(--space-1); }

/* ─── Colour key ──────────────────────────────────────────────────────────── */

/* Static legend beneath the strip. Every graphic used on the ribbon and rail
   is named here (fullSeason.js renderKey()) so a first-time reader isn't
   left guessing what a tint or a dot means. */
.season-key {
  margin-top: 20px;
  padding-top: 14px;
  border-top: 1px solid var(--color-border-subtle);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(185px, 1fr));
  gap: 14px 22px;
}

.season-key__group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.season-key__heading {
  font-size: 0.625rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--color-muted);
  font-weight: 700;
}

.season-key__item {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

/* Base swatch is the "band tile" shape (matches .season-gw__tile's rounding);
   modifiers below override size/shape for the dot, bar and line variants. */
.season-key__swatch {
  width: 15px;
  height: 11px;
  border-radius: var(--radius-sm);
  flex: none;
  display: inline-block;
}

/* Each swatch matches its .season-gw__tile--* twin above exactly, glow
   included — the key would otherwise lie about what the ribbon's tiles look
   like. */
.season-key__swatch--excellent {
  background: var(--band-excellent-bg);
  border: 1px solid var(--band-excellent);
  box-shadow: 0 0 5px color-mix(in srgb, var(--band-excellent) 60%, transparent);
}
.season-key__swatch--great { background: var(--band-great-bg);   border: 1px solid var(--band-great); }
.season-key__swatch--good  { background: var(--band-good-bg);    border: 1px solid var(--band-good); }
.season-key__swatch--even  { background: var(--band-neutral-bg); border: 1px solid var(--color-border); }

.season-key__swatch--double {
  background: var(--band-neutral-bg);
  outline: 1px solid var(--color-accent);
  outline-offset: -1px;
}

.season-key__swatch--postponed {
  background: var(--band-red-bg);
  outline: 1px solid var(--band-red);
  outline-offset: -1px;
}

/* The chip-reset divider is a vertical rule, not a tile — this swatch matches
   its shape rather than the default rectangle. */
.season-key__swatch--split {
  width: 2px;
  height: 14px;
  border-radius: 0;
  background: var(--color-border);
}

.season-key__swatch--player {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--band-green);
}

.season-key__swatch--standout {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--band-light-green);
  box-shadow: 0 0 6px color-mix(in srgb, var(--band-light-green) 90%, transparent);
}

.season-key__swatch--favoured {
  background: var(--band-light-green-bg);
  border: 1px solid var(--band-light-green);
}

/* Chip-window colours match the rail bands above 1:1 (season-rail__bar's
   --wildcard/--triplecaptain/--freehit), just wider than tall so the swatch
   reads as a bar rather than a tile. */
.season-key__swatch--wildcard      { width: 24px; height: 7px; background: var(--band-deep-blue); }
.season-key__swatch--triplecaptain { width: 24px; height: 7px; background: var(--band-orange); }
.season-key__swatch--freehit       { width: 24px; height: 7px; background: var(--band-amber); }

.season-key__swatch--loaded {
  border: 1px solid var(--band-green);
  box-shadow: 0 0 8px color-mix(in srgb, var(--band-green) 50%, transparent);
}

/* ─── Expand/collapse panel (Task 10) ────────────────────────────────────────
   js/modules/fullSeason.js's expand()/collapse() clone a column's markup into
   a standalone .season-gw--float element and drive the three-phase 990ms
   choreography on it directly (inline `transition`/style writes, chained by
   transition-delay). Everything below is the STATIC half of that: the shapes
   these classes put on screen, not the timing, which lives entirely in JS so
   there is exactly one place a phase length can be wrong.
   ────────────────────────────────────────────────────────────────────────── */

/* Fixed, so it escapes .season-scroller's overflow-x:auto clipping — a
   non-visible overflow on one axis forces the other to auto per spec, and an
   in-flow panel would either be clipped or spawn a second scrollbar rather
   than floating over the page. Both selectors are needed: an equal-specificity
   .season-gw rule earlier in this file sets `position: relative`, and without
   the second selector here that rule would win once .is-open is also present
   (same specificity, later rule wins) and reset position back to relative
   mid-animation. */
.season-gw.season-gw--float,
.season-gw.season-gw--float.is-open {
  position: fixed;
  z-index: 60;
  margin: 0;
  flex: 0 0 auto;
  background: var(--color-surface);
}

/* The accent chrome is its OWN class, deliberately not .is-open. .is-open
   carries LAYOUT (the summary's positioning, above) — `position` cannot be
   transitioned, so it can only be dropped at the very start of phase 3. The
   chrome has to come off at the START of collapse() instead, so its OWN 2×T
   transition-delay is what schedules the border/shadow/padding fade, rather
   than a setTimeout racing (and compounding with) that same delay. Padding
   rides with the chrome so the box lands on the column's own geometry when
   collapsed. */
.season-gw--float.season-gw--chrome {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 1px var(--color-accent), var(--shadow-raised);
  padding: 8px 10px;
}

/* The panel's own content, faded in as phase 3 — opacity only, so it can sit
   behind the height/width phases without ever affecting their geometry. */
.season-gw__body {
  opacity: 0;
  transition: opacity var(--season-t) ease;
}

.season-gw__body--in { opacity: 1; }

/* The "GW n" title lives HERE, inside .season-gw__body, not as a retitled
   copy of .season-gw__n — see columnHTML()'s own comment. Each fades with the
   group it belongs to instead of staying lit while its surroundings change. */
.season-gw__title {
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--color-accent);
  line-height: 1.4;
  margin-bottom: 6px;
}

.season-lab {
  display: block;
  font-size: 0.59375rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--color-muted);
  font-weight: 700;
  margin: 0 0 5px;
}

.season-plist + .season-lab,
.season-fxrow + .season-lab {
  margin-top: 10px;
}

/* One row per top matchup — the panel's equivalent of a collapsed tile, now
   with both sides named. Band modifiers reuse the SAME tokens as
   .season-gw__tile--* above, so a fixture reads the same colour collapsed and
   open. */
.season-fxrow {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: var(--font-size-xs);
  padding: 4px 7px;
  border-radius: var(--radius-sm);
  margin-bottom: 3px;
  white-space: nowrap;
}

.season-fxrow--great   { background: var(--band-great-bg); }
.season-fxrow--good    { background: var(--band-good-bg); }
.season-fxrow--neutral { background: var(--band-neutral-bg); }
.season-fxrow--tough   { background: var(--band-tough-bg); }
.season-fxrow--brutal  { background: var(--band-brutal-bg); }
/* The top tier carries its gold edge and glow here too — this row is the same
   fixture as the ribbon tile that glows above it, and the panel was the one
   place that treatment was missing. `outline` rather than `border`, matching
   --pp below: .season-fxrow has no border, so a real one would make only the
   80+ rows 2px taller and break the panel's row rhythm. The score lifts to
   silver for the reason every other 80+ number does — see
   --band-excellent-ink in base.css. The other six tiers stay flat fills; a
   panel where every row glowed would say nothing. */
.season-fxrow--excellent {
  background: var(--band-excellent-bg);
  outline: 1px solid var(--band-excellent);
  outline-offset: -1px;
  box-shadow: 0 0 7px color-mix(in srgb, var(--band-excellent) 40%, transparent);
}

.season-fxrow--excellent .season-sc {
  color: var(--band-excellent-ink);
  text-shadow:
    0 0 6px color-mix(in srgb, var(--band-excellent-ink) 55%, transparent),
    0 0 12px color-mix(in srgb, var(--band-excellent) 50%, transparent);
}

/* Carries its edge here for the same reason the top tier carries its gold
   one: this row is the same fixture as the tile above it, and a black row with
   no outline would read as an empty gap in the panel. `outline` rather than
   `border`, matching --excellent and --pp — .season-fxrow has no border, so a
   real one would make only these rows 2px taller. */
.season-fxrow--extreme {
  background: var(--band-extreme-bg);
  outline: 1px solid var(--band-extreme-ink);
  outline-offset: -1px;
}

.season-fxrow--extreme .season-sc { color: var(--band-extreme-ink); }

.season-fxrow--pp {
  background: var(--band-red-bg);
  outline: 1px solid var(--band-red);
  outline-offset: -1px;
}

.season-fxrow--pp .season-side__t { color: var(--color-muted); }

/* Primitive red — "postponed" is a fact about the fixture, not a rating. */
.season-pptag {
  font-size: 0.53125rem;
  font-weight: 700;
  color: var(--band-red);
  letter-spacing: 0.04em;
}

.season-dgwtag {
  font-size: 0.53125rem;
  font-weight: 700;
  color: var(--color-accent);
  letter-spacing: 0.04em;
}

/* One club within a .season-fxrow: badge + short name. --fav rings whichever
   side buildGameweekMatchups picked as favoured (m.favouredId) — the same
   composite comparison that chose the row's own band, not a second rule. */
.season-side {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 4px 1px 1px;
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
}

.season-side--fav {
  border-color: var(--band-light-green);
  background: var(--band-light-green-bg);
}

.season-side--fav .season-side__t {
  color: var(--band-light-green);
  font-weight: 700;
}

.season-side__t { font-size: 0.6875rem; }

.season-badge {
  width: 17px;
  height: 17px;
  object-fit: contain;
  flex: none;
  border-radius: 50%;
  background: var(--color-surface-raised);
}

.season-v {
  color: var(--color-muted);
  font-size: 0.625rem;
}

.season-sc {
  margin-left: auto;
  font-size: 0.6875rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--color-muted);
}

.season-plist {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.season-prow {
  display: grid;
  grid-template-columns: 22px 1fr auto auto;
  gap: 7px;
  align-items: center;
  white-space: nowrap;
  font-size: 0.71875rem;
  padding: 3px 7px;
  border-radius: var(--radius-sm);
  background: var(--color-surface-raised);
  border: 1px solid transparent;
}

/* SEASON_STANDOUT_PLAYERS rows (playerRowHTML) — the panel-row half of what
   .season-gw__dot--standout and .season-key__swatch--standout already
   promise, so a standout reads the same wherever it appears. Kept to a
   border tint plus the light-green name/points, deliberately without a
   filled background — a row highlight is emphasis, not the alert-red
   .season-gw__tile--brutal fill uses elsewhere on the strip. */
.season-prow--hi { border-color: color-mix(in srgb, var(--band-light-green) 45%, transparent); }
.season-prow--hi .season-prow__nm { color: var(--band-light-green); }

.season-prow__pos {
  font-size: 0.53125rem;
  font-weight: 700;
  color: var(--color-muted);
}

.season-prow__nm {
  overflow: hidden;
  text-overflow: ellipsis;
}

.season-prow__px {
  font-size: 0.625rem;
  color: var(--color-muted);
  font-variant-numeric: tabular-nums;
}

.season-prow__pts {
  font-size: 0.6875rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 0 5px;
  min-width: 30px;
  text-align: right;
}

.season-prow--hi .season-prow__pts {
  border-color: color-mix(in srgb, var(--band-light-green) 50%, transparent);
  color: var(--band-light-green);
}

.season-note {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--color-border-subtle);
  font-size: 0.6875rem;
  color: var(--color-muted);
  white-space: normal;
}
