/*
 * Catch-Up (CatchUp.js) — the PrezzBox room inside the app, and the splash
 * that opens it (CatchUpSplash.js).
 *
 * --- The room --------------------------------------------------------------
 *
 * `.prezzbox` is a brand boundary. The values are PrezzBox's own, read off
 * `puntrr_analytics/static/css/style.css` and `home.css`: the red, the
 * near-black text, the greys. Two kinds of token are set on it:
 *
 *   --pb-*       PrezzBox's palette by its own names, for rules written for
 *                the room and nothing else.
 *   --accent…    the app's *semantic* tokens, re-pointed at the red. This is
 *                what makes the boundary work: any app primitive mounted
 *                inside — a button, a pill, a focus ring — asks for --accent
 *                and gets PrezzBox's red without knowing it crossed a line.
 *                Each alpha form is restated rather than inherited, because a
 *                custom property resolves where it is *declared*: tokens.css's
 *                `--accent-soft: rgb(var(--accent-rgb) / 0.1)` was already blue
 *                on :root before this class could change the channels.
 *
 * The page ground is not remapped — PrezzBox's #f7f8fa is the app's
 * --color-bg-page already, which is a happy accident worth knowing about.
 */

.prezzbox {
  --pb-red: #e63946;
  --pb-red-bright: #ff6b6b;
  --pb-red-rgb: 230 57 70;
  --pb-bg: #f7f8fa;
  --pb-surface: #ffffff;
  --pb-border: #eceef2;
  --pb-border-strong: #e0e3ea;
  --pb-text: #111118;
  --pb-muted: #6b7280;
  --pb-faint: #9aa2ae;
  /* PrezzBox's own ease — a long settle, no overshoot. */
  --pb-ease: cubic-bezier(0.22, 1, 0.36, 1);

  --accent: var(--pb-red);
  --accent-rgb: var(--pb-red-rgb);
  --accent-soft: rgb(var(--pb-red-rgb) / 0.1);
  --accent-tint: rgb(var(--pb-red-rgb) / 0.07);
  --accent-pressed: #c92f3b;
  --ring: var(--pb-red);
  --text: var(--pb-text);
}

/*
 * The room in the dark theme. PrezzBox's site has no dark palette to read
 * off, so the room borrows the app's own: the same ground, cards, hairlines
 * and greys as every other dark screen, with PrezzBox's red kept as the
 * accent. The red is the brand; the greys were only ever the page. The
 * palette names are used rather than the semantic ones because `--text` is
 * itself re-pointed at `--pb-text` above, and the pair would be a cycle.
 */
:root[data-theme="dark"] .prezzbox {
  --pb-bg: var(--color-bg-page);
  --pb-surface: var(--color-bg-card);
  --pb-border: var(--color-border-subtle);
  --pb-border-strong: var(--color-gray-strong);
  --pb-text: var(--color-text-primary);
  --pb-muted: var(--color-text-muted);
  --pb-faint: var(--color-gray-mid);
  /* A flat 7% of the red disappears into a dark card; 14% reads as a tint. */
  --accent-tint: rgb(var(--pb-red-rgb) / 0.14);
}

.catch-up {
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--pb-bg);
  color: var(--text);
}

/* The content layer the room's screens fill. It rises through the splash's
 * exit (below) the first time; after that it is just the page. */
.catch-up__content {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* The room has no bar of its own yet, so the content clears the top inset
   * itself (layout.css names the panes that do not). */
  padding-top: calc(var(--space-4) + var(--safe-top));
  padding-bottom: var(--space-6);
  /* Between the room's sections — the snapshot, the highlights, the leaders. */
  gap: var(--space-6);
}


/* --- Game Snapshot ---------------------------------------------------------- */

/*
 * The room's first screen (GameSnapshot.js): one match as a peeking row of
 * cards — the match itself first, then blank cards for the other looks at
 * it that are still being decided. The row is the app's Carousel; the card
 * is the club page's Next match card in its bones (competition line, team /
 * centre / team), one rung up in every step, drawn here so the room can
 * tune it without reaching into club-overview.css, and watermarked with the
 * advanced bet builder's hairlines (`.adv-title__mark`, advanced-markets.css
 * — same lines, same stroke). Every colour is a semantic token, which
 * inside `.prezzbox` means the accent is PrezzBox's red.
 */

.game-snapshot {
  /* The shape of every player card's body: the heatmap's pitch with four
     units of grass on each side, so the pitch spans the card end to end
     (Tobi, 2026-09-23: "make the pitch span end to end… more emphasised").
     The field is 342 x 149 (`HEAT_LENGTH`, SnapshotStats.js — change the two
     together): the source's 238 made the row 60px taller than the compact
     one, and Tobi asked for the height back, so the field is drawn longer
     rather than the pitch squashed. The margin is in the pitch's units, not
     pixels, so the ratio holds at every width. */
  --snapshot-body-ratio: 350 / 157;

  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/*
 * The card's share of the row, and the first half of the room's own scale.
 *
 * It went 94% → 66% → 72% → 84% → 88% → 97% across one afternoon (Tobi,
 * 2026-09-22: 6% shorter, then "too big — 30%", then "a bit shorter and
 * slightly wider", then "10% bigger and wider relative to its height, a
 * landscape type of thing", then 5% more, then 10%). At 97% the sliver of
 * the next card is about a dozen pixels — the peek is nearly spent, and
 * the next width the room asks for has to come from the track's own
 * gutters instead. Both dimensions move together whether or not that is
 * wanted: every graphic in the row is a landscape pitch, a goal or a
 * half-pitch whose *height* is derived from the width it is given, so a
 * card cut in height alone leaves each of them a small rectangle centred
 * in a card still nearly as wide as the screen, and a card widened grows
 * taller unless something inside gives the height back. What gave it back
 * for the landscape shape was the header: the title row folded into the
 * head (SnapshotStats.js), which is a row of chrome a wide card no longer
 * has to spend.
 *
 * The track's snap, gutters and dots are untouched; only the card's share
 * of the row is the room's call. Written against the peeking selector in
 * full, since that is the rule it has to outweigh.
 */
.game-snapshot .carousel[data-peek="true"] .carousel__track > * {
  width: 97%;
}

/* The section's name, in the page gutter the carousel's own track keeps. */
.game-snapshot__title {
  margin: 0;
  padding-inline: var(--space-4);
  font-size: var(--text-md);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
  color: var(--text);
}

/*
 * The lead slot the match card sits in — a plain wrapper so the card can be
 * swapped (skeleton → match → note) without the track losing its first
 * child. It stretches with the row, and the card fills it, so a blank card
 * beside it is exactly as tall as the match card is.
 */
.game-snapshot__lead {
  display: flex;
}

/* A stat card with nothing to draw is out of the row, not a blank in it
   (GameSnapshot.js, `showStats`). Stated, because the display above
   outranks the `hidden` attribute's own. */
.game-snapshot__lead[hidden] {
  display: none;
}

.game-snapshot__lead > .game-snapshot__card {
  flex: 1;
  min-width: 0;
}

/*
 * The card's chrome, shared by every state: the app's card surface, border
 * and shadow, a step roomier than the club page's card. A button in the
 * match state, a div otherwise — the reset covers both. No flex of its own:
 * a blank card is a direct child of the track, whose geometry
 * (carousel.css) it must not override. The clip is for the rounded corners:
 * the heatmap's terrain runs close to them.
 */
.game-snapshot__card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  /* Even on every side, and the same on every card — the match and the
     player cards alike (2026-09-23; the player cards had been a step
     tighter, which put their graphics against the rounded corners). */
  padding: var(--space-3);
  /* One height for every card in the row, whatever it holds: every player
     card is head + a body of one shape (`--snapshot-body-ratio`) + foot, so
     they come out equal, and the row's stretch carries the match card to
     the same height. The floor is for a row with no player card in it —
     every slot a sentence — so it still reads as the screen's subject. */
  min-height: calc(171px * var(--scale));
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  color: var(--text);
  font: inherit;
  text-align: inherit;
  appearance: none;
  -webkit-tap-highlight-color: transparent;
}

/*
 * The match state is the tap target — the whole card, since nothing inside
 * it goes anywhere but the fixture — and takes the app's press language
 * (`pressable`, interactions.css) like any other pressed card. Its ground
 * is the plain surface: a wash of the two clubs' colours was tried under
 * the hairlines and taken out the same day (2026-09-22).
 *
 * Its content is centred in it, rather than a header at the top with the
 * scoreline under it (Tobi, 2026-09-22, when the scorer lists came out and
 * left the card top-heavy). The competition line is part of that block and
 * centres with it — there is no edge for it to sit against any more.
 */
.game-snapshot__card--match {
  justify-content: center;
  gap: var(--space-4);
  cursor: pointer;
}

.game-snapshot__card--match .game-snapshot__head {
  justify-content: center;
}

.game-snapshot__head,
.game-snapshot__row,
.game-snapshot__note {
  position: relative;
}

/* --- Head: competition, round -------------------------------------------- */

.game-snapshot__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  min-height: var(--mark-xs);
}

.game-snapshot__competition {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-width: 0;
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
}

.game-snapshot__competition-name {
  min-width: 0;
}

.game-snapshot__round {
  flex: none;
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
}

/* --- Row: team, centre, team --------------------------------------------- */

/*
 * Crest, scoreline, crest. One row now that the scorer lists have gone
 * (2026-09-22), and sized by its own content: the card centres the whole
 * block instead of this stretching to fill it, which is what a scoreboard
 * with nothing under it wants.
 */
.game-snapshot__row {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: start;
  gap: var(--space-2);
  padding-block: var(--space-1);
}

.game-snapshot__team {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-15);
  min-width: 0;
  text-align: center;
}

/* Two lines at most, the club news card's own clamp: a name is said in
   full, and the row's three columns stay level because both sides reserve
   the same two lines. */
.game-snapshot__team-name {
  display: -webkit-box;
  max-width: 100%;
  min-height: calc(var(--text-sm) * var(--leading-snug) * 2);
  overflow: hidden;
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
  color: var(--text);
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.game-snapshot__centre {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  /* Sat on the crest's centre line, so the score reads level with the
     badges rather than with the names under them. */
  padding-top: calc((var(--mark-3xl) - var(--text-5xl) * var(--leading-tight)) / 2);
  padding-inline: var(--space-2);
}

/* The fixture page's scoreline size — the card is the match at the page's
   own scale, one swipe from the page itself. */
.game-snapshot__score,
.game-snapshot__time {
  font-size: var(--text-5xl);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.game-snapshot__score {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
}

.game-snapshot__score-dash {
  color: var(--text-muted);
}

/*
 * Where the match is: a clock, "Full-time", or the day. The clock takes
 * `--highlight`, the match row's own live colour, and its prime blinks on
 * the shared `.live-prime` beat (interactions.css).
 */
.game-snapshot__state {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--text-secondary);
  white-space: nowrap;
}

/* A shoot-out or extra time under the state (`centre`, GameSnapshot.js):
   a step smaller and quieter, a caption on the result. */
.game-snapshot__note {
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  color: var(--text-muted);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.game-snapshot__state--live {
  color: var(--highlight);
  font-weight: var(--weight-bold);
}

/* --- Skeleton, note -------------------------------------------------------- */

/* The card's own shape in grey while the seam is out, so the row does not
 * change height when the match lands. */
.game-snapshot__skeleton-line {
  display: block;
  width: 45%;
  height: var(--text-sm);
}

.game-snapshot__skeleton-crest {
  display: block;
  width: var(--mark-3xl);
  height: var(--mark-3xl);
  /* The name's two lines under the crest, as space. */
  margin-bottom: calc(var(--text-sm) * var(--leading-snug) * 2 + var(--space-15));
}

.game-snapshot__skeleton-score {
  display: block;
  width: calc(96px * var(--scale));
  height: var(--text-5xl);
}

/* A sentence in the card's place — no match yet, or a refusal — centred
   in the card's box, which the shared floor already sizes. */
.game-snapshot__card--note {
  justify-content: center;
  align-items: center;
}

.game-snapshot__note {
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  text-align: center;
}


/* --- The player cards ------------------------------------------------------- */

/*
 * The three cards after the match (SnapshotStats.js): a heatmap, a shot map
 * and a ledger of figures, one player each (data/match-standouts.js).
 *
 * Every graphic in them is a painter the app already owns — `HeatCanvas`
 * for the cloud (the mini app's painter, which paints its own pitch into
 * the bitmap), `PitchSurface` and `Marker` from ShotMap.js for the attempts
 * — so what is here is the card around them. The `shot-map__*` rules the
 * markers wear are shot-map.css's and are not restated; only the box a
 * graphic sits in is this file's business.
 */

.snapshot-stat {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
  min-height: 0;
}

/*
 * The part of the card the graphic lives in: whatever the card's fixed
 * height leaves after the head and the foot, so it is the same box on
 * every card. A graphic fills it edge to edge on its own ground (below),
 * which is what makes the heat terrain, the grass and the goal read as one
 * frame in three colours rather than three differently sized pictures.
 *
 * The radius is the card's less its padding, near enough — a frame inside a
 * rounded card reads as nested when its corners follow the card's curve.
 */
.snapshot-stat__body {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  aspect-ratio: var(--snapshot-body-ratio);
  min-height: 0;
  min-width: 0;
  border-radius: var(--radius-sm);
  overflow: hidden;
}

/*
 * The line under the body: a highlight sentence, the shot chips, or nothing
 * at all. Reserved either way — one line's height whatever is in it —
 * because a card without a sentence must not be shorter than one with it,
 * or the row would resize as it is swiped.
 */
.snapshot-stat__foot {
  display: flex;
  flex: none;
  align-items: center;
  min-height: calc(var(--text-xs) * var(--leading-normal) + var(--space-1));
  min-width: 0;
}

/* --- Head -------------------------------------------------------------------- */

/* A round mark the height of the two lines beside it, then the pill. The
   same on every card, the called markets included (SnapshotStats.js,
   `Head`), so the body starts on one line across the row. */
.snapshot-stat__head {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--space-2);
  /* The name and caption's two lines, which stand a hair taller than the
     mark — a floor, so the loading skeleton's one-line head is the same
     height and the row does not twitch when the players land. */
  min-height: calc((var(--text-sm) + var(--text-xs)) * var(--leading-snug) + var(--space-05));
  min-width: 0;
}

/* The called markets' mark, where a player card has its face. */
.snapshot-stat__badge {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--mark-lg);
  height: var(--mark-lg);
  border-radius: var(--radius-full);
  background: var(--accent-soft);
  color: var(--accent);
}

.snapshot-stat__who {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-05);
  min-width: 0;
}

.snapshot-stat__name {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
  color: var(--text);
}

.snapshot-stat__team {
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  line-height: var(--leading-snug);
  font-variant-numeric: tabular-nums;
}

/*
 * The rating, in the pill the pitch and the season card already wear — the
 * same tiers off `ratingTier` (PitchLineup.js), so a 7.6 is the same green
 * wherever it is read. Not `.season-stats__rating` itself: that one is
 * absolutely positioned onto a player node's corner, and this one stands in
 * a row. Only the colours are shared, through the tokens.
 */
.snapshot-stat__rating {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: calc(30px * var(--scale));
  padding: var(--space-05) var(--space-15);
  border-radius: var(--radius-full);
  color: var(--text-inverse);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
  background: var(--rating-none-solid);
}

.snapshot-stat__rating[data-tier="high"] {
  background: var(--rating-high-solid);
}

.snapshot-stat__rating[data-tier="mid"] {
  background: var(--rating-mid-solid);
}

.snapshot-stat__rating[data-tier="low"] {
  background: var(--rating-low-solid);
}

/*
 * What the feed says this player led the match in. One line, clipped rather
 * than wrapped: the sentence is a bonus under the graphic, and a card whose
 * height depends on how many words a highlight happens to have would make
 * the row jump as it is swiped.
 */
.snapshot-stat__foot .snapshot-stat__note {
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --- Heatmap ------------------------------------------------------------------ */

/*
 * The mini app's `HeatmapPitch` widget (SnapshotStats.js, `HeatCard`), its
 * stylesheet ported from `puntrr-mini-app/styles/heatmap-pitch.css` under
 * this block's names: the green terrain, the arrow over it, and the frame
 * the cloud and the markings stack in.
 *
 * The green is the source's `#1f6b3a` under its own white top-light, and it
 * is a literal rather than a token on purpose — this is one widget's
 * livery, borrowed whole, and pinning it to `--pitch` would make the next
 * person to retune the app's neutral pitches change a picture that is not
 * theirs. The same goes for the ink the markings and the arrow are drawn in.
 */
/* The terrain is the whole body, and the body is the pitch's own shape plus
   a thin even edge of grass (`--snapshot-body-ratio`), so the pitch runs the
   card end to end at its true 238/149 — never stretched, or the centre
   circle would tell on it at once. */
.snapshot-stat__pitch {
  position: relative;
  display: block;
  align-self: stretch;
  width: 100%;
  border-radius: inherit;
  background:
    linear-gradient(180deg, rgb(255 255 255 / 0.04), transparent 40%),
    #1f6b3a;
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.2);
}

/*
 * Which way he is playing. The source turns the arrow and the stage together
 * for an away player — home attacks right, away attacks left — rather than
 * mirroring the cloud, so both rotations are written against the same
 * attribute and can never disagree.
 */
.snapshot-stat__arrow {
  position: absolute;
  top: var(--space-1);
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  color: #0a0a0a;
  pointer-events: none;
}

/* The source draws it at 66 x 20, which is most of a card this size. Scaled
   by its width, the height following, so the glyph keeps its proportions. */
.snapshot-stat__arrow svg {
  display: block;
  width: calc(44px * var(--scale));
  height: auto;
}

.snapshot-stat__pitch[data-side="away"] .snapshot-stat__stage,
.snapshot-stat__pitch[data-side="away"] .snapshot-stat__arrow {
  transform: rotate(180deg);
}

/* The pitch's own proportions (238 x 149), which the markings are drawn on
   and stretched to; the canvas fills the same box. The terrain less its four
   units of grass a side — top and bottom as a share of the height, the sides
   of the width, which on a body of `--snapshot-body-ratio` is 342 x 149. */
.snapshot-stat__frame {
  position: absolute;
  inset: calc(100% * 4 / 157) calc(100% * 4 / 350);
  display: block;
}

.snapshot-stat__stage {
  position: absolute;
  inset: 0;
  display: block;
}

.snapshot-stat__stage > * {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

/* The arrow is the one thing in the frame that is not a full-bleed layer:
   it keeps the top and the two sides it is centred between, and gives up
   only the height. */
.snapshot-stat__frame > .snapshot-stat__arrow {
  bottom: auto;
  height: auto;
}

/* Transparent: the heat is all the canvas draws, and the green under it is
   the box's. */
.snapshot-stat__generation {
  padding: 0.5px;
  border-radius: 2px;
  background: transparent;
}

/* Over the cloud, and never in the way of it. The source's 0.72 is on the
   drawing as a whole rather than in the ink: the longer field is drawn as
   overlapping strokes (HeatCloud.js, `longPitch`), and an ink with its own
   alpha would darken every join. */
.snapshot-stat__lines {
  pointer-events: none;
  color: rgb(8 12 10);
  opacity: 0.72;
}

/* --- Shot map ----------------------------------------------------------------- */

/*
 * The attacking half with his attempts on it. The markers are ShotMap.js's
 * own (`Marker`), so they carry `shot-map__*` classes and shot-map.css
 * dresses them; this is the box, which is sized by the graphic's own aspect
 * rather than given a height.
 *
 * `--shot-colour` has to be declared here. Every marker rule reads it, and
 * on the fixture page it resolves on `.shot-map` — the element this card
 * does without, since it draws no toggle, no goal panel and no selection.
 * Undeclared, the stroke is invalid and the discs come out unpainted. A
 * card is about one player, so it is his side's colour, from the pair the
 * Statistics bars already use.
 */
.snapshot-stat__shotmap {
  --shot-colour: var(--stat-home);

  display: flex;
  align-self: stretch;
  width: 100%;
  /* The grass's own colour (shot-map.css, `.shot-map__grass`), so the
     bleed round the drawing reads as run-off rather than as a white band. */
  background: var(--pitch-deep);
}

.snapshot-stat__shotmap[data-side="away"] {
  --shot-colour: var(--stat-away);
}

/* The graphic fills the body rather than setting it: `PitchSurface({ fill })`
   draws `xMidYMin slice`, so given both dimensions it covers them from the
   goal line down. */
.snapshot-stat__shotmap .shot-map__pitch {
  display: block;
  width: 100%;
  height: 100%;
}

/*
 * The line each attempt travelled (SnapshotStats.js, `Trail`), and the bar
 * across it where one was charged down.
 *
 * In the shot's own colour rather than the near-black the fixture page's
 * *selection* wears: there, one trajectory is an annotation picking one shot
 * out of several, and near-black is what says "this one". Here every attempt
 * has a line, so near-black four times over would be an annotation of
 * nothing and would read louder than the markers it belongs to.
 *
 * The line is faint and the bar is not. A trajectory is context — where the
 * ball was heading — while the bar is the event: it is the moment the shot
 * was stopped, and it is the only mark on the pitch that says a defender was
 * there.
 */
.snapshot-stat__trail {
  fill: none;
  stroke: var(--shot-colour);
}

.snapshot-stat__trajectory {
  opacity: 0.55;
}

/*
 * How the chances fell, in words — the "extra sauce": a penalty, a header
 * off a corner and a left-footed fast break are three different shots, and
 * three identical discs say none of it. One row, clipped if the chips run
 * past the card, because a second row would change the card's height.
 */
.snapshot-stat__types {
  display: flex;
  gap: var(--space-15);
  min-width: 0;
  overflow: hidden;
}

.snapshot-stat__chip {
  flex: none;
  padding: var(--space-05) var(--space-2);
  border-radius: var(--radius-full);
  background: var(--surface-sunken);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  white-space: nowrap;
}

/* --- Goalkeeper ---------------------------------------------------------------- */

/*
 * The goal, with a glove where each save crossed the line, and the keeper's
 * figures under it (SnapshotStats.js, `KeeperCard`).
 *
 * A column: the goal takes whatever the strip of figures leaves of the body,
 * and the strip keeps its one row.
 */
.snapshot-stat__keeper {
  display: flex;
  flex-direction: column;
  align-self: stretch;
  gap: var(--space-2);
  width: 100%;
  min-width: 0;
}

/*
 * The goal's frame: the same rounded box as the terrain and the grass, on
 * the sky's own colour. The panel is drawn filled (`GoalPanel({ fill })`),
 * so the goal stands the box's full height at its centre and the sky and
 * the turf run out to the box's sides.
 */
.snapshot-stat__goal {
  --shot-colour: var(--stat-home);

  display: flex;
  flex: 1;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
}

.snapshot-stat__goal[data-side="away"] {
  --shot-colour: var(--stat-away);
}

.snapshot-stat__goal .shot-map__goal {
  display: block;
  width: 100%;
  height: 100%;
}

/*
 * The glove itself. `MouthDot`'s expressive mark draws the glyph with no
 * fill of its own — the fixture page's floodlit replay paints it white
 * against a dark net (fixture-commentary.css), and this panel is the light
 * one, so here it takes the keeper's colour on the disc the mark already
 * sits on.
 */
.snapshot-stat__goal .shot-map__mouth-glyph {
  fill: var(--shot-colour);
}

/*
 * The marks are sized for the uncropped panel, where the goal is a third of
 * the width; at this framing they arrive three times over and a busy
 * afternoon becomes one blob. Scaled down here rather than parameterised in
 * ShotMap.js, because it is this framing that makes them too big and not
 * the mark that is wrong.
 *
 * On the inner group, which is the one free of the placement: the outer
 * carries a `transform` attribute and a CSS transform would replace it
 * rather than compose with it (see `MouthDot`). `fill-box` then makes
 * `center` the disc's own centre, so the mark shrinks in place instead of
 * sliding toward the corner.
 */
.snapshot-stat__goal .shot-map__mouth-mark {
  transform: scale(0.62);
  transform-box: fill-box;
  transform-origin: center;
}

/* --- Ledger -------------------------------------------------------------------- */

/*
 * The figures in a column — the mini app's recap rows in this app's type.
 * A `<dl>`: each row is a term and its value, which is what a screen reader
 * should hear rather than two unrelated strings. Hairlines between the rows
 * and none at the ends, so the block reads as a ledger and not as a table
 * boxed off from the card.
 *
 * The rows spread over the body's height rather than stacking at its top:
 * five figures and a graphic of the same height should look like two cards
 * of the same weight, which they do not if one of them stops halfway.
 */
.snapshot-stat__ledger {
  display: flex;
  flex: 1;
  flex-direction: column;
  justify-content: space-between;
  align-self: stretch;
  margin: 0;
}

.snapshot-stat__figure {
  display: flex;
  flex: 1;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding-block: var(--space-05);
}

.snapshot-stat__figure + .snapshot-stat__figure {
  border-top: 1px solid var(--border);
}

.snapshot-stat__figure-label {
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
}

.snapshot-stat__figure-value {
  margin: 0;
  color: var(--text);
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
}

/*
 * The keeper's figures, all across one row under the goal. Value over label,
 * each centred in an equal column, and no rules between them: side by side
 * they read as a strip of facts, and a hairline between each would make it a
 * table of one row. `column-reverse` puts the value first on screen while
 * the `<dl>` keeps the term before it for a screen reader.
 */
.snapshot-stat__ledger--strip {
  display: grid;
  flex: none;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--space-2);
  align-content: center;
}

.snapshot-stat__ledger--strip .snapshot-stat__figure {
  flex: none;
  flex-direction: column-reverse;
  align-items: center;
  gap: 0;
  padding-block: 0;
  min-width: 0;
}

.snapshot-stat__ledger--strip .snapshot-stat__figure + .snapshot-stat__figure {
  border-top: none;
}

.snapshot-stat__ledger--strip .snapshot-stat__figure-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

/* --- Called markets, and SureSCOUT's tag --------------------------------------- */

/*
 * What Puntrr said before the match, and whether it came in
 * (SnapshotStats.js, `CallsCard`; data/snapshot-calls.js).
 *
 * The verdict is the only colour on either of these. A called market is not
 * a status the way a slip's leg is — nothing here is won or lost money — so
 * the row stays in the card's own ink and the tick or the cross carries the
 * whole signal, which is also what keeps four rows of them from reading as
 * a traffic light.
 */
.snapshot-stat__calls {
  display: flex;
  flex: 1;
  flex-direction: column;
  justify-content: space-between;
  align-self: stretch;
  margin: 0;
  padding: 0;
  list-style: none;
}

.snapshot-stat__call {
  display: flex;
  flex: 1;
  align-items: center;
  gap: var(--space-3);
  padding-block: var(--space-05);
  min-width: 0;
}

.snapshot-stat__call + .snapshot-stat__call {
  border-top: 1px solid var(--border);
}

/* Two snug lines: four of these rows share the same body the other cards'
   graphics fill, so the row is what the body can give it and no more. */
.snapshot-stat__call-body {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-width: 0;
  line-height: var(--leading-snug);
}

.snapshot-stat__call-market {
  color: var(--text-muted);
  font-size: var(--text-2xs);
  font-weight: var(--weight-medium);
}

.snapshot-stat__call-line {
  color: var(--text);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

/* How sure the model was. Tabular and muted: it is the row's supporting
   figure, not its verdict. */
.snapshot-stat__call-confidence {
  flex: none;
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
}

/*
 * The mark. `--success` / `--error` and not the form or rating tokens: this
 * is a claim that came true or did not, which is the same kind of statement
 * a settled slip leg makes, and the two should not read differently.
 */
.snapshot-stat__verdict {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: calc(13px * var(--scale));
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  line-height: var(--leading-none);
}

.snapshot-stat__verdict[data-outcome="won"] {
  color: var(--success-strong);
}

.snapshot-stat__verdict[data-outcome="lost"] {
  color: var(--error-strong);
}

/*
 * SureSCOUT's tag on a player card. Its own chip rather than a line of text,
 * because it is a different voice from everything else on the card: the
 * figures are what happened, and this is what was said beforehand.
 */
.snapshot-stat__tag {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-15);
  max-width: 100%;
  min-width: 0;
  padding: var(--space-05) var(--space-2);
  border-radius: var(--radius-full);
  background: var(--surface-sunken);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
}

/* The product's name, in the accent — the one place on these cards a brand
   speaks, and inside the room the accent is PrezzBox's red. */
.snapshot-stat__tag-mark {
  flex: none;
  color: var(--accent);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-snug);
}

.snapshot-stat__tag-call {
  min-width: 0;
  color: var(--text-secondary);
}

/* Smaller inside the chip than in a called row, where it is the row's whole
   right-hand column. */
.snapshot-stat__tag .snapshot-stat__verdict {
  min-width: 0;
  font-size: var(--text-sm);
}

/* The foot holds the tag and the highlight line side by side; the line is
   what gives way when there is not room for both. */
.snapshot-stat__foot {
  gap: var(--space-2);
}

/* --- A stat card's skeleton ---------------------------------------------------- */

/* The face and the block below it, in grey, in the same places the card's
   own head and body stand, so the row does not resize when the players
   land. */
.game-snapshot__skeleton-face {
  display: block;
  flex: none;
  width: var(--mark-lg);
  height: var(--mark-lg);
}

.game-snapshot__skeleton-block {
  display: block;
  flex: none;
  aspect-ratio: var(--snapshot-body-ratio);
  border-radius: var(--radius-sm);
}


/* --- Ported from the mini app: Player Highlights and Stats Leaders ----------- */

/*
 * The mini app's own UI, 1:1 (Tobi, 2026-09-22: "use the exact UI from
 * puntrr-mini-app"): the "PLAYER HIGHLIGHTS" and "STATS LEADERS" blocks of
 * `puntrr-mini-app/styles/catch-up.css`, with its `.cu-section` and
 * `.cu-section-title`, rule for rule under the same `cu-*` names
 * (PlayerHighlights.js, StatsLeaders.js write the same markup). On
 * 2026-09-23 both were given the Game Snapshot's tidy-up at Tobi's asking —
 * one card shell (`--cu-card-*`), nested corners, the leaders' rows on one
 * grid, the section titles and gutters the room's own — and the notes on
 * those rules say what moved. Beyond that, two things are translated:
 *
 *   - Its theme tokens. The mini app follows the reader's light or dark
 *     scheme (`styles/theme.css`); the room is light, so `.cu-section`
 *     declares the light theme's values under the mini app's own names
 *     (--slab, --slate, --chalk, …) for these rules to read. They reach
 *     nothing outside the two sections.
 *   - Its lengths. Every one is the source's number, multiplied by the app's
 *     one density knob (`--scale`), so the port sits at the same density as
 *     everything around it — as the source sits among its own pages. The
 *     numbers are not the app's ladder steps (a 17px name, a 22px padding, a
 *     30px corner have no step), which is what the scale-lint fence below
 *     records: a port's absolutes, like the splash's spec. Hairline borders
 *     stay as the source has them.
 *
 * The faces are the source's — Barlow, Bricolage Grotesque, Space Mono —
 * linked in index.html for these rules alone. Its section entrance
 * animation (`slideUp` on `.cu-section`) is left out: the room's content
 * already rises in through the splash's exit, and two entrances would stack.
 */

/* scale-lint: off */

.cu-section {
  --blue: #0d4ff7;
  --slab: #ffffff;
  --slate: #f0f0f0;
  --chalk: #3b3b3b;
  --ghost: #a1a1a1;
  --dim: #e9e9e9;
  --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.03);
  --card-shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.04);
  --font: "Barlow", sans-serif;
  --font-display: "Bricolage Grotesque", "Barlow", sans-serif;
  --font-mono: "Space Mono", ui-monospace, monospace;
  /* The app's own face (base.css), for the section titles only. */
  --font-app: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /*
   * One shell for both sections' cards (2026-09-23, the Game Snapshot's
   * tidy-up carried over): the same width, corner, padding and gap, and every
   * panel inside a card rounded by the card's corner less its padding, so the
   * curves nest instead of each panel keeping the source's own radius. The
   * source's cards were three widths, three corners and four paddings.
   */
  --cu-card-width: min(calc(300px * var(--scale)), 82vw);
  --cu-card-radius: calc(24px * var(--scale));
  --cu-card-pad: calc(12px * var(--scale));
  --cu-panel-radius: calc(12px * var(--scale));
  --cu-gap: calc(8px * var(--scale));
  /* The room's page gutter — the Game Snapshot's track keeps the same one. */
  --cu-gutter: calc(16px * var(--scale));

  /* The source's page: its body face and colour, Bootstrap's line height,
     and the feed's side padding (`.cu-feed`). */
  padding-inline: var(--cu-gutter);
  font-family: var(--font);
  line-height: var(--leading-relaxed);
  color: var(--chalk);
}

.cu-section[hidden] {
  display: none;
}

/* The Game Snapshot's title exactly (`.game-snapshot__title`): the room's
   three sections are headed alike and start on the same gutter. */
.cu-section-title {
  font-family: var(--font-app);
  font-size: var(--text-md);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-normal);
  color: var(--text);
  margin: 0 0 var(--space-3);
}

/*
 * The rows run to the screen's edges and scroll under them, the way the Game
 * Snapshot's carousel does, with the first card snapped to the gutter — it
 * was snapping to the row's own edge, three pixels left of every title.
 */
.cu-hl-scroll,
.cu-sl-scroll,
.cu-sl-chips {
  margin-inline: calc(-1 * var(--cu-gutter));
  padding-inline: var(--cu-gutter);
  scroll-padding-inline: var(--cu-gutter);
}

/* --- Player Highlights --- */

/* Even padding on every side, and the lighter of the source's two shadows —
   the one its Stats Leaders cards wear, so the two sections' cards sit on the
   page at the same depth. */
.cu-hl {
  background: #f4f4f4;
  border: 1px solid #e4e4e4;
  border-radius: var(--cu-card-radius);
  padding: var(--cu-card-pad);
  display: flex;
  flex-direction: column;
  gap: var(--cu-gap);
  box-shadow: var(--card-shadow);
  width: var(--cu-card-width);
  max-width: 100%;
  box-sizing: border-box;
  flex-shrink: 0;
}

.cu-hl-league {
  display: flex;
  align-items: center;
  gap: calc(8px * var(--scale));
  padding: 0 calc(2px * var(--scale));
}
.cu-hl-league img {
  width: calc(18px * var(--scale));
  height: calc(18px * var(--scale));
  object-fit: contain;
  border-radius: 50%;
  background: transparent;
  flex-shrink: 0;
}
.cu-hl-league-ph {
  width: calc(18px * var(--scale));
  height: calc(18px * var(--scale));
  border-radius: 50%;
  background: #ddd;
  flex-shrink: 0;
}
.cu-hl-league span {
  font-size: calc(12px * var(--scale));
  font-weight: 700;
  color: #3a3a3a;
}

.cu-hl-player {
  display: flex;
  align-items: center;
  gap: calc(12px * var(--scale));
  background: #ffffff;
  border-radius: var(--cu-panel-radius);
  padding: var(--cu-card-pad);
}
/* The height of the three lines beside it — name, club, opponent. */
.cu-hl-avatar {
  width: calc(56px * var(--scale));
  height: calc(56px * var(--scale));
  border-radius: 50%;
  object-fit: cover;
  background: #ececec;
  flex-shrink: 0;
}
.cu-hl-player-info {
  flex: 1;
  min-width: 0;
}
.cu-hl-name {
  font-size: calc(15px * var(--scale));
  font-weight: 700;
  color: #2c2c2c;
  letter-spacing: calc(-0.3px * var(--scale));
  line-height: var(--leading-tight);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cu-hl-sub {
  margin-top: calc(2px * var(--scale));
  font-size: calc(12px * var(--scale));
  font-weight: 500;
  color: #9a9a9a;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cu-hl-vs {
  display: flex;
  align-items: center;
  gap: calc(5px * var(--scale));
  margin-top: calc(2px * var(--scale));
  min-width: 0;
}
.cu-hl-vs img {
  width: calc(14px * var(--scale));
  height: calc(14px * var(--scale));
  object-fit: contain;
  flex-shrink: 0;
}
.cu-hl-vs span {
  font-size: calc(12px * var(--scale));
  font-weight: 600;
  color: #5a5a5a;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cu-hl-rating {
  flex-shrink: 0;
  min-width: calc(44px * var(--scale));
  padding: calc(7px * var(--scale)) calc(8px * var(--scale));
  border-radius: calc(8px * var(--scale));
  background: #00e676;
  color: #ffffff;
  font-family: var(--font-mono);
  font-size: calc(14px * var(--scale));
  font-weight: 800;
  text-align: center;
  font-variant-numeric: tabular-nums;
  line-height: var(--leading-none);
}
.cu-hl-rating.is-top {
  background: var(--blue);
}

/* `flex: 1` so a card with fewer figures than its neighbours ends level with
   them — the row stretches every card to the tallest — with the button kept
   at the foot rather than the slack pooling under it. */
.cu-hl-stats {
  flex: 1;
  background: #ffffff;
  border-radius: var(--cu-panel-radius);
  padding: calc(10px * var(--scale)) var(--cu-card-pad);
  display: flex;
  flex-direction: column;
  gap: calc(8px * var(--scale));
}
.cu-hl-stat {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: calc(12px * var(--scale));
}
.cu-hl-stat-label {
  font-size: calc(12px * var(--scale));
  font-weight: 500;
  color: #9a9a9a;
}
.cu-hl-stat-value {
  font-size: calc(13px * var(--scale));
  font-weight: 700;
  color: #2c2c2c;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.cu-hl-cta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: calc(6px * var(--scale));
  width: 100%;
  /* A button, not a banner: the source's was the card's tallest element
     after the player. */
  padding: calc(8px * var(--scale)) calc(14px * var(--scale));
  border-radius: calc(100px * var(--scale));
  border: 1px solid var(--blue);
  background: rgba(13, 79, 247, 0.08);
  color: var(--blue);
  font-family: var(--font);
  font-size: calc(13px * var(--scale));
  font-weight: 700;
  line-height: var(--leading-snug);
  cursor: pointer;
  transition: transform 0.15s, opacity 0.15s, background 0.15s;
}
.cu-hl-cta svg {
  width: calc(14px * var(--scale));
  height: calc(14px * var(--scale));
}
.cu-hl-cta:active {
  transform: scale(0.98);
  opacity: 0.9;
  background: rgba(13, 79, 247, 0.14);
}

.cu-hl-rank {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: calc(11px * var(--scale));
  font-weight: 700;
  color: #8a8a8a;
}

/* Room above and below for the cards' shadows; the sides are the gutter
   (above, with the other two rows). */
.cu-hl-scroll {
  display: flex;
  gap: calc(12px * var(--scale));
  overflow-x: auto;
  padding-block: calc(4px * var(--scale)) calc(8px * var(--scale));
  scrollbar-width: none;
  scroll-snap-type: x mandatory;
}
.cu-hl-scroll::-webkit-scrollbar {
  display: none;
}

.cu-hl-scroll .cu-hl {
  flex: 0 0 auto;
  width: var(--cu-card-width);
  scroll-snap-align: start;
}

/* The compact card (`cu-hl--compact`, the one in a row) is the card now: the
   source's full-size variant was a size apart from it, and the tidy-up made
   the compact one the only size. The class stays on the markup, 1:1. */

/* --- Stats Leaders --- */

.cu-section--stats-leaders:empty {
  display: none;
}

.cu-sl-chips {
  display: flex;
  gap: calc(8px * var(--scale));
  overflow-x: auto;
  padding-block: 0;
  margin-bottom: calc(8px * var(--scale));
  scrollbar-width: none;
}
.cu-sl-chips::-webkit-scrollbar {
  display: none;
}

.cu-sl-chip {
  display: flex;
  align-items: center;
  gap: calc(6px * var(--scale));
  padding: calc(7px * var(--scale)) calc(12px * var(--scale));
  border: 1px solid var(--dim);
  border-radius: calc(100px * var(--scale));
  background: var(--slab);
  color: var(--ghost);
  font-family: var(--font);
  font-size: calc(12px * var(--scale));
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
  flex-shrink: 0;
}
.cu-sl-chip img {
  width: calc(16px * var(--scale));
  height: calc(16px * var(--scale));
  object-fit: contain;
}
.cu-sl-chip.active {
  background: var(--blue);
  border-color: var(--blue);
  color: #fff;
}
.cu-sl-chip:active {
  opacity: 0.85;
}

.cu-sl-scroll {
  display: flex;
  gap: calc(12px * var(--scale));
  overflow-x: auto;
  padding-block: calc(4px * var(--scale)) calc(8px * var(--scale));
  scrollbar-width: none;
  scroll-snap-type: x mandatory;
}
.cu-sl-scroll::-webkit-scrollbar {
  display: none;
}

.cu-sl-card {
  flex: 0 0 var(--cu-card-width);
  width: var(--cu-card-width);
  display: flex;
  flex-direction: column;
  gap: var(--cu-gap);
  background: var(--slab);
  border: 1px solid var(--dim);
  border-radius: var(--cu-card-radius);
  padding: var(--cu-card-pad);
  box-shadow: var(--card-shadow);
  scroll-snap-align: start;
}

/* On the same inset as a highlight card's competition line. */
.cu-sl-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: calc(8px * var(--scale));
  padding: calc(2px * var(--scale)) calc(2px * var(--scale)) 0;
}
.cu-sl-metric {
  font-family: var(--font-display);
  font-size: calc(15px * var(--scale));
  font-weight: 700;
  line-height: var(--leading-tight);
  color: var(--chalk);
  letter-spacing: calc(-0.3px * var(--scale));
}
.cu-sl-league-badge {
  width: calc(18px * var(--scale));
  height: calc(18px * var(--scale));
  object-fit: contain;
}

/*
 * The leader and the two under him on one set of columns — rank, face, name,
 * figure — so the three ranks, the three faces, the three names and the three
 * figures each stand in a line down the card. In the source each row sized
 * its own: two disc sizes on two insets, two face sizes, and so names and
 * figures that started and ended in different places on every row. A
 * runner's smaller face is centred in the leader's column.
 */
.cu-sl-featured,
.cu-sl-runner {
  display: grid;
  grid-template-columns: calc(24px * var(--scale)) calc(48px * var(--scale)) minmax(0, 1fr) auto;
  align-items: center;
  column-gap: calc(10px * var(--scale));
  padding: calc(10px * var(--scale)) var(--cu-card-pad);
  background: var(--slate);
  border-radius: var(--cu-panel-radius);
}
.cu-sl-rank {
  width: calc(24px * var(--scale));
  height: calc(24px * var(--scale));
  border-radius: 50%;
  background: var(--blue);
  color: #fff;
  font-family: var(--font-mono);
  font-size: calc(13px * var(--scale));
  font-weight: 800;
  display: grid;
  place-items: center;
  flex-shrink: 0;
}
.cu-sl-photo {
  width: calc(48px * var(--scale));
  height: calc(48px * var(--scale));
  border-radius: 50%;
  object-fit: cover;
  background: var(--slab);
  flex-shrink: 0;
}
.cu-sl-info {
  flex: 1;
  min-width: 0;
}
.cu-sl-name {
  font-size: calc(15px * var(--scale));
  font-weight: 700;
  line-height: var(--leading-tight);
  color: var(--chalk);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* One line, like every other name and club on the card: "Bayern München"
   wrapped to two and made its leader's panel taller than the rest. */
.cu-sl-team {
  display: flex;
  align-items: center;
  gap: calc(5px * var(--scale));
  margin-top: calc(3px * var(--scale));
  min-width: 0;
  font-size: calc(12px * var(--scale));
  font-weight: 500;
  line-height: var(--leading-snug);
  color: var(--ghost);
}
.cu-sl-team span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cu-sl-team img {
  width: calc(14px * var(--scale));
  height: calc(14px * var(--scale));
  object-fit: contain;
  flex-shrink: 0;
}
.cu-sl-value {
  font-family: var(--font-mono);
  font-size: calc(22px * var(--scale));
  font-weight: 800;
  line-height: var(--leading-none);
  color: var(--blue);
  text-align: right;
  flex-shrink: 0;
}

.cu-sl-runners {
  display: flex;
  flex-direction: column;
  gap: calc(6px * var(--scale));
}

.cu-sl-runner {
  padding-block: calc(8px * var(--scale));
}
.cu-sl-runner-rank {
  width: calc(24px * var(--scale));
  height: calc(24px * var(--scale));
  border-radius: 50%;
  background: var(--dim);
  color: var(--ghost);
  font-family: var(--font-mono);
  font-size: calc(11px * var(--scale));
  font-weight: 700;
  display: grid;
  place-items: center;
  flex-shrink: 0;
}
.cu-sl-runner-avatar {
  position: relative;
  justify-self: center;
  width: calc(36px * var(--scale));
  height: calc(36px * var(--scale));
  flex-shrink: 0;
}
.cu-sl-runner-avatar img:first-child {
  width: calc(36px * var(--scale));
  height: calc(36px * var(--scale));
  border-radius: 50%;
  object-fit: cover;
  background: var(--slab);
}
.cu-sl-runner-badge {
  position: absolute;
  bottom: calc(-2px * var(--scale));
  right: calc(-4px * var(--scale));
  width: calc(16px * var(--scale));
  height: calc(16px * var(--scale));
  object-fit: contain;
  background: var(--slab);
  border-radius: 50%;
  border: 1px solid var(--slate);
}
.cu-sl-runner-info {
  flex: 1;
  min-width: 0;
}
.cu-sl-runner-name {
  font-size: calc(13px * var(--scale));
  font-weight: 600;
  color: var(--chalk);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cu-sl-runner-team {
  font-size: calc(11px * var(--scale));
  font-weight: 500;
  color: var(--ghost);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cu-sl-runner-value {
  font-family: var(--font-mono);
  font-size: calc(16px * var(--scale));
  font-weight: 700;
  line-height: var(--leading-none);
  color: var(--chalk);
  text-align: right;
  flex-shrink: 0;
}

/*
 * The two ported sections in the dark theme. The source only ever drew them
 * light — its dark theme is a navy the rest of this room does not wear — so
 * these map its names onto the app's dark greys instead, and the literals
 * Player Highlights sets rule by rule get a dark twin each. The light rules
 * above stay the source's, 1:1. `--blue` takes the source's own
 * `--blue-bright`, since its #0d4ff7 is 2.5:1 as type on a dark card.
 */
:root[data-theme="dark"] .cu-section {
  --blue: #4d82ff;
  --slab: var(--color-bg-card);
  --slate: var(--color-gray);
  --chalk: var(--color-text-primary);
  --ghost: var(--color-text-muted);
  --dim: var(--color-border-subtle);
  --card-shadow: none;
  --card-shadow-lg: none;
}

:root[data-theme="dark"] .cu-hl {
  background: var(--color-bg-card);
  border-color: var(--color-border-subtle);
}

:root[data-theme="dark"] .cu-hl-player,
:root[data-theme="dark"] .cu-hl-stats {
  background: var(--color-gray);
}

:root[data-theme="dark"] .cu-hl-league-ph,
:root[data-theme="dark"] .cu-hl-avatar {
  background: var(--color-gray-strong);
}

:root[data-theme="dark"] .cu-hl-name,
:root[data-theme="dark"] .cu-hl-stat-value {
  color: var(--color-text-primary);
}

:root[data-theme="dark"] .cu-hl-league span,
:root[data-theme="dark"] .cu-hl-vs span {
  color: var(--color-text-secondary);
}

:root[data-theme="dark"] .cu-hl-sub,
:root[data-theme="dark"] .cu-hl-stat-label,
:root[data-theme="dark"] .cu-hl-rank {
  color: var(--color-text-muted);
}

:root[data-theme="dark"] .cu-hl-cta {
  background: rgb(77 130 255 / 0.12);
}

:root[data-theme="dark"] .cu-hl-cta:active {
  background: rgb(77 130 255 / 0.2);
}

/* scale-lint: on */


/* --- The splash ------------------------------------------------------------- */

/*
 * Tobi's mock of 2026-10-07 (`catchup-splash.html`), ported 1:1 — every
 * size, colour, delay and curve below is the mock's. Its phone frame (the
 * rounded corners and the bezel) is the mock's presentation, not the screen,
 * and is left out; its 44px status-bar margin is the device's top inset here.
 *
 * The mock is drawn at 390 wide. `--u` is one of its pixels: 1px from 390 up,
 * and proportionally less on a narrower phone, so "CATCH" at 104 never runs
 * off a 360 screen and the composition keeps its proportions.
 *
 * `data-phase` is the layer's (enter → exit); `data-sequence` is the mock's
 * own entrance, started by CatchUpSplash.js once the face is in (idle → in).
 * Only opacity and transform ever animate.
 */
.catch-up-splash {
  --bg: #d42a3c;
  --fg: #ffffff;
  --faded: rgb(255 255 255 / 0.32);
  --splash-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
  --splash-ease-in: cubic-bezier(0.22, 1, 0.36, 1);
  --splash-ease-out: cubic-bezier(0.4, 0, 1, 1);

  position: fixed;
  inset: 0;
  /* Over the tab and the app's nav (layout.css, 10); under the pages and the
   * gates that open on top of everything (72 and up). */
  z-index: 20;
  /* The ground eases in (Tobi, 2026-09-21: every part of the splash eases
   * in) — 300ms from mount, the mock's sequence running on top of it. */
  opacity: 0;
  /* The app's own column, not the window: on a desktop or a wide browser tab
   * the app is a phone-width column centred on the page (layout.css), and
   * every fixed layer keeps to it the same way the sign-in gate does. */
  max-width: var(--app-max-width);
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--fg);
  font-family: "Space Grotesk", system-ui, sans-serif;
  overflow: hidden;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}

/* scale-lint: off — the mock's own absolutes (a splash is not a rung of the
 * app's hierarchy), so it is fenced from the scale rather than bent onto it. */

.catch-up-splash {
  --u: min(1px, calc(100vw / 390));
}

/* The whistle, huge and faint, bleeding off the right edge behind the
 * heading. */
.catch-up-splash__ghost {
  position: absolute;
  right: calc(-80 * var(--u));
  bottom: calc(60 * var(--u));
  width: calc(300 * var(--u));
  height: calc(600 * var(--u));
  opacity: 0;
  pointer-events: none;
}

.catch-up-splash__ghost .prezzbox-mark {
  display: block;
  width: 100%;
  height: 100%;
}

/* The top bar: the whistle and the app's name, under the device's inset. */
.catch-up-splash__bar {
  position: relative;
  flex: none;
  display: flex;
  align-items: center;
  gap: calc(8 * var(--u));
  height: calc(56 * var(--u));
  margin-top: var(--safe-top);
  padding: 0 calc(24 * var(--u));
  opacity: 0;
}

.catch-up-splash__bar .prezzbox-mark {
  flex: none;
  width: calc(13 * var(--u));
  height: calc(26 * var(--u));
}

.catch-up-splash__brand {
  font-size: calc(16 * var(--u));
  font-weight: 700;
}

/* Everything else sits at the bottom, left-aligned. */
.catch-up-splash__body {
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 0 calc(24 * var(--u)) calc(56 * var(--u) + var(--safe-bottom));
}

.catch-up-splash__heading {
  margin: 0;
  display: flex;
  flex-direction: column;
  font-weight: 700;
  line-height: 0.88;
  letter-spacing: calc(-4 * var(--u));
  transform-origin: left bottom;
}

.catch-up-splash__line {
  display: block;
  font-size: calc(104 * var(--u));
  opacity: 0;
}

.catch-up-splash__line--faded {
  color: var(--faded);
}

.catch-up-splash__foot {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: calc(20 * var(--u));
  padding-top: calc(16 * var(--u));
  opacity: 0;
}

/* The rule along the foot's top edge, drawn left to right. */
.catch-up-splash__rule {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
  background: rgb(255 255 255 / 0.4);
  transform: scaleX(0);
  transform-origin: left;
}

.catch-up-splash__tagline {
  font-size: calc(15 * var(--u));
  font-weight: 500;
}

.catch-up-splash__by {
  font-size: calc(13 * var(--u));
  font-weight: 500;
}

.catch-up-splash__by strong {
  font-weight: 700;
}

/* The loader: a hairline 28px under the foot, at the text's left edge, only
 * after two seconds of the room not being ready. It pulses; it is never a
 * spinner. */
.catch-up-splash__loader {
  position: absolute;
  top: calc(100% + 28px);
  left: 0;
  width: 32px;
  height: 2px;
  border-radius: 1px;
  background: var(--fg);
  opacity: 0.3;
  animation: splash-loader 1200ms ease-in-out infinite;
}

/* --- The mock's sequence, from the font gate -------------------------------- */

.catch-up-splash[data-sequence="in"] .catch-up-splash__ghost {
  animation: splash-ghost-in 1100ms var(--splash-ease) 50ms forwards;
}

.catch-up-splash[data-sequence="in"] .catch-up-splash__bar {
  animation: splash-fade-in 400ms ease-out 150ms forwards;
}

.catch-up-splash[data-sequence="in"] .catch-up-splash__line {
  animation: splash-slide-in 500ms var(--splash-ease) 250ms forwards;
}

.catch-up-splash[data-sequence="in"] .catch-up-splash__line--faded {
  animation-delay: 370ms;
}

.catch-up-splash[data-sequence="in"] .catch-up-splash__foot {
  animation: splash-fade-in 400ms ease-out 600ms forwards;
}

.catch-up-splash[data-sequence="in"] .catch-up-splash__rule {
  animation: splash-draw 600ms var(--splash-ease) 600ms forwards;
}

/* scale-lint: on */

/* --- The entrance and the exit of the layer itself ---------------------------- */

.catch-up-splash[data-phase="enter"] {
  animation: splash-fade-in 300ms var(--splash-ease-in) forwards;
  will-change: opacity;
}

.catch-up-splash[data-phase="exit"] {
  /* `both`, not `forwards`: the layer's base opacity is 0 (it eased in), so
   * the 50ms before this fade starts must hold the `from` frame, or the
   * ground would blink out and back for the length of the delay. */
  animation: splash-fade-out 400ms var(--splash-ease-out) 50ms both;
  will-change: opacity;
  pointer-events: none;
}

/* The pieces go a beat ahead of the ground. These come after the sequence's
 * rules, so they replace its animations; the rule keeps its own, inside a
 * foot that is fading. */
.catch-up-splash[data-phase="exit"] .catch-up-splash__bar,
.catch-up-splash[data-phase="exit"] .catch-up-splash__foot {
  animation: splash-fade-out 250ms var(--splash-ease-out) forwards;
}

.catch-up-splash[data-phase="exit"] .catch-up-splash__heading {
  animation: splash-heading-out 350ms var(--splash-ease-out) forwards;
}

.catch-up-splash[data-phase="exit"] .catch-up-splash__ghost {
  animation: splash-ghost-out 350ms var(--splash-ease-out) forwards;
}

/* The room's content, rising through the departing splash: held invisible
 * while the splash is up, then 24px → 0 over 450ms from 100ms into the exit
 * (CatchUp.js flips the state on the splash's `onExit`). */
.catch-up__content[data-state="waiting"] {
  opacity: 0;
}

.catch-up__content[data-state="entering"] {
  animation: splash-content-in 450ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
  will-change: transform, opacity;
}

/* The ghost lands at the mock's 0.14, and leaves from it. */
@keyframes splash-ghost-in {
  from {
    opacity: 0;
    transform: translate(40px, 30px) rotate(8deg) scale(0.92);
  }
  to {
    opacity: 0.14;
    transform: none;
  }
}

@keyframes splash-ghost-out {
  from {
    opacity: 0.14;
  }
  to {
    opacity: 0;
  }
}

@keyframes splash-slide-in {
  from {
    opacity: 0;
    transform: translateX(-28px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes splash-draw {
  to {
    transform: scaleX(1);
  }
}

@keyframes splash-heading-out {
  from {
    opacity: 1;
    transform: none;
  }
  to {
    opacity: 0;
    transform: scale(1.06);
  }
}

@keyframes splash-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes splash-fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

@keyframes splash-content-in {
  from {
    opacity: 0;
    transform: translateY(24px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes splash-loader {
  0%,
  100% {
    opacity: 0.3;
  }
  50% {
    opacity: 0.8;
  }
}

/*
 * Reduced motion. The sequence is the mock's own rule — it lands at once.
 * The layer's ease-in is already a plain fade; the exit is 250ms opacity
 * fades, no scale; the content fades rather than rises; the loader is a
 * static hairline at half opacity. The exit rules come after the sequence's,
 * so they win while both match.
 */
@media (prefers-reduced-motion: reduce) {
  .catch-up-splash[data-sequence="in"] .catch-up-splash__ghost,
  .catch-up-splash[data-sequence="in"] .catch-up-splash__bar,
  .catch-up-splash[data-sequence="in"] .catch-up-splash__line,
  .catch-up-splash[data-sequence="in"] .catch-up-splash__foot,
  .catch-up-splash[data-sequence="in"] .catch-up-splash__rule {
    animation-duration: 0.01s;
    animation-delay: 0s;
  }

  .catch-up__content[data-state="entering"] {
    animation-name: splash-fade-in;
    animation-duration: 250ms;
    animation-timing-function: ease;
  }

  .catch-up-splash[data-phase="exit"],
  .catch-up-splash[data-phase="exit"] .catch-up-splash__bar,
  .catch-up-splash[data-phase="exit"] .catch-up-splash__foot,
  .catch-up-splash[data-phase="exit"] .catch-up-splash__heading {
    animation-name: splash-fade-out;
    animation-duration: 250ms;
    animation-timing-function: ease;
  }

  .catch-up-splash[data-phase="exit"] .catch-up-splash__ghost {
    animation-duration: 250ms;
    animation-timing-function: ease;
  }

  .catch-up-splash__loader {
    animation: none;
    opacity: 0.5;
  }
}
