/* FPL System — shared styles. Tokens copied from the FPL System design system (tokens.json v1).
   Light theme only: the dark theme has not been designed or checked (fixture-good sits too close
   to the dark surface). */
:root {
  --surface: #f5f4ef;
  --surface-raised: #ffffff;
  /* The two header chips, sampled off navigation.png and office-phone.png,
     which agree. Not --surface (#f5f4ef), which is a shade lighter and reads
     as no chip at all. */
  --chip: #ebe8e0;
  /* ── the desk monitor, which is the one dark surface on this site ───────
     ⚠ THESE ARE TOKENS BECAUSE EVERY COLOUR HERE IS. `test_css_tokens` refuses
     a hex literal outside a definition, and it caught six of these when the
     staff inbox landed on the monitor — correctly: a one-off palette hidden
     inside a component is how a site stops having a palette.
     ⚠ AND THEY ARE A SURFACE, NOT A THEME. The monitor is a photographed
     object that is switched on; nothing else on the Office is dark, so these
     do not belong in the light/dark pair above. Sampled to sit just above the
     glass's own near-black so the panel reads as lit rather than as a hole. */
  --screen-face: #0f1113;
  --screen-chrome: #171a1d;
  --screen-ink: #eceef0;
  --screen-ink-dim: rgba(236, 238, 240, 0.52);
  --screen-line: rgba(255, 255, 255, 0.07);
  --screen-bar: rgba(255, 255, 255, 0.13);
  --screen-bar-on: rgba(255, 255, 255, 0.28);
  --screen-row-on: rgba(255, 255, 255, 0.09);
  --screen-row-hover: rgba(255, 255, 255, 0.05);
  --screen-good: #6ee7a8;
  --screen-bad: #ff8f8f;
  /* ⚠ WHITE IS THE NO-OP, WHICH IS WHY THE DEFAULT IS A COLOUR AND NOT
     'unset'. The jersey is tinted by multiplying it, and multiplying by
     white leaves every pixel where it was -- so the shirt prints as the
     white photograph it is until a manager picks something, and the rule
     that tints it needs no second branch. It was briefly read and never
     defined, which test_css_tokens_exist.py refused: an undefined custom
     property takes the property's INITIAL value, and for background-color
     that is transparent -- the shirt would have gone see-through. */
  /* ⚠ VIOLET IS THE DEFAULT NOW, NOT WHITE. Amr, 2026-10-07: the blank
     jersey should wear a colour that belongs to us and cannot be mistaken
     for a Premier League club's. No club in the division plays in purple --
     Villa is claret, Palace is red and blue -- so a violet shirt on the
     pitch is unambiguously OURS and not somebody's away kit. The colour is
     already one of the eight the picker offers, so it has already been
     measured: tests/test_kit_colours_survive.py holds it to fold survival
     and to 4.5:1 for the lettering, and the ink that clears it is white.

     ⚠ THE INK MUST MOVE WITH THE TINT, in the same declaration block. The
     default used to be white cloth with dark ink; violet cloth with dark
     ink is 2.6:1 and the names would have gone. */
  --kit-tint: var(--kit-violet);
  /* ⚠ THE LETTERING FOLLOWS THE SHIRT. On a crimson or royal kit the dark
     ink on the back of the jersey falls to a contrast ratio of 2.9 and 2.1;
     white reaches 6.2 and 8.6. Which way round it goes is a property of the
     colour, so it is set beside the colour. */
  --kit-ink: var(--on-turf);
  --kit-ink-muted: var(--on-turf);
  /* ⚠ EIGHT COLOURS, AND THE OTHER FOUR WERE REJECTED BY MEASUREMENT RATHER
     THAN BY TASTE. The jersey is white photography tinted with `multiply`, so
     the folds survive only while the colour is light enough to keep them: the
     unmultiplied cloth varies with a standard deviation of 11.6, and navy,
     forest, claret and charcoal crush that to 2.2-3.2 -- a silhouette with a
     number on it. Every colour here holds 4.0 or better AND clears 4.5:1
     against one of the two lettering colours. tools/ has no script for this;
     the figures are in the commit that added them. */
  --kit-crimson: #c62a34;
  --kit-royal:   #1c4ab2;
  --kit-sky:     #6cabdd;
  --kit-teal:    #148086;
  --kit-violet:  #6c429e;
  --kit-amber:   #df9626;
  --kit-coral:   #e66e56;
  --kit-lime:    #8eb63a;
  --line: #d9d7ce;
  --ink: #15191a;
  /* ⚠ THE ONE PIECE OF HARDWARE IN THE PALETTE. The line-up board is drawn
     as a tablet because the design folder ships none, and a bezel is the only
     thing on this site that is neither ink nor surface -- near-black, but
     cooler than --ink, which is a reading colour and reads as a smudge at
     this size. Defined here because every colour on this site is. */
  --device: #14171b;
  --ink-muted: #545b57;
  --turf: #1d6b43;
  --turf-soft: #dcece2;
  --on-turf: #ffffff;
  /* ink for a label sitting on any saturated fill. Same value as --on-turf
     and a different job: that one names the turf green, this one covers the
     fixture bins, where the label is white because the fill is dark and not
     because it is green. */
  --on-fill: #ffffff;
  --amber: #924900;
  --amber-soft: #f8e7d3;
  /* ⚠ A CARD IS AN OBJECT, NOT A LABEL, so it does not take the text
     colours. --amber is #924900, picked to clear 4.5:1 as type on white,
     and a yellow card painted in it is a brown rectangle -- which is what
     shipped. Same for the armband: a captain's band is gold, and gold is
     not a colour anything on this site has had to write words in. */
  --card-yellow: #f2c200;
  --armband: #e3a008;
  /* ⚠ THE SQUAD BOARD'S MARKER AND MAGNET. Written as tokens because the
     stylesheet refuses a literal in a rule and refuses a var() fallback, and
     both refusals are earned: four fallbacks in this repo have disagreed
     with the token they shadowed. The board is a whiteboard with the pitch
     drawn on it in blue marker and the cards held up by a red magnet, so
     these are kit colours for a drawing rather than palette colours for
     type, and none of them is ever written in. */
  --pitch-line: #bcd0ea;
  --magnet: #c0392b;
  --magnet-lit: #e06a62;
  --magnet-dark: #8e2a20;
  --danger: #b3261e;
  --danger-soft: #f9dedb;
  /* Fixture strip: cividis, 4 bins, hardest -> easiest. Always print the word, never a value. */
  /* Fixture bins, hot -> cold, as a DIVERGING pair: intensity grows outward
     from the middle, so the two warm steps and the two cool steps each read as
     a pair. Measured with tools/check_palette.js under Vienot protan/deutan/
     tritan simulation -- worst adjacent separation 19.4 (OKLab x100) against a
     target of 8, worst normal-vision step 21.8 against a floor of 15, every
     label at or above 4.87:1 contrast. Re-run that tool before changing any of
     these four values. */
  --fixture-hard: #b35806;   /* strong orange -- hottest */
  --fixture-tough: #f1a340;  /* pale orange */
  --fixture-fair: #67a9cf;   /* pale blue */
  --fixture-good: #16436e;   /* strong blue -- coldest */

  /* ── club primaries ──────────────────────────────────────────────────────
     One first-choice shirt colour per club, keyed by the THREE-LETTER CODE and
     not by a numeric id. Both numeric spaces in the feed are ambiguous and they
     disagree: clubs["1"] is ARS while data.short["1"] is MUN, so a build that
     treats them interchangeably labels Arsenal's fixtures as Manchester United's
     without failing. The code is unambiguous in both.

     ⚠ THESE ARE EXEMPT FROM THE PAIR-SEPARATION FLOOR, DELIBERATELY, AND THE
     EXEMPTION IS MEASURED RATHER THAN ASSUMED:

         node tools/check_palette.js "$(hexes)" --all-pairs \
              --labels ARS,AVL,... --bg "#ffffff" --text "$(inks)"

         NORMAL VISION, ALL 190 pairs (floor 15) ....  36 fail   0 warn
         PROTANOPIA   (target 8, floor 6) ..........   26 fail   3 warn
         DEUTERANOPIA (target 8, floor 6) ..........   23 fail   4 warn
         TRITANOPIA   (target 8, floor 6) ..........   24 fail   5 warn

     BOU and MUN are THE SAME HEX, #DA291C, and separate by 0.0 under every
     vision model — no floor, metric or measurement point can part them.
     CHE/CRY separate by 1.4, BRE/NFO by 1.5, ARS/SUN by 1.8.

     ⚠ ALL PAIRS, NOT ADJACENT ONES. The gate defaults to consecutive pairs
     because a SCALE has a fixed order and bin 1 never sits beside bin 4.
     Clubs have no order: the fixture list decides who sits beside whom, so
     every pair can be adjacent. Running the default here would have scored
     19 arbitrary pairs, passed, and left 171 unexamined.

     That is not a fault in these values. Six of the twenty clubs play in red and
     four in blue; real brand colours are not designed to be mutually separable,
     and no honest set of twenty will pass a floor built for a four-step diverging
     scale. The consequence is the rule, not the colours:

         THE COLOUR IS DECORATION. THE THREE-LETTER CODE CARRIES IDENTITY.

     Every shirt, locker and chip must print the code. Nothing may distinguish two
     clubs by colour alone — on a BOU/MUN fixture the colour says nothing at all.
     A future reader running the palette gate over this block will see 27 failures;
     they are expected, and fixing them means abandoning real club colours.

     ⚠ WHAT IS **NOT** EXEMPT, AND WHAT PASSES CLEAN: text contrast (20/20 clear
     WCAG AA 4.5:1 against their own ink — tightest ARS 4.68 and SUN 4.69, both
     on black) and separation from the page background (20/20). Those two are
     enforced by tests/test_club_colours.py. Only pair separation is waived.

     Static, one per club, changes once a season. Not a feed field: the feed
     carries no colour of any kind, and this is a presentation choice rather than
     a measurement. */
  /* ⚠ THE TWENTY PAIRS MOVED OUT ON 2026-10-07 and are GENERATED now, at the
     foot of this file, from tools/club_colours.json -- the design pack's own
     file, which also carries each club's second colour and whether it wears
     stripes. Hand-written here they could not express a pattern, and sixty
     numbers maintained by hand against a file somebody else re-checks every
     August is a drift waiting to happen. Run tools/gen_club_css.py; a test
     fails if the stylesheet and the file disagree. */
  /* the page colour at 0.4, washed over every scene -- see the scene block */
  /* ⚠ THE WASH AND THE DESATURATION ARE ONE DIAL AND THEY BOTH CAME DOWN
     A TENTH ON 2026-10-06. Amr: the scenes read too grey and too flat, so
     the colour has to come through more. The wash went 0.40 -> 0.36 and the
     filter 0.8 -> 0.9 saturation, which is the same 10% from the other side.
     Both still exist for the reason they were added: every figure on these
     pages sits on an opaque plate, and the gate that measures that is the
     thing to re-run if either moves again. */
  --scene-wash: rgba(245, 244, 239, 0.36);
  --scene-sat: 0.9;
  /* ⚠ PURE WHITE ON PURPOSE, AND IT IS NOT --surface-raised. The club code
     sits on this inside a roundel whose fill is a real club primary, so this
     one patch has to hold a fixed contrast against all twenty. Tying it to a
     surface token would let a future warm-up of the page colour quietly cut
     the contrast behind the only thing that identifies a club. */
  --club-pill: #ffffff;
  /* Scout's corkboard. Not club colours and not page surfaces -- a physical
     object rendered once, on one screen, and tokenised so the palette check
     can see it like everything else. */
  --cork: #C9A273;  --cork-edge: #8F6438;  --cork-dot: #BD986C;
  --cork-ink: #3A2A18;  --pinhead: #2456A6;
  /* the forecast blue: the colour that means "the Analyst said so" wherever
     a measured and a modelled half sit on one screen */
  --forecast: #2456A6;  --forecast-soft: #DFE8F7;
  /* Plan's seesaw: a drawn wooden object, not a data colour. Tokenised so
     the palette check sees it like everything else. */
  --plank: #B8824A;  --fulcrum: #8A5A2B;
  --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --space-1: 4px; --space-2: 8px; --space-4: 16px; --space-6: 24px;
  --radius-sm: 4px; --radius-md: 6px; --radius-lg: 10px;
  color-scheme: light;
}
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body { margin: 0; background: var(--surface); color: var(--ink); font-family: var(--font-sans); font-size: 15px; line-height: 22px; }
a { color: var(--turf); }
a:hover { color: var(--ink); }
:focus-visible { outline: 2px solid var(--turf); outline-offset: 2px; }
button { font: inherit; }
[hidden] { display: none !important; }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }

/* Text styles */
.display { margin: 0; font-size: 32px; line-height: 36px; font-weight: 650; letter-spacing: -0.01em; }
.title { margin: 0; font-size: 20px; line-height: 28px; font-weight: 600; }
.label { font-size: 13px; line-height: 18px; font-weight: 500; color: var(--ink-muted); }
.micro { font-size: 11px; line-height: 16px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
.muted { color: var(--ink-muted); }
.data { font-family: var(--font-mono); font-size: 14px; line-height: 20px; font-weight: 500; }
.data-sm { font-family: var(--font-mono); font-size: 12px; line-height: 16px; }
.strong { font-weight: 600; }

/* Stage navigation */
/* ── app bar and tab bar ──────────────────────────────────────────────────
   The design pack's navigation sheet: five tabs in a bar at the BOTTOM,
   within thumb reach, and a header carrying the crest and the feed's age.
   What was here was eight tabs in a scrolling row at the top, which is
   neither the count nor the position the pack specifies. */
/* ⚠ A BANNER, AND DELIBERATELY NOT STICKY. The old `.appbar` was
   `position: sticky; top: 0` and carried the menu, so it had to stay. A
   banner carries nothing you need mid-page, and this app already measures
   top sticky bars and caps them at 8% of the screen -- pinning 100px of
   photograph there would spend that budget on decoration. It scrolls away;
   the rail, which you do need at any moment, is what stays. */
.masthead {
  position: relative; overflow: hidden;
  background-image: var(--banner); background-size: cover;
  background-position: center 64%; background-repeat: no-repeat;
  border-bottom: 1px solid var(--line);
}
[data-banner="fans"] { --banner: url("../img/scenes/onb-fans.jpg"); }
/* The photograph is a floodlit ground at night and the chips on it are
   light; the scrim is what keeps that contrast off the brightest part of
   the pitch rather than relying on where the crop happens to land. */
.masthead::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, rgba(12, 16, 22, 0.62), rgba(12, 16, 22, 0.22) 58%,
                              rgba(12, 16, 22, 0.52));
}
.mast-in {
  position: relative; z-index: 1;
  display: flex; align-items: center; gap: var(--space-2);
  padding: 12px var(--space-4); min-height: 68px;
}
/* ⚠ THE MARK GOES ON A PLATE BECAUSE THE CREST IS NEAR-BLACK. Straight on
   the photograph it disappears into the stand. A white plate carrying a mark
   over a photograph is this app's own idiom -- it is what the locker
   nameplates do with a club's three letters. */
.mast-lockup {
  display: flex; align-items: center; gap: 9px;
  background: var(--surface-raised); border-radius: var(--radius-lg);
  padding: 7px 13px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.masthead .crest { width: 32px; height: 32px; flex: 0 0 auto; }
.appmark { display: flex; flex-direction: column; justify-content: center; min-width: 0; }
.masthead .appname { font-weight: 800; font-size: 19px; letter-spacing: 0.01em; }
/* The model hash the render prints under the wordmark. Desktop only: the
   three phone renders show the wordmark alone. */
.appmodel { display: none; font-family: var(--font-mono); font-size: 11px;
            line-height: 14px; color: var(--ink-muted); }
.appbar-right { margin-left: auto; display: flex; gap: 6px; align-items: center; }
.gwchip, .builtchip {
  border-radius: 999px; padding: 3px 10px; font-size: 13px; white-space: nowrap;
}
/* ⚠ THE GAMEWEEK CHIP IS NEUTRAL, NOT GREEN. Both renders that show it
   give it the same fill as the build-age chip beside it; --turf-soft made it
   read as a status, which a gameweek number is not. */
.gwchip { background: var(--chip); color: var(--ink); font-weight: 650; }
.builtchip { background: var(--chip); color: var(--ink-muted); font-family: var(--font-mono); font-size: 12px; }
/* ⚠ THE ONLY STALENESS SIGNAL LEFT. The banner under the header came off on
   2026-10-07; past the age limit this chip is the whole warning, so it stops
   being furniture and takes the amber the banner used to. */
.builtchip.stale { background: var(--amber-soft); color: var(--amber); font-weight: 650; }

/* ⚠ TWO ARRANGEMENTS OF THE SAME FIVE TABS, ONE DRAWN AT A TIME. Both are in
   every page's DOM; this breakpoint is the only thing that chooses. 760px is
   the width the rest of this sheet already uses to switch layouts.

   ⚠ THE DESKTOP ONE IS A LEFT RAIL NOW, ON AMR'S INSTRUCTION OF 2026-10-06,
   and that is a departure from navigation.png, which draws the five inline
   along the header. Recorded, not silently done. */
.sidenav { display: none; }


/* ⚠ FIXED, AND THE PAGE RESERVES ITS HEIGHT. A bottom bar that overlays
   content hides the last row of every list, which on Deadline is a player. */
.tabbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  display: grid; grid-template-columns: repeat(5, 1fr);
  background: var(--surface-raised);
  border-top: 1px solid var(--line);
  padding-bottom: env(safe-area-inset-bottom, 0);
}
.tabbar a {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; min-height: 56px; text-decoration: none;
  color: var(--ink-muted); font-size: 12px; font-weight: 500;
}
.tabbar a.on { color: var(--turf); font-weight: 700; }
.tabbar a.on svg { stroke: var(--turf); }
body.has-tabbar { padding-bottom: 72px; }

@media (min-width: 760px) {
  /* ⚠ A PROPER MASTHEAD, NOT A BAR. Amr, 2026-10-07: enlarge it. 104px was
     a tall header; a site banner carrying a photograph is usually 160-240px,
     and under about 150 the picture reads as a texture rather than a place.
     176 shows the stand, the pitch and the floodlights, which is the whole
     reason for using a photograph at all. */
  .mast-in { padding: 26px 32px; min-height: 176px; align-items: flex-end; }
  .masthead .crest { width: 44px; height: 44px; }
  .masthead .appname { font-size: 26px; letter-spacing: 0.005em; }
  .mast-lockup { padding: 10px 18px; gap: 12px; }
  .appmodel { font-size: 12px; }
  .masthead .appbar-right { align-self: flex-end; }
  .appmodel { display: block; }
  /* ⚠ EVERY CONTENT CHILD IS PLACED EXPLICITLY, BECAUSE `#app` DOES NOT HOLD
     THREE CHILDREN. Written for banner-rail-page and left to auto-place, it
     was correct on Review and wrong on Deadline, which appends five: a div
     and a trailing note either side of the page. Auto-placement put the page
     in column 1 on a later row and the changing room came out 239px wide.
     The rule is the whole population, not the page in front of you.

     `minmax(0, 1fr)` on the content column and not `1fr`: a bare `1fr` floors
     at min-content, and one wide table inside would push the whole page wider
     than the viewport rather than scroll itself. */
  body.has-rail #app {
    display: grid; grid-template-columns: 208px minmax(0, 1fr);
    align-items: start;
  }
  body.has-rail #app > .masthead { grid-column: 1 / -1; }
  body.has-rail #app > .sidenav { grid-column: 1; grid-row: 2; }
  /* ⚠ COLUMN PINNED, ROW LEFT ALONE. Deadline appends its Bench Boost note
     from a `.then`, long after the wrapper was built, so a rule that put
     every content child in row 2 stacked that note on top of the page. The
     column is the instruction ("on the left hand side" is about columns);
     the row is not, and anything arriving later simply takes the next one. */
  body.has-rail #app > :not(.masthead):not(.sidenav) { grid-column: 2; }
  /* ⚠ THE RAIL IS A PANEL, NOT FIVE LOOSE WORDS. Amr, 2026-10-07: give it
     boundaries so it reads as a menu. A surface, a hairline against the page,
     and a rule between the items -- which is what a site menu looks like and
     what tells a reader the five belong together and the page does not. */
  .sidenav {
    display: flex; flex-direction: column; gap: 0;
    margin: 12px 0 12px 12px;
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    overflow: hidden;
    padding: 6px;
    /* It follows you down the page because it is the navigation; the banner
       above it does not, because it is not. `align-self: start` is what
       lets it be shorter than the page and therefore able to stick at all. */
    position: sticky; top: 12px; align-self: start;
  }
  .sidenav a {
    display: flex; align-items: center; gap: 10px;
    padding: 11px 12px; border-radius: var(--radius-md);
    text-decoration: none; color: var(--ink-muted);
    font-size: 15px; font-weight: 650; white-space: nowrap;
    position: relative;
  }
  /* a hairline between items, and never above the first or below the last */
  .sidenav a + a::before {
    content: ""; position: absolute; left: 12px; right: 12px; top: -1px;
    border-top: 1px solid var(--line);
  }
  .sidenav a:hover { background: var(--chip); color: var(--ink); }
  .sidenav a:hover::before, .sidenav a.on::before,
  .sidenav a.on + a::before { border-color: transparent; }
  /* ⚠ THE LIVE TAB IS MARKED TWICE, by fill AND by an edge. Fill alone is a
     colour difference, and the one reader who cannot use it is the one this
     whole stylesheet is light-only for. */
  .sidenav a.on {
    background: var(--turf-soft); color: var(--turf); font-weight: 750;
    box-shadow: inset 3px 0 0 var(--turf);
  }
  .sidenav a.on svg { stroke: var(--turf); }
  /* ⚠ AND THE BOTTOM BAR GOES AWAY. Left drawn, it was 68px of fixed
     furniture over the foot of all five desktop pages. */
  .tabbar { display: none; }
  body.has-tabbar { padding-bottom: 0; }
}


/* Layout */
.page { max-width: 1440px; margin: 0 auto; padding: var(--space-6) 32px 48px; display: flex; flex-direction: column; gap: 20px; }
.page-head { display: flex; justify-content: space-between; align-items: flex-end; gap: var(--space-4); flex-wrap: wrap; }
.page-head .meta .chip { white-space: normal; }
.page-head .meta { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; margin-top: 6px; }
.cols { display: grid; grid-template-columns: minmax(0, 1fr) 500px; gap: 20px; align-items: start; }
.stack { display: flex; flex-direction: column; gap: 20px; min-width: 0; }
@media (max-width: 1100px) { .cols { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 640px) { .page { padding: var(--space-4) var(--space-4) 40px; } .nav { padding: var(--space-2) var(--space-4); } }

.card { background: var(--surface-raised); border: 1px solid var(--line); border-radius: var(--radius-lg); padding: 20px; min-width: 0; }
/* ⚠ A SLIDE IS PLATED AT EVERY WIDTH, AND THAT IS NOT A STYLING CHOICE.
   The Office screen shows the persona's graphics as toggled slides, and the
   plating gate -- which runs at 390px -- walks up from every text node for an
   opaque background and STOPS at `.scene-band`. The first version declared
   this inside the desktop media query, so on a phone a five-row table rendered
   straight onto the photograph and the gate was right to refuse it.

   ⚠ AND THE PLATE IS ON THE FRAME, NOT ON WHAT IT HOLDS, because a slide may
   not bring one: the Board's `seasonRecord` returns no `.card`. */
.slides { background: var(--surface-raised); border-radius: var(--radius-md); }
.slides > .slide-body { padding: 10px 12px; }
.slides > .rnav-bar { padding: 0 12px 8px; }
/* a slide that IS a card must not draw a second one inside the plate */
.slides > .slide-body > .card { margin: 0; border: 0; background: none; padding: 0; }
.card.call { border-color: var(--turf); }
.card.alert { border-color: var(--danger); }
.card-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-4); flex-wrap: wrap; margin-bottom: 12px; }

/* Chips */
.chip { display: inline-block; white-space: nowrap; padding: 2px 8px; border-radius: var(--radius-sm);
  font-size: 11px; line-height: 16px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
.chip.turf { color: var(--turf); background: var(--turf-soft); }
.chip.amber { color: var(--amber); background: var(--amber-soft); }
.chip.danger { color: var(--danger); background: var(--danger-soft); }
.chip.mute { color: var(--ink-muted); background: var(--surface); border: 1px solid var(--line); }

/* Verdict tiles */
.tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
@media (max-width: 760px) { .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.tile { display: flex; flex-direction: column; gap: 4px; padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius-md); }
.tile-top { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.tile-n { font-family: var(--font-mono); font-size: 24px; line-height: 28px; font-weight: 600; }
.tile-note { font-size: 12px; line-height: 16px; color: var(--ink-muted); }

/* Interval bar: band = 90% interval, tick = mean, dark line = no difference */
/* ⚠ THE PACK'S HEADLINE COMPONENT, MEASURED OFF house-style.png RATHER THAN
   EYEBALLED. Sampling a vertical slice through its capsule gives: track 4px,
   capsule 12px (a 2px border over an 8px fill), median tick 3px wide and 22px
   tall so it overhangs the capsule by 5px top and bottom. The fill, border,
   track and tick colours came back as EXACTLY --turf-soft, --turf, --line and
   --turf, so the tokens were already right and only the proportions were
   wrong: an 8px capsule with a 1px border reads as an outline at any real
   width, which is what the audit saw. The tick is the tallest thing here, so
   the row is sized to it. */
.range { position: relative; height: 24px; min-width: 120px; }
.range .track { position: absolute; left: 0; right: 0; top: 10px; height: 4px; border-radius: 2px; background: var(--line); }
.range .band { position: absolute; top: 6px; height: 12px; border-radius: 6px; background: var(--turf-soft); box-shadow: inset 0 0 0 2px var(--turf); }
.range .mid { position: absolute; top: 1px; width: 3px; height: 22px; margin-left: -1.5px; border-radius: 1px; background: var(--turf); }
/* Not in the pack's sheet, because its example is an absolute points range
   that never crosses zero. Plan's bar is a GAP and does, so the crossing is
   marked -- in ink, so it is not mistaken for a second median. */
.range .zero { position: absolute; top: 0; width: 2px; height: 24px; margin-left: -1px; background: var(--ink); }
.range.small { height: 16px; }
.range.small .track { top: 7px; height: 3px; }
.range.small .band { top: 4px; height: 9px; border-radius: 4.5px; box-shadow: inset 0 0 0 1.5px var(--turf); }
.range.small .mid { top: 1px; height: 15px; width: 2px; margin-left: -1px; }
.range.small .zero { height: 16px; }

/* Fixture strip */
.fixtures { display: flex; gap: 2px; flex-wrap: nowrap; }
/* ⚠ 2px of page background BETWEEN fills, not a border: a border adds a third
   colour to every edge and is the first thing to go muddy at this size. */
/* ⚠ A CHEVRON STRIP OVERLAPS, so the advance per cell is (width - point), not
   width. Measured before building: six 45px chevrons occupy 6*45 - 5*9 = 225px
   in the 250px column, which is WIDER than the 38px flat blocks they replace --
   the overlap buys the room back. The previous size was forced by a SILENT clip
   (six 46px blocks needing 286px while the strip still looked complete), so
   this arithmetic is the first thing checked, not the last. */
.fixtures.chev { gap: 0; }
.fx { width: 45px; height: 24px; margin-left: -9px; padding: 0 9px 0 11px;
      display: inline-flex; align-items: center; justify-content: center;
      font-size: 10px; font-weight: 700; letter-spacing: 0; border-radius: 0; }
.fx.first { margin-left: 0; }
.fx .fxo { line-height: 1; }
.fx.b0 { background: var(--fixture-hard); color: var(--on-fill); }
.fx.b1 { background: var(--fixture-tough); color: var(--ink); }
.fx.b2 { background: var(--fixture-fair); color: var(--ink); }
.fx.b3 { background: var(--fixture-good); color: var(--on-fill); }
.legend { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; font-size: 12px; line-height: 16px; color: var(--ink-muted); }

/* Plan rows */
.target { border-top: 1px solid var(--line); padding-top: 14px; margin-top: 14px; }
.target:first-of-type { border-top: 0; margin-top: 0; padding-top: 0; }
.target-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-4); flex-wrap: wrap; margin-bottom: 6px; }
.prow { display: grid; grid-template-columns: 210px 120px minmax(160px, 1fr) 170px 250px 72px; gap: 14px; align-items: center; padding: 10px 0; border-bottom: 1px solid var(--line); }
.prow-head { padding: 6px 0; color: var(--ink-muted); }
/* ⚠ THE PLAYER STAYS PUT, as on Deadline. This row is 982px of columns inside a
   390px phone, so it is read by scrolling -- and without a pinned name you
   scroll to an interval and no longer know whose it is, which makes a
   comparison unreadable exactly while it is being made. */
.prow > :first-child { position: sticky; left: 0; z-index: 2;
                       background: var(--surface-raised);
                       box-shadow: 2px 0 0 var(--line); }
.prow-head > :first-child { background: var(--surface-raised); }
.prow .num { text-align: right; display: flex; flex-direction: column; align-items: flex-end; }
.alts { padding: 4px 0 14px 224px; display: flex; flex-direction: column; gap: 6px; border-bottom: 1px solid var(--line); }
.alt { display: grid; grid-template-columns: 24px 170px minmax(120px, 1fr) 150px; gap: 14px; align-items: center; font-size: 13px; }
.scroll-x { overflow-x: auto; }
.scroll-x > .inner { min-width: 1100px; }
@media (max-width: 900px) { .alts { padding-left: 0; } }

/* Checks */
.check { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: start; padding: 10px 0; border-top: 1px solid var(--line); }
.check:first-child { border-top: 0; }

/* Squad list */
.srow { display: grid; grid-template-columns: 38px minmax(0, 1fr) 56px 48px 258px; gap: 8px; align-items: center; min-height: 32px; }
/* ⚠ THE NARROW LAYOUT USED TO HIDE THE FIXTURES AND THE OPPONENT. That is the
   phone, which is where this page is actually read, so the columns most worth
   having were the ones dropped. They now scroll instead, and the player stays
   put: without a sticky name you scroll to a number and no longer know whose
   it is, which makes a comparison unreadable exactly when it is being made. */
@media (max-width: 760px) {
  .srow { overflow-x: auto; -webkit-overflow-scrolling: touch;
          grid-template-columns: 38px 150px 56px 48px 258px; }
  .srow > :nth-child(2) { position: sticky; left: 0; z-index: 2;
                          background: var(--surface-raised);
                          box-shadow: 2px 0 0 var(--line); }
}

/* Buttons */
.btn { height: 44px; padding: 0 16px; border-radius: var(--radius-md); border: 1px solid var(--line); background: var(--surface-raised); color: var(--ink); font-size: 15px; font-weight: 500; cursor: pointer; }
.btn.primary { background: var(--turf); border-color: var(--turf); color: var(--on-turf); font-weight: 600; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn.small { height: 32px; padding: 0 10px; font-size: 13px; }

/* Messages */
.banner { display: flex; gap: var(--space-4); align-items: flex-start; }
/* ⚠ STACKS ON A PHONE. Side by side, the chip takes half of a 390px row and
   the sentence beside it wraps to one word per line -- technically not an
   overflow, which is why the overflow gate passed it, and unreadable anyway.
   A layout check that only asks "does it fit" does not ask "can it be read". */
@media (max-width: 560px) {
  .banner { flex-direction: column; gap: var(--space-2); }
  .banner > :first-child { align-self: flex-start; }
}
.note { padding: 8px 10px; border-radius: var(--radius-md); font-size: 13px; line-height: 18px; }
.note.danger { background: var(--danger-soft); }
.note.amber { background: var(--amber-soft); }
.note.turf { background: var(--turf-soft); }
.confirm { display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px; border-radius: var(--radius-md); border: 1px solid var(--line); background: var(--surface-raised); cursor: pointer; }
.confirm.need { border-color: var(--danger); }
.confirm input { width: 20px; height: 20px; margin: 0; flex-shrink: 0; accent-color: var(--turf); }
.fail { max-width: 720px; margin: 64px auto; }
.fail pre { background: var(--surface); padding: 10px 12px; border-radius: var(--radius-md); overflow-x: auto; font-size: 13px; }

/* ── faces and badges ──────────────────────────────────────────────────────
   ⚠ A FIXED BOX WITH A NEUTRAL FILL, so a missing photo changes nothing about
   the layout. Photos arrive late and some never arrive; a row that reflows when
   one 404s is worse than one that never had them. */
.face { width: 30px; height: 38px; flex: none; display: inline-flex;
        align-items: flex-end; justify-content: center; overflow: hidden;
        border-radius: 4px; background: var(--line); position: relative; }
.face img { width: 100%; height: 100%; object-fit: cover; object-position: top center; display: block; }
.face-fb { font: 600 11px/1 system-ui, sans-serif; color: var(--ink-muted);
           position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
.face.sm { width: 22px; height: 28px; }
.face.sm .face-fb { font-size: 9px; }
.badge { width: 16px; height: 16px; object-fit: contain; vertical-align: -3px; margin-right: 3px; }
.who { display: flex; align-items: center; gap: 7px; min-width: 0; }

/* the crossover sentence spans the whole row rather than sitting in a 120px
   column -- see plan.js.

   ⚠ AND SO DOES THE FLAG NOTE, which is the same kind of thing and was
   missed. Measured 2026-09-29 by check_layout at 390px: 2.7 words per line
   against a floor of 3.0, because without a span it lands in whichever grid
   column comes next and gets a 120px measure. It had been live in that state.
   The two selectors are kept together so the next prose block added to a row
   is added to both. */
.prow .crossover,
.prow [data-flag-unmodelled] { grid-column: 1 / -1; padding-top: 2px; }

/* a value and its unit are one thing; they must not be split by a line break */
.nb { white-space: nowrap; }

/* ── the verdict, abbreviated, riding in the sticky player cell ───────────── */
.vdot { font: 700 9px/1 system-ui, sans-serif; letter-spacing: .04em;
        padding: 3px 4px; border-radius: 3px; flex: none; align-self: flex-start; }
.vdot.turf { background: var(--turf-soft); color: var(--turf); }
.vdot.amber { background: var(--amber-soft); color: var(--amber); }
.vdot.danger { background: var(--danger-soft); color: var(--danger); }
.vdot.mute { background: var(--line); color: var(--ink-muted); }

/* ⚠ THE EXPLANATION IS PROSE AND WANTS A MEASURE. Run to the full width of a
   scrolling row it becomes a single long line nobody reads to the end of. */
/* ⚠ PINNED TO THE VIEWPORT INSIDE A HORIZONTAL SCROLLER. The row grid is
   982px wide, so a full-width cell is 982px and only the first 390 are ever on
   screen -- the sentence was there, and unreadable past its first line, unless
   you thought to scroll a row sideways to read prose. Sticky-left keeps it in
   view; the max-width keeps it to a readable measure. */
.prow .crossover,
.prow [data-flag-unmodelled] { position: sticky; left: 0;
                   /* ⚠ 100vw IS NOT THE AVAILABLE WIDTH. The element sits
                      37px in from the viewport edge (page + card padding) and
                      its scroller ends at 353, leaving 316 -- so a 342px cap
                      measured "correct" and still ran 26px past the edge.
                      Measure the space the element is actually in. */
                   max-width: min(46em, calc(100vw - 80px)); }
.target-head, .target-head > * { position: sticky; left: 0;
                                max-width: calc(100vw - 80px); }

/* the alternative's identity stays put while its interval scrolls */
.altwho { position: sticky; left: 0; z-index: 1;
          background: var(--surface-raised); padding-right: 6px; }

/* ⚠ SMALLER BARS. At the phone width the interval was the widest thing in the
   row, so the number it qualifies read as the secondary item. */
.range.small { max-width: 120px; }

/* ⚠ THE TOGGLE'S TRAILING SENTENCE GETS ITS OWN LINE. In one flex row it ran
   between and around the buttons, so the control and its explanation read as
   one broken sentence. */
.meta[role="group"] { flex-wrap: wrap; align-items: center; }
.meta[role="group"] > .data-sm { flex-basis: 100%; margin-top: 4px; }

/* ── the pitch on Review ──────────────────────────────────────────────────
   ⚠ NO GREEN RECTANGLE. A drawn pitch spends its contrast on decoration and
   leaves the scores, which are the content, fighting it. The rows carry the
   shape; the shirts carry the numbers. */
.pitch { display: flex; flex-direction: column; gap: 8px; margin: 10px 0 12px; }
/* ⚠ NOWRAP, DELIBERATELY. A wrapped line is not a smaller pitch, it is a
   DIFFERENT SHAPE: a five-man midfield that folds onto two rows reads as 4-1,
   and the row is there to show the shape. With nowrap a future regression
   overflows, which the layout gate already fails on, instead of silently
   restacking into a formation nobody played. */
.pitch-row { display: flex; justify-content: center; gap: 6px; flex-wrap: nowrap; }

/* ⚠ THE FIRST VERSION SAID "SIZED FOR FIVE ACROSS A 390px PHONE" AND WAS
   WRONG -- it set 64px and five of them wrapped on the first render. MEASURED
   2026-09-29: the row is 316px inside the card at a 390px viewport, so five
   shirts and four 6px gaps leave (316 - 24) / 5 = 58.4px each. `flex: 1 1 0`
   divides the row instead of asserting a width, so the arithmetic is the
   browser's and cannot go stale; the cap stops a lone goalkeeper stretching
   to the full 316. */
.shirt { flex: 1 1 0; max-width: 72px; min-width: 0;
         display: flex; flex-direction: column;
         align-items: center; gap: 1px; padding: 5px 3px;
         border: 1px solid var(--line); border-radius: 6px;
         background: var(--surface-raised); text-align: center; }
.shirt-top { display: flex; align-items: center; gap: 3px; max-width: 100%; }
/* 10px, not 11: at a 58px shirt the name box is ~48px, and 11px ellipsised
   "Calafiori" after five characters. */
.shirt-name { font-size: 10px; font-weight: 600; overflow: hidden;
              text-overflow: ellipsis; white-space: nowrap; }
.shirt-pts { font: 700 15px/1.1 system-ui, sans-serif; }
.shirt-pred { font-size: 9px; color: var(--ink-muted); }
.shirt-c, .shirt-up { font: 700 9px/1 system-ui, sans-serif; flex: none;
                      padding: 2px 3px; border-radius: 3px; }
.shirt-c { background: var(--turf); color: var(--on-turf); }
.shirt-up { background: var(--turf-soft); color: var(--turf); }
/* ⚠ A BLANK AND A ZERO ARE DIFFERENT THINGS. A player who did not appear
   scored 0 and so did a player who played 90 minutes badly; only one of them
   tells you anything about the pick. */
.shirt.dnp { opacity: .55; border-style: dashed; }
.shirt-dnp { font-size: 9px; color: var(--ink-muted); }
.shirt.up { border-color: var(--turf); }

.bench-strip { display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
               padding-top: 10px; border-top: 1px solid var(--line); }

/* ⚠ `.num` IS SCOPED TO `.prow`, so the two spans here were inline and ran
   together as "+7points". The gain and its unit stack. */
.split-row .num { display: flex; flex-direction: column; align-items: flex-start; }
.split { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; }
.split-row { display: grid; grid-template-columns: 76px minmax(0, 1fr);
             gap: 12px; align-items: start;
             padding-top: 10px; border-top: 1px solid var(--line); }
.split-row:first-child { border-top: 0; padding-top: 0; }

/* the decision record's provenance line spans the row rather than sharing it
   with the "Recorded" chip -- see deadline.js. `anywhere` so a longer
   filename breaks instead of pushing the column. */
.check .dec-source { grid-column: 1 / -1; overflow-wrap: anywhere; }

/* ── club mark ────────────────────────────────────────────────────────────
   The club's colour and its three-letter code, as one element. See the club
   primaries block above for why the two cannot be separated: the colour alone
   does not identify a club and never will, so nothing paints a club colour
   without printing the code inside it. */
.club-mark {
  display: inline-block;
  min-width: 2.6em;
  padding: 1px 5px;
  border-radius: 3px;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.5;
  font-weight: 600;
  letter-spacing: .04em;
  text-align: center;
  vertical-align: baseline;
}
/* An unknown code is visibly unpainted rather than quietly grey: a grey chip
   reads as a club whose colour happens to be grey. */
.club-unknown {
  background: transparent;
  color: var(--ink-muted);
  border: 1px dashed var(--line);
}

/* ── Office and Scout rows ────────────────────────────────────────────────
   ⚠ NOT `.prow`. That row is a fixed six-column 982px grid built for Plan's
   side-by-side comparison, and it is only readable because it sits inside
   `.scroll-x`, which scrolls the container rather than the page. Reusing it
   for a label-and-value row put 739px of grid in a 390px phone and scrolled
   the whole document sideways -- every card, not just the row. These rows
   size to their content instead, so they fit any width without scrolling. */
.orow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-4);
  align-items: baseline;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}
.orow:last-child { border-bottom: 0; }
.orow > :last-child { text-align: right; white-space: nowrap; }
/* The label may wrap; the number may not, so the number wins the space it
   needs and the label takes what is left. */
.orow > :first-child { min-width: 0; }

/* The gameweek ledger: four narrow numeric columns that must stay on one
   line at 390px, so they are sized in fractions rather than pixels. */
.grow {
  display: grid;
  grid-template-columns: 4rem minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-2);
  align-items: baseline;
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}
.grow > * { text-align: right; }
.grow > :first-child { text-align: left; }
.grow:last-child { border-bottom: 0; }

/* Filter buttons wrap instead of forcing a minimum width. */
.filters { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-4); }
.filters .btn.on { background: var(--turf); border-color: var(--turf); color: var(--on-turf); font-weight: 600; }


/* ── scenes ───────────────────────────────────────────────────────────────
   Rule 1 of the design pack: each background photo sits at saturation 0.8
   under a wash of the page colour, and DATA NEVER SITS STRAIGHT ON A PHOTO.
   Both halves matter and the second is what keeps the contrast gates valid:
   every number is on a `.card`, which is opaque, so text contrast is still
   text-against-card and the measured ratios do not move.

   ⚠ TWO FIXED LAYERS, NOT A FILTER ON `body`. `filter` on an element applies
   to its descendants, so saturating the body would desaturate every card,
   chip and fixture colour drawn on top of it -- including the fixture scale,
   whose separation was measured at full saturation. The photo is saturated in
   its own layer and nothing else is touched.

   ⚠ AND `position: fixed` RATHER THAN A SCROLLING BACKGROUND, because these
   pages are long on a phone: a 956px-tall photo scrolled with the content
   would run out part-way down and the page would change colour mid-read.

   ⚠ NO SCENE IS THE SUPPORTED STATE. A page without `data-scene` gets the
   plain surface and looks finished -- the pseudo-elements only paint when the
   variable is set, so season, stats and squad are unaffected and a missing
   image degrades to the background it already had.

   ⚠ AND ONLY SCOUT PAINTS FULL PAGE, which is four screens fewer than this
   block used to cover. Checked by sampling each render's page background well
   below its content: scout-scene.png comes back (143,152,125) and (121,129,
   113) -- a photograph -- while review-scene.png and office-scene.png both
   come back EXACTLY (245,244,239), which is --surface to the byte, and
   plan-scene.png is white. Those three, and deadline-scene.png, put their
   picture in a BOUNDED band with the page plain around it; only Scout stands
   its whole page on one. Deadline was painting it twice, once here and once
   in its own `.scene-band`, and nobody could see the second one through the
   first. */
body[data-scene="scout"]::before,
body[data-scene="scout"]::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}
body[data-scene="scout"]::before {
  background-image: var(--scene);
  background-size: cover;
  background-position: center top;
  filter: saturate(var(--scene-sat));
}
body[data-scene="scout"]::after { background: var(--scene-wash); }

/* ⚠ THE PAGE HEAD IS A PLATE TOO, AND IT WAS NOT. The first build of this
   block put a photograph behind every screen and left `.page-head` on it --
   so the title, the subtitle and the whole `THIS RUN` stamp sat straight on
   the picture. Measured by climbing each text node to its nearest opaque
   background: 7 unplated nodes on Review and Plan, 8 on Office and Scout, 10
   on Deadline, which included the deadline time and both bank figures.

   ⚠ I HAD ASSERTED THE OPPOSITE ONE STEP EARLIER -- "every number is on a
   .card, which is opaque, so the measured ratios do not move". The cards are
   opaque and the claim was still false, because the page head is not a card.
   Checking it was Amr's instruction and the check is what found it.

   Scoped to `body[data-scene]` so the plain pages keep the open header they
   already had: there is nothing behind them to be read against. */
/* ⚠ `> .label` AND `> .note-sm` JOINED `> p` FOR THE SAME REASON IT EXISTS.
   Plan's within-position banner is a div.label and a direct child of the
   page, so it sat straight on the photograph carrying two percentages --
   the shape this rule was written for, in an element it did not name. */
/* ⚠ AND THIS FOLLOWS THE PHOTOGRAPH, because it only exists because of it.
   A plate under the page title is right when there is a picture behind it
   and wrong when there is not: every render with a plain page draws its
   title as bare text on the surface, and plating it there turns the heading
   into a card the pack does not have. */
/* ⚠ SCOUT'S HEADER IS A FULL-WIDTH WHITE BAND, NOT A CARD. Measured down
   the left edge of scout-scene.png: pure white to y=68, then the photograph.
   It is the only screen whose title sits on a picture, so it is the only one
   that needs this -- and a rounded, inset card would read as a panel where
   the render has a strip. The negative margins reach the page edge and the
   padding puts the text back where it was. */
/* ⚠ GATED ON THE PADDING IT CANCELS, NOT ON THE USUAL 760. `.page` is
   padded 32px here and var(--space-4) at 640 and below, so a flat -32px
   overshot the phone by 16 and put a scrollbar on the page -- measured, not
   guessed: left -16, width 422 in a 390 viewport. The breakpoint is 641
   because that is where the number this rule subtracts changes. And the
   phone does not want the band anyway: scout-phone.png puts the title
   straight on the photograph. */
@media (min-width: 641px) {
  body[data-scene="scout"] main.page > .page-title {
    background: var(--surface-raised);
    margin-left: -32px; margin-right: -32px;
    padding: 8px 32px 0;
  }
}

body[data-scene="scout"] .page-head,
body[data-scene="scout"] main.page > .label,
body[data-scene="scout"] main.page > .note-sm,
body[data-scene="scout"] main.page > p {
  background: var(--surface-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 16px;
}
/* ⚠ `main.page > p` RATHER THAN THE TWO KNOWN OFFENDERS. Review's "not a
   backtest" footer and Plan's "selling to fund a later move" note are bare
   paragraphs appended beside the cards, and naming them individually would
   leave the next one unplated. A direct paragraph child of the page is, by
   construction, prose with nothing behind it. */

/* ⚠ THREE SCREENS NOW HAVE A PHONE SCENE OF THEIR OWN. The desktop
   pictures are landscape rooms and a landscape pitch; cropped to a phone
   they become a patch of wall or a patch of grass. These three were chosen
   portrait for the narrow viewport and are swapped at the same 760px the
   rest of this sheet switches on. */
body[data-scene="review"]   { --scene: url("../img/scenes/review.jpg"); }
body[data-scene="office"]   { --scene: url("../img/scenes/office.jpg"); }
body[data-scene="scout"]    { --scene: url("../img/scenes/scout.jpg"); }
body[data-scene="plan"]     { --scene: url("../img/scenes/plan.jpg"); }
body[data-scene="deadline"] { --scene: url("../img/scenes/deadline.jpg"); }
@media (max-width: 759px) {
  body[data-scene="review"] { --scene: url("../img/scenes/review-phone.jpg"); }
  body[data-scene="office"] { --scene: url("../img/scenes/office-phone.jpg"); }
  body[data-scene="plan"]   { --scene: url("../img/scenes/plan-phone.jpg"); }
}
/* The press conference has its own room in the pack: confirmation-press.jpg,
   installed as announced.jpg. Without it the band fell back to the changing
   room's lockers, which is a different place. */
body[data-scene="announced"] { --scene: url("../img/scenes/announced.jpg"); }
/* Each onboarding step has its own room in the pack. */
[data-scene-band="onb-signing"] { --scene: url("../img/scenes/onb-signing.jpg"); }
[data-scene-band="onb-finance"] { --scene: url("../img/scenes/onb-finance.jpg"); }
[data-scene-band="onb-physio"] { --scene: url("../img/scenes/onb-physio.jpg"); }
[data-scene-band="onb-board"] { --scene: url("../img/scenes/onb-board.jpg"); }
[data-scene-band="onb-analyst"] { --scene: url("../img/scenes/onb-analyst.jpg"); }
[data-scene-band="onb-fans"] { --scene: url("../img/scenes/onb-fans.jpg"); }

/* ── the build stamp, folded away ─────────────────────────────────────────
   Five lines of run ids and simulation settings used to sit under every page
   title. One sentence that changes a decision stays out; the rest is one tap
   away, because the ids are what someone quotes when reporting a page wrong. */
.stamp { margin-top: 8px; }
.stamp > summary {
  cursor: pointer;
  min-height: 44px;
  display: flex;
  align-items: center;
  font-size: 13px;
  color: var(--ink-muted);
}
.stamp[open] > summary { margin-bottom: 4px; }
.stamp > div { margin-top: 2px; }

/* Prose is not monospace. `.data-sm` stays for figures, codes and aligned
   columns; sentences move here. Monospace at 12px is what makes the app read
   as machine output, which is the thing a reader outside this project notices
   first. */
.note-sm { font-size: 15px; line-height: 22px; color: var(--ink-muted); }

/* ── approve ──────────────────────────────────────────────────────────────
   The one action in the app. It used to sit in the page header, 3,374px above
   the checkbox that gates it, so the control and its own precondition were
   never on screen together. It now closes the Pre-flight card: checks, then
   question, then button. Full width because a 94px pill aligned right is a
   secondary control, and this is not one. */
.approve-row { margin-top: 16px; display: flex; flex-direction: column; gap: 8px; }
.btn.approve { width: 100%; height: 48px; font-size: 16px; }

/* ── phone: stack what does not fit ───────────────────────────────────────
   ⚠ MEASURED, NOT ASSUMED. At 390px, `.prow` draws 1100px of grid into a
   316px window: 784px — 71% of every row on Plan and Review — is off-screen,
   including the six-fixture strip, which is the most FPL-native thing on
   either page and is unreachable at every scroll offset because the sticky
   name cell eats 210 of the 316 visible pixels. `.srow` on Deadline hides
   226px of 582 (39%), and Scout's table hides 310px of 626 (50%), including
   the column it is sorted by.

   ⚠ SCROLLING WAS THE WRONG REMEDY, NOT THE WRONG GOAL. The comments at the
   grid definitions above are right that HIDING these columns was worse. A
   horizontal scroller inside a vertically-scrolling page is a gesture most
   readers never try, so the content is not hidden — it is invisible while
   appearing complete, which is the quieter failure. Below 560px the rows
   stack instead; nothing is dropped.

   ⚠ AND THE STICKY FIRST CELL GOES WITH THEM. It exists to keep a player's
   name in view while you scroll sideways. With nothing to scroll sideways it
   is a 210px column of dead space holding the row open. */
@media (max-width: 560px) {
  /* ⚠ THE MIN-WIDTH GOES, THE SCROLLER STAYS. Setting `overflow-x: visible`
     here also released the scroller around Scout's, Season's and Stats' real
     TABLES, which cannot stack with CSS -- so they pushed the document wide
     and three pages gained horizontal page scroll, which is worse than the
     container scroll it replaced. Dropping only the 1100px floor lets the
     stacked grids fit, and leaves `overflow-x: auto` doing nothing when the
     content does fit and its job when it does not. */
  .scroll-x > .inner { min-width: 0; }

  .prow,
  .prow-head {
    grid-template-columns: minmax(0, 1fr);
    gap: 6px;
    padding: 14px 0;
  }
  .prow > :first-child,
  .prow-head > :first-child {
    position: static;
    background: transparent;
    box-shadow: none;
  }
  .prow .num { text-align: left; align-items: flex-start; }
  /* the head row labels columns that no longer exist side by side */
  .prow-head { display: none; }

  .alts { padding-left: 0; }
  .alt { grid-template-columns: 24px minmax(0, 1fr); row-gap: 2px; }

  .srow { grid-template-columns: 38px minmax(0, 1fr); row-gap: 4px; }
  /* the fixture strip spans the full width rather than a 258px track */
  .srow > :nth-child(n + 3) { grid-column: 2 / -1; }
}

/* The "show all" control under a capped table. Full width and 44px because it
   is the only way to reach the rest of the list. */
.btn.show-all { width: 100%; margin-top: 12px; height: 44px; font-size: 15px; }

/* ── The Office: staff strip and report ───────────────────────────────────
   The design pack's office screen: five members of staff across the top with
   their mood, tap one to put their report on the big screen. */
.page-title { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.page-title > .note-sm { flex-basis: 100%; margin-top: 4px; }
/* ⚠ THE TITLE'S EXEMPTION FROM THE PLATING RULE WAS MEASURED FOR --ink AND
   NOT FOR --ink-muted. The pack draws "The Office · after GW5" straight on
   the photograph and that is allowed, because a title is not a number -- but
   the measurement behind the exemption is 6.09:1 for full ink, and the muted
   half of the same line came out at 1.75:1 against the same crop. Measured
   2026-10-05, not assumed. On a scene page the whole title line is ink. */
body[data-scene] .page-title .label,
body[data-scene] .page-title .display-sub { color: var(--ink); }

.staff-strip {
  display: flex; gap: var(--space-2); overflow-x: auto; padding: 4px 0 8px;
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
}
.staff-strip::-webkit-scrollbar { display: none; }
.staff {
  flex: 0 0 auto; width: 88px; min-height: 44px;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 8px 4px; border: 1px solid transparent; border-radius: var(--radius-lg);
  background: var(--surface-raised); cursor: pointer; font: inherit; text-align: center;
}
.staff.on { border-color: var(--turf); background: var(--turf-soft); }
.staff .face { width: 56px; height: 56px; border-radius: 50%; object-fit: cover; }
.staff-name { font-size: 13px; font-weight: 650; color: var(--ink); }
/* the mood may be two words and must not clip to one */
.staff-mood { font-size: 11px; line-height: 14px; color: var(--ink-muted); }
.staff-mood.turf { color: var(--turf); font-weight: 600; }
.staff-mood.danger { color: var(--danger); font-weight: 600; }
.staff-sum { display: none; }

/* ⚠ A VERTICAL RAIL FROM 760px, A HORIZONTAL STRIP BELOW IT. The two
   4 Oct renders differ on purpose: office-scene.png lists the five down the
   side with a figure under each name, office-phone.png tiles them across
   with the figure dropped, because a phone has the width for five faces or
   for one line of numbers and not for both. */
@media (min-width: 760px) {
  .staff-strip { flex-direction: column; gap: 6px; overflow: visible; }
  .staff {
    width: auto; flex: 0 0 auto; flex-direction: row; align-items: center;
    gap: var(--space-4); text-align: left; padding: 8px 12px;
  }
  .staff .face { width: 44px; height: 44px; flex: none; }
  .staff-name { flex: 0 0 auto; font-size: 15px; }
  /* The mood is pushed to the right edge, where the render hangs it. */
  .staff-mood { margin-left: auto; order: 3; font-size: 13px; }
  .staff-sum {
    display: block; order: 2; flex: 1 1 auto; min-width: 0;
    font-size: 12px; line-height: 16px; color: var(--ink-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
}

.report-head { display: flex; align-items: center; gap: var(--space-4); margin-bottom: 10px; }
.report-head .face.lg { width: 64px; height: 64px; border-radius: 50%; object-fit: cover; }
.mood-line { margin: 0 0 14px; }

.mtiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; }
/* ⚠ THE STANDING FIGURE GRID, under the brief rather than behind the pager.
   Amr, 2026-10-08: the Board's figures were inside a hash-picked sentence —
   "it says his mood, the placeholder". `office-review.json` puts them in
   metric tiles, and his rule is that a voice string is never a figure's only
   carrier. Twelve of them, so they are smaller and denser than a pager tile:
   the pager's four get 28px, these get 22px, and both stay well clear of the
   note beneath. */
.mtiles.grid { margin: 12px 0 0; gap: 8px; }
/* ── the figure grid on the wall screen ───────────────────────────────────
   ⚠ THREE ACROSS AND A FAINT RULE BETWEEN GROUPS. Amr, 2026-10-09 grouped the
   twelve himself and asked for "faint seperation" — a border, not a card
   each, because a card inside the slide plate would be a plate on a plate.
   ⚠ SIZED FOR 420x283. This plate is nearly square, so the band is 611px
   where the portrait one gave 933 and the screen lost 157px of height. Twelve
   tiles in four rows means about 62px a tile: 11px label, 19px figure, 10px
   note. Measured against the rendered box, not chosen. */
.figgrid { display: flex; flex-direction: column; gap: 0; }
.figrow { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
          gap: 6px 8px; padding: 7px 0; }
.figrow.ruled { border-top: 1px solid var(--line); }
.figgrid .mtile { border: 0; padding: 3px 6px; border-radius: var(--radius-sm); }
.figgrid .mtile-k { font-size: 10.5px; line-height: 13px; }
.figgrid .mtile-v { font-size: 19px; line-height: 23px; }
.figgrid .mtile-n { font-size: 9.5px; line-height: 12px; }
/* ⚠ ONLY THE PACE ROW IS TONED, so the colour means something at a glance.
   The same test drives the mood chip on the rail, so the two cannot disagree. */
.figgrid .mtile.t-good { background: var(--turf-soft); }
.figgrid .mtile.t-good .mtile-v { color: var(--turf); }
.figgrid .mtile.t-bad  { background: var(--danger-soft); }
.figgrid .mtile.t-bad  .mtile-v { color: var(--danger); }
.mtiles.grid .mtile { padding: 8px 10px; }
.mtiles.grid .mtile-k { font-size: 11.5px; line-height: 15px; }
.mtiles.grid .mtile-v { font-size: 22px; line-height: 27px; }
.mtiles.grid .mtile-n { font-size: 11px; line-height: 14px; }

/* ── a line chart ─────────────────────────────────────────────────────────
   ⚠ PLATED AT EVERY WIDTH, OUTSIDE ANY MEDIA QUERY. `.lc-card` is a `.card`
   and therefore opaque, which is what satisfies `scenes: no data is drawn
   straight on a photograph` — that gate runs at 390px and stops its walk at
   `.scene-band`. A chart plated only from 760px sits raw on the photograph on
   a phone, which is exactly how the slides failed the first time. */
.lc-card { padding: 12px 14px; }
.lc { width: 100%; height: auto; display: block; }
.lc-rule { stroke: var(--ink-muted); stroke-width: 1; stroke-dasharray: 5 4; opacity: 0.65; }
.lc-rlab, .lc-x, .lc-y { fill: var(--ink-muted); font-size: 10px;
                         font-family: inherit; }
.lc-line { stroke-width: 2; fill: none; }
.lc-dot { stroke: var(--surface-raised); stroke-width: 1.5; }
.lc-line.lc-you  { stroke: var(--turf); }
.lc-dot.lc-you   { fill: var(--turf); }
.lc-line.lc-fld  { stroke: var(--ink-muted); }
.lc-dot.lc-fld   { fill: var(--ink-muted); }
/* ⚠ THE PREDICTION IS DASHED AND IT IS NOT DECORATION. `absolute_valid` is
   false, so the last point is an Analyst Prediction on an untested scale
   sitting beside five settled scores. A solid line would say they are the
   same kind of number. */
.lc-line.lc-fc   { stroke: var(--amber); stroke-dasharray: 5 4; }
.lc-dot.lc-fc    { fill: var(--amber); }
.lc-line.lc-rank { stroke: var(--turf); }
.lc-dot.lc-rank  { fill: var(--turf); }
.lc-note { font-size: 11.5px; line-height: 16px; color: var(--ink-muted); margin: 4px 0 0; }
.lc-rlab { text-anchor: start; }
.lc-key { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 6px 0 4px; }
.lc-k { font-size: 11px; color: var(--ink-muted); display: inline-flex;
        align-items: center; gap: 5px; }
.lc-k::before { content: ""; width: 14px; height: 0; border-top: 2px solid currentColor; }
.lc-k.lc-you  { color: var(--turf); }
.lc-k.lc-fc   { color: var(--amber); }
.lc-k.lc-fc::before { border-top-style: dashed; }
.lc-k.lc-rank { color: var(--turf); }

/* ── the wide tables, below the scene ─────────────────────────────────────
   ⚠ FULL WIDTH BY AMR'S OWN RULE: "unless a box below the scene spans the full
   page width, in which case the scene row ends there". Eleven columns fit
   neither the 460px panel nor the 458px screen, and dropping columns to make
   them fit would be deciding for the reader which of his own weeks matter. */
.office-tables { margin: 14px 0 0; }
.office-pace { margin: 14px 0 0; }
.wide-wait { padding: 18px 20px; }
.wait-list { margin: 8px 0 10px; padding-left: 20px; display: flex; flex-direction: column; gap: 5px;
             font-size: 13.5px; line-height: 19px; color: var(--ink); }
/* ⚠ A DEFINITE BOX THAT SCROLLS, FOR THE SAME REASON THE PANEL IS ONE. The
   staff put different amounts here — the Board's season table is five rows of
   eleven columns, the Physio's flags are one — and a region sized by its
   content brings back exactly the fault Amr reported on 2026-10-08: "office
   page gets longer and shoter because the left box keeps reing". The height
   gate asserts the page is the SAME height on all five staff to within 2px,
   so this has to be fixed and scroll inside, not grow.
   ⚠ DESKTOP ONLY. On a phone there is one column, nothing to match, and a
   capped region would hide rows behind a second scrollbar inside a scroll. */
@media (min-width: 760px) {
  /* ⚠ FULL WIDTH AND ONE CHILD, AND THE SCREENSHOT IS WHY. First it was a
     stack inside 470px, which hid the table behind an inner scroll -- a scroll
     within a scroll. Then it was two columns, which clipped four of the season
     table's eleven headings off the right edge. Eleven columns want the page.
     ⚠ FIXED HEIGHT, because the staff put different amounts here and a region
     sized by its content is the fault Amr reported: "office page gets longer
     and shoter". The height gate asserts 2px across all five. */
  /* ⚠ TALLER NOW, AND THE NEW PLATE PAID FOR IT. The nearly-square boardroom
     drops the band from 933px to 611, so the page has about 320px back; the
     charts came down off the screen into that space. Still a FIXED height,
     because the five staff put different amounts here and a content-sized
     region is the fault Amr reported: "office page gets longer and shoter". */
  .office-tables { height: 860px; overflow: auto; overscroll-behavior: contain;
                   display: flex; flex-direction: column; gap: 14px; }
  .office-tables > * { flex: 0 0 auto; margin: 0; }
  /* ⚠ THE ROADMAP CARD STRETCHES TO FILL, because the region is a fixed box
     and a persona not yet ported would otherwise sit in 700px of nothing. A
     tall card that says what is coming is honest; a tall void reads as broken.
     It shrinks back to a normal card the week that staff member's tables land. */
  .office-tables > .wide-wait { flex: 1 1 auto; display: flex;
                                flex-direction: column; justify-content: center; }
  .chartrow { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
  .office-tables > * { margin: 0; }
}
.office-tables .tbl { font-size: 12.5px; }
.office-tables .tbl th:first-child, .office-tables .tbl td:first-child { white-space: nowrap; }
.office-tables .tbl td.good { color: var(--turf); font-weight: 600; }
.office-tables .tbl td.bad  { color: var(--danger); font-weight: 600; }
.adds-ok  { color: var(--turf); font-weight: 600; margin-top: 10px; font-size: 12.5px; line-height: 17px; }
.adds-bad { color: var(--danger); font-weight: 700; margin-top: 10px; font-size: 12.5px; line-height: 17px; }
.mtile { border: 1px solid var(--line); border-radius: var(--radius-md); padding: 10px 12px; }
.mtile-k { font-size: 13px; color: var(--ink-muted); }
.mtile-v { font-size: 28px; line-height: 34px; font-weight: 700; }
.mtile-n { font-size: 12px; line-height: 16px; color: var(--ink-muted); }
/* The Board's dashed box: the things it declines to report. Dashed, because
   the render draws it as a held space rather than a finding -- a solid card
   would read as a fourth report. */
.declines { border: 1px dashed var(--line); border-radius: var(--radius-md);
            padding: 12px 14px; margin: 14px 0 0; font-size: 14px;
            line-height: 21px; color: var(--ink-muted); }
/* Three tiles fit across at desktop; at 2 columns the third sits alone. */
@media (min-width: 760px) { .mtiles { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.para { border-top: 1px solid var(--line); padding-top: 12px; margin-top: 12px; }
.para .body { margin: 0 0 4px; font-size: 16px; line-height: 24px; }
/* ⚠ THE ONE PLACE MONOSPACE IS RIGHT ON THIS SCREEN: a field path, not prose.
   The pack prints it under every paragraph so a reader can see which part of
   the feed produced the sentence. */
.prov { font-family: var(--font-mono); font-size: 12px; line-height: 16px; color: var(--ink-muted); }

/* ── house-style components ───────────────────────────────────────────────
   The pack's shared vocabulary: a segmented control, a shirt carrying one big
   number, a club roundel, and bars that are hatched rather than empty when
   the data is not there. Built once; four screens use them. */
.seg {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  background: var(--surface-raised); border: 1px solid var(--line);
  border-radius: 999px; padding: 4px; gap: 2px;
}
.seg-opt {
  min-height: 44px; border: 0; border-radius: 999px; background: transparent;
  font: inherit; font-size: 15px; font-weight: 700; color: var(--ink);
  cursor: pointer; padding: 0 10px; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
/* ⚠ THREE LONG LABELS IN A 356px PILL. "Keepers + fwds 5" at 15px does not
   fit three across on a phone and the control overflowed the page by seven
   pixels -- enough to scroll, not enough to see. 44px stays: this is a
   touchscreen read under time pressure. */
.page.deadline .seg-opt { font-size: 13px; padding: 0 4px; white-space: normal;
                          line-height: 16px; }
.seg-opt.on { background: var(--turf); color: var(--on-turf); }

.shirt-u { position: relative; display: flex; flex-direction: column; align-items: center; min-width: 0; }
.shirt-u .shirt-club { margin-bottom: -12px; z-index: 2; }
/* position:relative so the captain's armband anchors to the shirt and not to
   the whole stack -- pinned to .shirt-u it landed behind the name chip. */
.shirt-u .shirt-body {
  position: relative;
  width: 96px; padding: 16px 6px 12px; text-align: center;
}
/* The silhouette, behind the name and number. See FPL.shirt for why this is
   a stroked path and not the clip-path it replaced. */
.shirt-shape { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; }
.shirt-body > .shirt-name, .shirt-body > .shirt-num { position: relative; z-index: 1; }
.shirt-u.sm .shirt-body { width: 64px; padding: 15px 4px 6px; }
/* ⚠ THE NAME IS CUT BY THE TORSO, AND THE TORSO IS 14%-86%. The pack cuts
   surnames across the shoulders on purpose -- "JOAO PED", "DE CUYPE" -- and
   while the silhouette was a clip-path that happened for free. A stroked path
   clips nothing, so the name ran out past the shirt into the card. The same
   72% the path uses, so the two cannot drift apart. */
.shirt-name, .shirt-u .shirt-num { width: 72%; margin-left: auto; margin-right: auto; }
.shirt-name { font-size: 10px; font-weight: 700; letter-spacing: 0.04em; color: var(--ink-muted);
  overflow: hidden; text-overflow: clip; white-space: nowrap; }
.shirt-num { font-size: 32px; line-height: 36px; font-weight: 800; font-variant-numeric: tabular-nums; }
.shirt-u.sm .shirt-num { font-size: 22px; line-height: 26px; }
.shirt-sub { font-size: 10px; font-weight: 700; color: var(--ink-muted); }
.shirt-cap { margin-top: 6px; font-size: 12px; line-height: 16px; color: var(--ink-muted); text-align: center; max-width: 150px; }
.shirt-badge {
  position: absolute; top: 18px; right: 2px; z-index: 3;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--ink); color: var(--surface-raised);
  font-size: 12px; font-weight: 800; display: flex; align-items: center; justify-content: center;
}
.shirt-u.dim { opacity: 0.55; }

/* ⚠ flex:none AND box-sizing, OR IT IS AN ELLIPSE. A three-letter pill is
   wider than 30px at any comfortable size, so a flex item with a width and no
   shrink rule simply grew -- the roundel came out as a lozenge on every shirt
   and still looked deliberate. */
.club-circle {
  display: inline-flex; align-items: center; justify-content: center;
  flex: none; box-sizing: border-box;
  width: 38px; height: 38px; border-radius: 50%; overflow: hidden;
  font-family: var(--font-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.02em;
  border: 2px solid var(--surface-raised);
}
.club-circle.club-unknown { background: transparent; color: var(--ink-muted); border: 1px dashed var(--line); }

/* ⚠ HATCHED, NOT EMPTY. The house-style sheet's own rule: a bar with no data
   behind it must not look like a bar whose value is zero. */
.bar { display: inline-block; width: 72px; height: 8px; border-radius: 4px; background: var(--line); vertical-align: middle; }
.bar-fill { display: block; height: 100%; border-radius: 4px; background: var(--turf); }
.bar.hatched {
  background-image: repeating-linear-gradient(135deg,
    var(--line) 0 3px, transparent 3px 6px);
  background-color: transparent;
  border: 1px solid var(--line);
}

/* ── Review: one pitch behind a three-way control ─────────────────────────
   The pack puts the photograph behind the eleven rather than behind the whole
   page, so the scene stops being a backdrop and becomes the pitch itself. */

/* ⚠ RULE 1 IS NOT RELAXED HERE, ONLY SCOPED. `data-scene` stays on <body> so
   the plating gate keeps running against this page; the page-wide wash is
   switched off and the same image plus the same wash are painted inside the
   panel instead. Removing the attribute would have taken the gate with it. */
body[data-scene="review"]::before,
body[data-scene="review"]::after { background-image: none; background: none; }

.review-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; }
.seg-wrap { margin: 12px 0; }
.seg-opt.off { opacity: 0.4; cursor: not-allowed; }

/* ⚠ THE SHIRT IS MEASURED OFF review-phone.png, NOT CHOSEN. The four row
   centres are percentages from the pack. Taken off the render at 2x: roundel
   30, shirt body 52 with a 24px number, name chip 17, collar overlap 13 — a
   stack of 93. The first build drew 103 into it and the midfield's collars
   landed on the defenders' names.

   ⚠ 620 ON A PHONE, AGAINST THE RENDER'S 538, AND THE REASON IS THE MARKS.
   At 538 those percentages leave 97px between the tightest pair of rows, so
   a 93px shirt fits and "↓ didn't play" has nowhere to go: it landed on the
   player above or beside it, pointing at the wrong man. The row positions
   are the design and the type size is what makes it readable one-handed, so
   the panel is what gives. At 620 the gaps are 112/136/112 and a one-line
   mark has 19px of clearance. No shirt in the render carries a mark, which
   is why the geometry held there and not here. */
.pitch-panel {
  position: relative; overflow: hidden;
  min-height: 620px; border-radius: var(--radius-lg);
  background: var(--turf-soft);
}
/* ⚠ THE PITCH INSIDE THE PHOTOGRAPH, MEASURED NOT GUESSED. Scanning
   review.jpg for green gives the grass at x 22%–77% and y 31%–84% of the
   image. The panel is pinned to the image's own aspect ratio (1151×1329) so
   `cover` crops nothing, which is what lets those image percentages be panel
   percentages. Move the picture and these four numbers have to be retaken --
   they are a fact about this photograph, not about pitches. */
.pitch-panel::before,
.pitch-panel::after { content: ""; position: absolute; inset: 0; z-index: 0; }
.pitch-panel::before {
  background-image: var(--scene); background-size: cover; background-position: center;
  filter: saturate(var(--scene-sat));
}
.pitch-panel::after { background: var(--scene-wash); }

/* ⚠ THE DEFAULTS ARE HERE, NOT ONLY IN THE JAVASCRIPT THAT SETS THEM. A
   var() with no definition anywhere renders its fallback, or nothing -- and a
   shirt whose position silently failed would pile at the panel's top-left and
   still look like a drawn page. Centred is the degraded state: visibly wrong,
   not plausibly right. */
.pitch-slot {
  --xp: 50%; --yp: 50%; --spanp: 84%;
  --xd: 50%; --yd: 50%; --spand: 60%;
  position: absolute; z-index: 1;
  left: var(--xp); top: var(--yp);
  width: var(--spanp); max-width: 120px;
  transform: translate(-50%, -50%);
}

/* ⚠ OPAQUE, BECAUSE IT CARRIES TEXT OVER A PHOTOGRAPH. The previous build
   measured 1.8:1 for a caption laid straight on the scene. */
/* ⚠ TWO LINES IS A CONSTRAINT, NOT A STYLE. The forwards sit at 76% of the
   panel and this plate is opaque and above them, so every line it gains hides
   a shirt. Measured: 2 lines leave 36px of clearance, 3 lines take 18 of it
   back and a marked forward disappears behind the plate. Keep each caption
   short enough to wrap to two at 390px. */
.pitch-caption {
  position: absolute; z-index: 2; left: 12px; right: 12px; bottom: 8px;
  background: var(--surface-raised); border-radius: 10px;
  padding: 8px 12px; font-size: 13px; line-height: 18px; color: var(--ink);
}
.pitch-arrow {
  position: absolute; z-index: 3; top: 42%; transform: translateY(-50%);
  width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.86); color: var(--ink);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
}
/* ⚠ NOT ON A PHONE. The pack's phone render has no arrows, and at 390px a
   44px button centred on the panel lands on the defenders' name chips. The
   swipe and the segmented control above already move between the views, and
   the control is a real tablist, so nothing is lost for the keyboard. */
.pitch-arrow { display: none; }
/* ⚠ 4px AND 40px, BECAUSE THE ARROWS ARE A BOUND ON THE SPREAD. At 10px and
   44px they occupied 1.4%-7.5% of the panel at each edge, which is grass the
   wide midfielders now want. Pulled to the rim they take 0.6%-6.1% and the
   outermost shirt clears them. See ROWS_DESK in review.js for the pairing. */
.pitch-arrow.left { left: 4px; }
.pitch-arrow.right { right: 4px; }
@media (min-width: 760px) { .pitch-arrow { width: 40px; height: 40px; } }
.pitch-arrow[aria-disabled="true"] { opacity: 0.35; pointer-events: none; }

/* The shirt on the pitch: the club roundel over the collar, one big number in
   the body, the name and its meaning underneath. */
/* ⚠ THE SLOT SIZES TO ITS CONTENT, THE SHIRT DOES NOT. Pinning the whole
   stack to the shirt's width made every caption overflow its own box by four
   to eight pixels -- invisible on screen, and a clip the moment anything gets
   a longer name. The shirt is the fixed thing; the chip under it wraps. */
.pitch-slot .shirt-u { width: 100%; }
.pitch-slot .shirt-u .shirt-body {
  /* ⚠ 47, NOT 54: AMR'S 13% OFF THE JERSEY, TAKEN OFF THE WHOLE GARMENT.
     "Length" cannot be taken on its own -- the box is pinned to the asset's
     364:483 and `background-size: 100% 100%`, so squeezing the height alone
     would stretch the photograph and the name printed on it. 13% off both
     dimensions is 13% off the length with the jersey still the shape it is. */
  width: 100%; max-width: 47px; padding: 13px 2px 3px;
  /* ⚠ NO BORDER HERE ANY MORE. A 2px box border was how this shirt got its
     outline: the clip-path cut the rectangle into a shirt on its way out.
     With the silhouette drawn as a stroked path the border has nothing
     cutting it and shows as a square around the shirt, so it goes. The
     surname still does its own clipping on .shirt-name, where it belongs --
     the pack's renders cut it across the shoulders ("JOAO PED", "DE CUYPE").
     The shadow now traces the shirt rather than a box, which is the shape
     the render casts. */
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.28));
}
.pitch-slot .shirt-u .shirt-num { font-size: 24px; line-height: 26px; }
/* ⚠ THE ROUNDEL OVERLAPS THE COLLAR, NOT THE NAME. At -10 it sat across the
   surname and both became unreadable -- measured off the render, the circle's
   lower edge lands on the shirt's shoulder seam and the name begins just
   under it, which is what the 13px of top padding is for. */
.pitch-slot .shirt-u .shirt-name { font-size: 9px; line-height: 10px; }
/* ⚠ -18, NOT -13, AND THE 5px IS THE ROW SPACING'S. The disc went from 26
   to 29 on Amr's instruction and kept the same overlap, so it reached 3px
   further above the collar on a pitch whose rows had none to give. Pushing
   it down puts it ON the collar -- which is where the pack's render has it,
   with no grass showing between disc and shoulder -- and gives the caption
   row below the shirt the clearance it needs. The disc is still 29px. */
/* ⚠ THE OVERLAP SCALES WITH THE DISC, OR THE SMALL ONE IS BURIED. 18px of
   a 33px disc leaves 15 showing above the collar; 18px of a 27px disc would
   leave 9, so two thirds of it would be lying on the jersey. Both widths
   keep the same 55% on the shirt. The name's `top` is tied to whatever this
   is -- see the shirt-name rules, and check_layout.js measures the gap. */
.pitch-slot .shirt-u .shirt-club { margin-bottom: -15px; }
@media (min-width: 760px) {
  .pitch-slot .shirt-u .shirt-club { margin-bottom: -18px; }
}
/* ⚠ 34, AND THE PHONE MISSED THE LAST 13%. The previous increase was
   written as 26 -> 29 in the desktop block at the foot of this file, and
   this rule -- three classes to that one's three, but earlier -- is what
   the phone has always used. So the phone pitch stayed at 30 while the
   desktop went to 29, and the "bigger" disc was the smaller one. Both move
   together now. */
/* ⚠ 27 ON A PHONE, NOT 34, AND THAT IS A RATIO NOT A SIZE. Amr,
   2026-10-07: at 34 on a 47px shirt the disc was 72% of the garment's width
   -- "the disc becomes the object and the shirt the background, which is the
   wrong way round when the shirt carries the name and the number". The two
   ±13% changes compounded on the phone, where the shirt shrank at the same
   time. 27 is 57%, against the desktop's 33 of 66 = 50%. */
.pitch-slot .club-circle { width: 27px; height: 27px; }
.pitch-slot .club-pill { font-size: 9px; padding: 0 2px; letter-spacing: 0; }
/* ⚠ IT WRAPS AT DESKTOP RATHER THAN OVERHANGING. Measured: the panel is
   832px, five midfielders tile it in 100px slots with NO gap between them,
   and "Tavernier · MID" needs 117 at 13px -- so it clipped to
   "Tavernier · ...", dropping the position, which is the half the chip adds
   that the shirt does not already carry. Letting it overhang fixed the clip
   and produced two chip collisions at 1440, which is worse. The desktop
   rows are 170 to 190px apart and a stack is about 130, so the room is
   vertical: see the wrap rule in the desktop block. */
.pitch-slot .shirt-cap {
  margin-top: 3px; max-width: 100%; padding: 1px 4px;
  background: var(--surface-raised); border: 1px solid var(--line);
  border-radius: 6px; font-size: 11px; line-height: 13px; color: var(--ink);
  font-weight: 700;
}
/* ⚠ 11px AND 4px OF PADDING, MEASURED NOT CHOSEN. The phone slots tile
   flush at 65px with no gap, so a chip that overhangs touches its neighbour.
   Exactly two overflowed at 12px, each by 5px -- "Tavernier" wanting 69 in
   64 and the captain's "De Cuyper · 6 × 2" wanting 112 in 107 -- and a
   point off the type clears both without moving a slot. */
/* ⚠ ONE LINE, ALWAYS, AND THAT IS A CONSTRAINT ON THE ROW SPACING. The row
   centres leave 97px between the tightest pair and a shirt stands 93, so any
   chip that wraps to a second line puts its shirt into the row below. Names
   ellipsise; the captain's chip is allowed to be wider instead, because
   "6 × 2" is the reason his number is doubled and dropping it would leave a
   figure with no explanation.

   ⚠ DEVIATION FROM THE RENDER, DELIBERATE: the pack stacks the captain's
   "6 × 2" as a second chip under his name. That works in the render because
   its captain is a forward, with the caption plate's clearance under him; a
   midfield captain would have landed on the row below. */
.pitch-slot .shirt-cap {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pitch-slot .shirt-cap.cap { max-width: 100%; text-overflow: clip; }
.pitch-slot .shirt-cap .x2 { font-weight: 400; font-size: 11px; color: var(--ink-muted); }
/* ⚠ THE BOTTOM ROW'S MARK HANGS UPWARDS. Everywhere else it sits under the
   name chip; on the forwards' row that lands it behind the caption plate,
   which is opaque, so the mark would be computed, rendered and invisible --
   the exact failure this project keeps recording. */
.pitch-slot.low .shirt-mark { top: auto; bottom: 100%; margin: 0 0 2px; }

/* ⚠ THE STATUS CHIP IS TAKEN OUT OF THE STACK, NOT SHRUNK. "↑ was on your
   bench" is three lines at 70px and it was adding 50px to one shirt in a row
   of equal ones, which is what pushed Tzolakis onto De Cuyper. Pinned under
   the name chip instead, so a marked shirt occupies exactly what an unmarked
   one does and the rows stay on their measured centres. */
.pitch-slot .shirt-u { position: relative; }
/* ⚠ AS WIDE AS THE SHIRT, NOT AS WIDE AS THE WORDS. At 76px a mark reached
   into the next player's column and read as his -- "↓ didn't play" sat under
   Gakpo while it belonged to João Pedro. A label that points at the wrong
   player is worse than no label, so the mark is clipped to its own shirt and
   the full sentence lives in the card. */
.pitch-slot .shirt-mark {
  position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
  margin-top: 2px; width: 100%; min-width: 64px; max-width: none;
  white-space: nowrap; font-size: 10px; line-height: 12px; padding: 1px 3px;
  background: var(--surface-raised);
  box-shadow: inset 0 0 0 1px currentColor;
}
/* The position only fits beside the name on the wider layout. On a phone the
   name alone is the caption, which is what the pack's phone render shows. */
.cap-pos { display: none; }

.shirt-c {
  position: absolute; z-index: 3; bottom: 6px; right: -7px;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--ink); color: var(--surface-raised);
  font: 700 10px/18px system-ui, sans-serif; text-align: center;
}
.shirt-slot { font: 700 11px/14px system-ui, sans-serif; color: var(--ink-muted); }
.shirt-mark {
  margin-top: 4px; max-width: 72px; padding: 2px 5px; border-radius: 4px;
  font: 700 11px/14px system-ui, sans-serif; text-align: center;
}
.shirt-mark.amber  { background: var(--amber-soft);  color: var(--amber); }
.shirt-mark.danger { background: var(--danger-soft); color: var(--danger); }
.shirt-mark.turf   { background: var(--turf-soft);   color: var(--turf); }

/* ⚠ THE WHITE PILL IS WHAT MAKES THE COLOURED FILL SAFE. The code sits on
   white at a fixed contrast whatever the club's primary is, which is why the
   fill can be the real colour and the colour can mean nothing. */
.club-pill {
  background: transparent; color: inherit; border-radius: 0;
  padding: 0; font-family: var(--font-mono);
  font-size: 10px; font-weight: 700; letter-spacing: 0.02em;
}
.club-circle.club-unknown .club-pill { background: transparent; }
/* ⚠ THE PILL HAS TO LEAVE THE DISC VISIBLE. At 26px under a pill 24px wide
   the club colour survived as a 1px ring and the roundel read as an outline,
   not a badge. The render's bench roundel measures 29-30px across with the
   pill about two-thirds of it. */
.shirt-u.sm .club-circle { width: 30px; height: 30px; }
.shirt-u.sm .shirt-club { margin-bottom: -13px; }
.shirt-u.sm .club-pill { font-size: 8px; padding: 0 1px; max-width: 70%; }

/* Tally rows: one label, one figure, a rule between. Used by all three total
   cards and both rows of the Analyst's scoreboard. */
.tally-list { margin-top: 10px; }
.tally {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  min-height: 35px; border-top: 1px solid var(--line);
}
.tally:first-child { border-top: 0; }
.tally-k { font-size: 14px; line-height: 20px; color: var(--ink); }
.tally-v { font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.tally-who { font-family: var(--font-mono); font-size: 12px; font-weight: 400; color: var(--ink-muted); }

.verdict { margin-top: 10px; font-size: 13px; line-height: 19px; }
.verdict.ok   { color: var(--turf); }
.verdict.warn { color: var(--amber); }
.verdict.bad  { color: var(--danger); }
/* ⚠ A NOTE THAT IS ONLY A MARK STILL NEEDS A TARGET BIG ENOUGH TO HOVER AND
   TO TAP. 13px of glyph is 13px of hover area, which on a phone is nothing
   at all, so the mark carries its own padding and is focusable -- a tooltip
   reachable only by a mouse hides the words from everyone else. */
.verdict-mark { line-height: 0; }
/* ⚠ ON THE SCORE, IT IS NOT A BLOCK. .verdict owns a 10px top margin for the
   case where it is a line of its own; riding the total it has to sit on the
   baseline instead of pushing the figure down. */
.gw-score .verdict-mark { display: inline-flex; margin: 0 0 0 6px; }
.note-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; margin: -5px -5px -5px -6px;
  vertical-align: -6px; cursor: help;
}
.note-icon:focus-visible { outline: 2px solid var(--turf); outline-offset: 0; border-radius: 4px; }
/* the bench shirt's mark sits where its word used to */
.shirt-sub .ev, .shirt-sub .note-icon { margin: 0; width: auto; height: auto; }

.card.fail { border-color: var(--danger); }
.card.warn { border-color: var(--amber); }
.card .body { margin: 8px 0 0; font-size: 16px; line-height: 24px; }

.bench-row { display: flex; gap: 8px; margin: 12px 0; align-items: flex-start; }
.bench-row .shirt-u { flex: 1 1 0; min-width: 0; max-width: 78px; }
.bench-row .shirt-cap { font-size: 12px; font-weight: 700; color: var(--ink); max-width: 78px; }
.legend-k { font-size: 12px; color: var(--ink-muted); }

.mrow { padding-top: 12px; border-top: 1px solid var(--line); margin-top: 12px; }
.mrow:first-of-type { border-top: 0; }
.mrow-top { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.mrow-v { font-size: 26px; font-weight: 700; font-variant-numeric: tabular-nums; }

.display-sub { font-size: 17px; font-weight: 400; color: var(--ink-muted); }
/* ⚠ SCOPED TO REVIEW, AND IT WAS NOT. The pack's phone render for Review
   runs the title straight into the segmented control, because there the
   sub-line costs a third of the pitch. Written unscoped it also deleted
   Scout's subtitle, which that screen's spec puts on both widths -- one
   screen's measurement applied to a selector that matches five. */
.page.review .page-title > .note-sm { display: none; }
/* ⚠ THE STAMP LINE IS DATA AND IT SITS ON A PLATE. The free-transfer
   sentence carries a points cost and the details carry a run id; rule 2 says
   white panels carry every number. On a scene page it was laid straight on
   the photograph, which is the 1.8:1 failure the plating gate exists for. */
.meta {
  margin: 12px 0; padding: 12px 14px;
  background: var(--surface-raised); border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}

.footnote {
  display: flex; gap: 8px; align-items: flex-start; flex-wrap: wrap;
  margin-top: 16px; padding: 14px 16px;
  background: var(--surface-raised); border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  font-size: 13px; line-height: 19px; color: var(--ink-muted);
}

/* Phone keeps the pitch full-bleed: the page gutter is cancelled for the
   panel alone, so the photograph runs edge to edge as the render shows. */
.pitch-wrap { margin: 0 calc(-1 * var(--space-4)); position: relative; }
/* Phone: a full-width bar above the photograph, as review-phone.png has it. */
.pitch-wrap > .seg-wrap { margin: 0 var(--space-4) 10px; }
.pitch-panel { border-radius: 0; }

@media (min-width: 760px) {
  /* ⚠ THE CARDS ON THE LEFT, THE SCENE ON THE RIGHT, AND THE SCENE REACHES
     THE PAGE EDGE. Amr, 2026-10-06: the picture is the screen and should run
     to the edge of the window on its side; the boxes that read against it
     belong on the other. The DOM order stays pitch-then-cards -- the pitch is
     the subject and a screen reader should meet it first -- and `order` does
     the swap, so neither the reading order nor the tab order follows the
     layout. The negative margin cancels .page's 32px so the photograph ends
     where the window does rather than in a 32px gutter. */
  .review-grid { grid-template-columns: 460px minmax(0, 1fr); gap: 24px; align-items: start; }
  .review-grid > .pitch-wrap { order: 2; margin-right: -32px; }
  .review-grid > :not(.pitch-wrap) { order: 1; }
  .pitch-wrap { margin: 0; }
  /* ⚠ THREE QUARTERS OF A PITCH, SO THE PAGE FITS THE WINDOW. 1095 put the
     foot of the panel 450px below the fold at 1440x900, so the only way to
     see the forwards was to scroll past the team total. `cover` with the
     position pinned to the top crops the bottom of the photograph, which is
     the grass behind the halfway line. */
  /* ⚠ 680, NOT 620, AND THE PAGE IS NO TALLER FOR IT. At 620 the desktop
     rows sat 124px apart and a shirt with a caption row stands 130, so every
     captioned shirt reached into the roundel of the row below -- measured at
     1440 on all three views. The gate never saw it because the pitch scan
     ran only at 390px; it runs at both widths now. 680 puts the panel's foot
     at 934 against the left column's 930, so the window still ends where it
     ended. */
  .pitch-panel { border-radius: 14px; min-height: 680px; }
  .pitch-panel::before { background-position: center top; }
  /* the unit shrinks with the pitch, or eleven men fill a smaller field */
  .pitch-slot { max-width: 128px; }
  /* ⚠ THE PHOTOGRAPH IS TALLER THAN THE DRAWING WAS, and the rows are what
     pay for it. A jersey is 364x483, so a 128px unit is 170px tall against
     the 124px the four rows allow at this panel height -- the midfielders
     landed on the defenders' captions. 86px wide is 114 tall, which fits
     with air. The number comes down with the shirt. */
  .pitch-slot .shirt-u .shirt-body { max-width: 66px; }
  .pitch-slot .shirt-u .shirt-num { font-size: 20px; line-height: 22px; }
  .pitch-arrow { display: flex; }
  /* ⚠ ELEVEN PLAYERS ON A PITCH ARE SMALL. The old sizes were drawn for a
     card that was all pitch; against a stadium seen from above a 150px unit
     is a man the size of the centre circle. Capped to the field's own share
     so five midfielders fit between the touchlines with air either side. */
  .pitch-slot { left: var(--xd); top: var(--yd); width: var(--spand); max-width: 150px; }
  .pitch-slot .shirt-u .shirt-body { max-width: 66px; }
  .pitch-slot .shirt-u .shirt-num { font-size: 36px; line-height: 40px; }
  /* ⚠ THE CHIP MAY OVERHANG ITS SLOT AT THIS WIDTH, BECAUSE THE RENDER'S
     DOES. Five midfielders share a 500px spread, so a slot is 100px and
     "Tavernier · MID" needs 98 before padding. Allowed to WRAP it took two
     lines and lifted that one shirt into the row below; capped at the slot
     with the base rule's ellipsis it came out "Tavernier · …", which is a
     name the reader cannot finish. review-scene.png measures the same chip
     at 108px on the same 100px spacing: the pack lets them touch, and they
     read because each is an opaque plate with its own edge.

     ⚠ THE PHONE KEEPS THE ELLIPSIS AND THAT IS NOT AN OVERSIGHT. The base
     rule's reason is a phone measurement -- slots tile flush at 65px with no
     gap, so a chip that overhangs there has nowhere to go. At 1440 the same
     row has 100px centres and chips under 110, so an overhang lands in slack
     that exists. An earlier attempt used calc(100% + 18px) and pushed every
     chip out whether it needed it or not; lifting the cap widens only the
     ones that would otherwise be cut. */
  .pitch-slot .shirt-cap { font-size: 13px; white-space: nowrap; max-width: none; }
  .cap-pos { display: inline; }
  .page.review .page-title > .note-sm { display: block; }
  /* ⚠ 360, NOT 320. Three segments inside 320px clipped the first to
     "No chan..." at 1440 -- the control naming the three views could not
     name the first one. Measured: "No changes" needs 104px of the 312
     available inside the padding, and the ellipsis appeared because the
     track was sized before the labels were. */
  /* Desktop: the pill floats over the top of the pitch, centred, as
     review-scene.png has it. pointer-events on the wrapper would otherwise
     swallow clicks meant for the shirts behind it. */
  .page.review .pitch-wrap > .seg-wrap {
    position: absolute; top: 14px; left: 0; right: 0; z-index: 4;
    margin: 0; display: flex; justify-content: center; pointer-events: none;
  }
  .page.review .pitch-wrap > .seg-wrap .seg { pointer-events: auto; }
  .display-sub { font-size: 28px; }
}

/* The season record, absorbed from Review into the Board's report. */
.title.sm { font-size: 16px; line-height: 22px; margin-top: 18px; }
.record-rows { margin: 10px 0; }
/* ⚠ THREE COLUMNS AT 358px LEFT THE MIDDLE ONE 160px WIDE, and the gap
   sentence in it wrapped to two words a line. Nothing overflowed and nothing
   was clipped; it was simply unreadable, which is the third kind of failure
   this project keeps finding. Stacked on a phone, three across from 560. */
.record-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: 4px 10px; align-items: center;
  padding: 10px 0; border-top: 1px solid var(--line);
}
.record-row .record-mid { grid-column: 1 / -1; }
@media (min-width: 560px) {
  .record-row { grid-template-columns: 86px minmax(0, 1fr) auto; }
  .record-row .record-mid { grid-column: auto; }
}
.record-row:first-child { border-top: 0; }
.record-gw { display: flex; align-items: baseline; gap: 4px; flex-wrap: wrap; }
.record-pts { font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; }
.record-chip { justify-self: end; }

/* ⚠ FIVE FACTS IN ONE GREY STRING IS ONE FACT. Amr, 2026-10-08, item 10:
   "try to make different information stand out and not run into each other
   using colours and fonts." The row's middle column was a single
   monospace sentence -- "15 of 15 played · 0 transfers · bank £2.0m" --
   repeated down the panel at one weight, so the score was the only thing
   the eye could land on.

   The hierarchy is three deep and no deeper, because a fourth weight is
   another flat page: the HIT is the figure that cost points and carries the
   danger colour; a SHORT SQUAD is a warning and carries amber; everything
   else stays the muted grey it always was. Only the exceptions are
   coloured, so colour continues to mean "look here" -- a palette where
   every chip is tinted says nothing at all.

   Each is its own bordered chip rather than a run of text, so the eye can
   skip to the number it wants instead of reading a sentence to find it. */
.record-facts { display: flex; flex-wrap: wrap; gap: 4px 6px; }
.rfact {
  font-family: var(--font-mono); font-size: 11px; line-height: 15px;
  font-variant-numeric: tabular-nums;
  padding: 2px 7px; border-radius: 999px;
  background: var(--chip); color: var(--ink-muted);
  white-space: nowrap;
}
.rfact.warn { background: var(--amber-soft); color: var(--amber); font-weight: 650; }
.rfact.cost { background: var(--danger-soft); color: var(--danger); font-weight: 650; }
/* the reason, said once under the rows when every week gives the same one */
.record-shared { margin: 2px 0 0; }

/* ── Registering the moves you have already made ─────────────────────────
   Amr's item 20. A row per move: who went out, who came in, what FPL gave
   you and what it charged. Four fields at 390px is two rows of two, and at
   desk width one line. */
.tr-card { margin: 14px 0; }
.tr-rows { display: flex; flex-direction: column; gap: 14px; margin: 14px 0; }
.tr-row {
  display: grid; gap: 10px;
  grid-template-columns: minmax(0, 1fr); align-items: end;
  padding: 12px; border: 1px solid var(--line); border-radius: var(--radius-md);
  background: var(--surface);
}
@media (min-width: 760px) {
  .tr-row { grid-template-columns: minmax(0, 2fr) minmax(0, 1.4fr) auto; }
}
.tr-pair { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; }
.tr-pick, .tr-money { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.tr-pick select, .tr-money input {
  font: inherit; font-size: 14px; padding: 7px 8px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface-raised); color: var(--ink); min-width: 0; width: 100%;
}
.tr-money input { font-family: var(--font-mono); text-align: right; }
.tr-del { justify-self: start; }
.tr-add { display: flex; gap: 8px; margin: 10px 0; flex-wrap: wrap; }
.tr-foot { margin-top: 12px; }
.tr-bank { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.tr-bank-v {
  font-family: var(--font-mono); font-size: 22px; font-weight: 650;
  font-variant-numeric: tabular-nums;
}
/* ⚠ THE FILE IS SHOWN AS A FILE. It is going to be pasted into a text
   editor, so it is monospace, selectable and scrolls rather than wrapping
   JSON across lines that would no longer parse. */
.tr-json {
  font-family: var(--font-mono); font-size: 12px; line-height: 18px;
  background: var(--chip); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: 10px 12px;
  overflow-x: auto; margin: 10px 0; white-space: pre;
}

/* ── The weakest links, in the Assistant's panel ─────────────────────────
   Amr's item 18. The Assistant SAYS which to move on the screen; this is the
   working beside it, in the panel, because three names with a rate, a bar
   and four facts each is a table and the screen holds a sentence. */
.weak-card { margin-top: 14px; }
.weak-rows { display: flex; flex-direction: column; gap: 12px; margin: 12px 0 10px; }
.weak-row { display: flex; flex-direction: column; gap: 5px; }
/* ⚠ ONE ROW IS MARKED, NOT THREE. The worst is the answer; the others are
   the alternatives. Marking all of them would be marking none. */
.weak-row.worst .weak-name { font-weight: 700; }
.weak-row.worst .weak-rate { color: var(--danger); }
.weak-top { display: flex; align-items: baseline; gap: 6px; }
.weak-name { font-weight: 600; }
.weak-rate {
  margin-left: auto; font-family: var(--font-mono);
  font-size: 15px; font-weight: 650; font-variant-numeric: tabular-nums;
}
/* the same track the pace block uses, so six a week sits at the same
   half-way mark on both and the two blocks can be read against each other */
.weak-track { display: block; }
.weak-facts { display: flex; flex-wrap: wrap; gap: 4px 6px; }
.rfact.model { background: var(--amber-soft); color: var(--amber); }

/* ── The Office squad list: what he gave, what we banked ─────────────────
   Amr's item 16. The columns answer four questions and the styling has one
   job: keep the MEASURED half and the MODELLED half from looking alike. */
.lp { display: inline-flex; align-items: center; gap: 7px; }
.lp-n { font-weight: 600; }
.lnew {
  font-family: var(--font-mono); font-size: 11px;
  padding: 1px 6px; border-radius: 999px;
  background: var(--chip); color: var(--ink-muted);
}
/* ⚠ ONE COLOUR EACH, AND ONLY WHERE IT MEANS SOMETHING. Banked is the figure
   that worked, bench is the figure that did not, the armband is the manager's
   own. A zero bench is the normal state and stays grey — tinting fifteen
   zeroes would spend the column's one signal on the rows with nothing to
   say. */
.lgood { font-weight: 650; }
.lcost { font-weight: 650; color: var(--danger); }
.larm { font-weight: 650; color: var(--amber); }

.contrib { display: inline-flex; flex-wrap: wrap; gap: 3px 5px; align-items: center; }
.contrib-bit {
  display: inline-flex; align-items: baseline; gap: 3px;
  padding: 1px 6px; border-radius: 999px;
  background: var(--chip); font-size: 11px; line-height: 16px;
}
.contrib-what { color: var(--ink-muted); }
.contrib-n { font-family: var(--font-mono); font-weight: 650; }
.contrib-bit.bad { background: var(--danger-soft); }
.contrib-bit.bad .contrib-what,
.contrib-bit.bad .contrib-n { color: var(--danger); }
.contrib-more { font-size: 11px; color: var(--ink-muted); }

/* ⚠ THE SEAM IS DRAWN ON THE CELLS, NOT ONLY ABOVE THE TABLE. Scrolled
   sideways on a phone the heading is gone, and a modelled figure that looks
   exactly like a measured one is the whole hazard the fence exists for. The
   left rule marks where FPL's fields stop and the Analyst's begin; the tint
   carries that mark down every row. */
.tbl th.mcol, .tbl td.mcol { background: var(--amber-soft); }
.tbl th.mcol-first, .tbl td.mcol-first { border-left: 2px solid var(--amber); }
.tbl th.mcol { color: var(--amber); }

/* ── Scout: a corkboard over three tabs ───────────────────────────────────
   Two files feed this screen and the design's whole job is keeping them
   apart: the measured tables, the forecasts, and a fence between. */

.cork {
  margin: 14px 0; padding: 16px;
  background: var(--cork); border: 10px solid var(--cork-edge);
  border-radius: 10px;
  box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.18);
  background-image: radial-gradient(var(--cork-dot) 1px, transparent 1px);
  background-size: 8px 8px;
}
.cork-h { margin: 0; font-size: 20px; line-height: 26px; font-weight: 700; color: var(--cork-ink); }
.cork-count { font-size: 13px; color: var(--cork-ink); opacity: 0.85; margin-top: 2px; }
.cork-empty, .cork-note {
  margin: 10px 0 0; padding: 10px 12px;
  background: var(--surface-raised); border-radius: 8px;
  font-size: 14px; line-height: 20px; color: var(--ink);
}
.cork-note { font-size: 13px; color: var(--ink-muted); }
.cork-cards { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 12px; }
.pin-card {
  position: relative; width: 150px; min-height: 100px; padding: 16px 12px 12px;
  background: var(--surface-raised); border-radius: 6px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
  transform: rotate(-1.5deg);
}
.pinhead {
  position: absolute; top: -6px; left: 50%; transform: translateX(-50%);
  width: 12px; height: 12px; border-radius: 50%; background: var(--pinhead);
}
.pin-name { font-size: 17px; font-weight: 700; }
.pin-meta { font-family: var(--font-mono); font-size: 13px; color: var(--ink-muted); }

/* The tab strip sits on the panel's top edge, so the active tab joins it. */
.tabstrip { display: flex; gap: 2px; margin-top: 18px; align-items: stretch; }
.stab {
  flex: 1 1 0; min-width: 0; text-align: left; cursor: pointer;
  padding: 6px 8px; border: 1px solid var(--line); border-bottom: 0;
  border-radius: 8px 8px 0 0; background: var(--surface);
  display: flex; flex-direction: column; gap: 2px;
}
.stab.on { background: var(--surface-raised); }
.stab-n { font-size: 14px; line-height: 18px; font-weight: 700; color: var(--ink); }
.stab-s { font-size: 9.5px; line-height: 12px; font-weight: 700; letter-spacing: 0.04em; }
.stab.measured .stab-s { color: var(--turf); }
.stab.mixed .stab-s,
.stab.modelled .stab-s { color: var(--forecast); }
.tabpanel {
  background: var(--surface-raised); border: 1px solid var(--line);
  border-radius: 0 10px 10px 10px; padding: 16px; min-height: 420px;
}

/* The provenance band: a label column and a sentence, tinted by which kind
   of number the panel below it carries. */
.pband {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 10px 12px; border-radius: 4px; margin: 10px 0;
  font-size: 14px; line-height: 20px;
}
.pband-k { flex: none; font-size: 11px; font-weight: 700; letter-spacing: 0.04em; padding-top: 2px; }
.pband.measured { background: var(--turf-soft); }
.pband.measured .pband-k { color: var(--turf); }
.pband.modelled { background: var(--forecast-soft); }
.pband.modelled .pband-k { color: var(--forecast); }

/* ⚠ THE FENCE IS A RULE WITH WORDS ON IT, NOT A FOOTNOTE. Everything above
   it can be checked against the game and nothing below it can; a caveat
   under the numbers is read after them, which is too late. */
.fence { display: flex; align-items: center; gap: 10px; margin: 24px 0 12px; }
.fence-rule { flex: 1; height: 2px; background: var(--line); }
.fence-words { font-size: 11px; font-weight: 700; letter-spacing: 0.04em; color: var(--ink-muted); }

.controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; }
.fld { display: flex; flex-direction: column; gap: 4px; flex: 1 1 150px;
       font-size: 12px; font-weight: 700; color: var(--ink-muted); }
.fld:first-child { flex-basis: 100%; }
.fld.narrow { flex: 0 1 110px; }
.fld input, .fld select {
  min-height: 44px; padding: 0 10px; font: inherit; font-size: 15px; font-weight: 400;
  color: var(--ink); background: var(--surface-raised);
  border: 1px solid var(--line); border-radius: 8px;
}
.count-line { text-align: right; font-size: 13px; color: var(--ink-muted); margin-top: 8px; }
.morecols summary { font-size: 14px; font-weight: 700; cursor: pointer; padding: 6px 0; }

.sub2 { display: block; font-size: 11px; color: var(--ink-muted); }
.whorow { display: flex; align-items: center; gap: 6px; }
.pen { font-size: 11px; color: var(--ink-muted); }
.flagmark { color: var(--danger); }

.clubgrid { margin-top: 10px; }
.clubrow {
  display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 10px;
  align-items: center; padding: 10px 0; border-top: 1px solid var(--line);
}
.clubrow:first-child { border-top: 0; }
.clubrow-t { grid-column: 2; display: grid; grid-template-columns: auto 1fr; gap: 2px 8px; align-items: baseline; }
/* ⚠ THE LABEL GOES ABOVE THE STRIP ON A PHONE. Six chevrons are 225px and
   the label is 76; side by side that is 309 in a 270px column, and the page
   gained six pixels of sideways scroll -- enough to feel, not enough to see.
   Stacked, the strip has room at its measured width and nothing is cut. */
.strip-line { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.strip-k { flex: none; font-size: 11px; color: var(--ink-muted); }

.rot-list { margin-top: 8px; }
.rot-row { padding: 8px 0; border-top: 1px solid var(--line); font-size: 14px; line-height: 20px; }
.rot-row:first-child { border-top: 0; }

.sugg-list { display: flex; flex-direction: column; gap: 12px; margin: 12px 0; }
.gaps { margin: 10px 0; }
.gap-row { display: flex; flex-wrap: wrap; gap: 4px 10px; padding: 7px 0;
           border-top: 1px solid var(--line); font-size: 14px; line-height: 20px; }
.gap-k { font-weight: 700; flex: 0 0 auto; }
.gap-v { color: var(--ink-muted); }

/* A wide table on a phone scrolls inside its own box rather than pushing the
   page sideways. The container is the scroller; the page never is. */
.scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }

@media (min-width: 760px) {
  .strip-line { flex-direction: row; align-items: center; gap: 8px; }
  .strip-k { width: 76px; }
  .clubrow { grid-template-columns: 56px minmax(0, 1fr) 220px; }
  .clubrow-t { grid-column: 3; }
  .controls .fld { flex: 1 1 200px; }
}

/* ── Plan: one of yours against one candidate ─────────────────────────────
   The pack's head-to-head. Two shirt cards, a seesaw whose fulcrum sits over
   the bar's zero tick, and the Analyst's read underneath. */

/* ⚠ THE BOARD IS THE PLATE, AND THAT IS RULE 2 AND NOT A STYLE. The pack
   draws a white board floating on the meeting-room photograph and puts every
   figure on it. Without the plate the pickers, the two totals and the
   figures line sat straight on the scene -- thirteen of them, which is what
   the plating gate counted. */
/* ⚠ AND THAT PLATE CAME OFF WITH THE PHOTOGRAPH IT WAS FOR. This was a
   white card wrapping everything, because the whole page used to stand on
   the scene. The picture is now a bounded band with `.plan-room` plating
   its contents, so the outer card wrapped the band in a second white box
   and made a third: card, band, room, two of them white. plan-scene.png has
   the band on the plain page and nothing around it. The cards inside go
   back to the raised surface too -- they were dropped to `--surface` only
   to separate them from this box. */
.board { margin-top: 14px; }
/* ⚠ THE TWO CARDS SHARE A ROW AND THE BEAM GOES UNDER THEM. In DOM order
   the beam sits BETWEEN the cards, because that is where it belongs at
   desktop; left to auto-placement at phone width it could not start in
   column two, so it took a row of its own and pushed the second card to a
   third -- card, beam, card stacked down the page, which is not a
   comparison. plan-phone.png puts both shirts abreast with the beam
   underneath. Pinning the rows costs two declarations and keeps one DOM
   order serving both widths. */
.headtohead { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.headtohead > .vcard { grid-row: 1; }
.h2h-mid { grid-column: 1 / -1; grid-row: 2; }
.h2h-cap { font-size: 13px; color: var(--ink-muted); text-align: center; margin-bottom: 4px; }

.vcard {
  position: relative; overflow: hidden; min-width: 0;
  background: var(--surface-raised); border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}
.vcard-bar { display: block; height: 10px; }
.vcard-in { padding: 10px 12px 12px; }
.vcard-eyebrow { font-size: 10px; font-weight: 700; letter-spacing: 0.05em;
                 color: var(--ink-muted); text-transform: uppercase; }
.vcard-num { font-size: 34px; line-height: 40px; font-weight: 800;
             font-variant-numeric: tabular-nums; min-height: 40px;
             display: flex; align-items: center; }
.vcard-name { font-size: 19px; font-weight: 700; }
.vcard-meta { font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted); }
.vcard-cap { margin-top: 6px; font-size: 12px; line-height: 17px; color: var(--ink-muted); }
.vcard-fx { margin-top: 8px; }

/* ⚠ A BAR, NOT A HYPHEN. In the big-number slot a hyphen reads as a minus
   sign against a figure. Same rule as the hatched bar: it means waiting on
   data, and it has to be visibly a placeholder rather than a small number. */
/* The pack's placeholder for a missing NUMBER: a short solid rule, measured
   off deadline.png's waiting figures at 13x3. Not a hatch -- see FPL.dash. */
.dash { display: inline-block; width: 13px; height: 3px; border-radius: 1px;
        background: var(--ink); vertical-align: middle; }
/* Deadline's team-expected panel: three figures across, the render's order. */
.tex { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
       gap: 10px; margin: 12px 0 10px; text-align: center; }
.tex-k { font-size: 13px; color: var(--ink-muted); }
.tex-v { font-size: 30px; line-height: 38px; font-weight: 700;
         font-variant-numeric: tabular-nums; min-height: 38px;
         display: flex; align-items: center; justify-content: center; }
.tex-v .dash { width: 26px; height: 5px; border-radius: 2px; }
.vcard-num .dash { width: 44px; height: 6px; border-radius: 2px; }

.seesaw { position: relative; margin: 2px 0 -6px; }
/* The two tags the render hangs on the beam: filled, tilted with it, and
   coloured so KEEP and SWITCH are told apart before either is read. */
.see-k { position: absolute; top: 18px; padding: 3px 9px; border-radius: 6px;
         font-size: 12px; font-weight: 700; letter-spacing: 0.04em;
         color: var(--on-fill); }
.see-k.left { left: 6px; background: var(--danger); }
.see-k.right { right: 6px; background: var(--forecast); }
.vcard-fxk { margin-top: 8px; font-size: 11px; font-weight: 700;
             letter-spacing: 0.04em; color: var(--ink-muted); }
/* ⚠ THE BIN WORD SHOWS AT EVERY WIDTH ON A CARD. plan-phone.png prints
   "fair" and "tough" inside each chip at 390px; hiding it there left the
   band carried by colour alone on the one screen that compares two players
   by it. The pitch strips keep the narrow treatment -- they have six cells
   in a 54px column, not a 160px card. */
.vcard-fx .fxm-w { display: inline; }

.vbar-wrap { margin-top: 2px; }
.vbar-caps { display: flex; justify-content: space-between; margin-top: 10px;
             font-size: 11px; color: var(--ink-muted); }
.vbar-caps .mid { position: relative; }
.figline { margin-top: 8px; text-align: center;
           font-family: var(--font-mono); font-size: 13px; color: var(--ink); }

.speak { position: relative; margin-top: 14px; padding-left: 20px; }
.speak-face { position: absolute; left: -10px; top: 16px; width: 52px; height: 52px;
              border-radius: 50%; border: 2px solid var(--surface-raised);
              object-fit: cover; }
.speak-in { padding-left: 48px; }
.speak-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
.speak-h { margin: 0; font-size: 20px; line-height: 26px; font-weight: 800; color: var(--amber); }
.speak-deck { font-size: 15px; color: var(--ink-muted); }

.rotpair { display: grid; gap: 12px; margin-top: 14px; }
.rgrid { margin: 10px 0; }
.rgrid-row { display: flex; align-items: center; gap: 4px; margin-bottom: 3px; }
.rgrid-k { flex: none; width: 34px; font-family: var(--font-mono);
           font-size: 12px; font-weight: 700; color: var(--ink-muted); }
.rgrid-cells { display: flex; gap: 2px; flex: 1 1 auto; min-width: 0; }
.rcell { flex: 1 1 0; min-width: 0; height: 18px; border-radius: 3px; }
.rcell.b0 { background: var(--fixture-hard); }
.rcell.b1 { background: var(--fixture-tough); }
.rcell.b2 { background: var(--fixture-fair); }
.rcell.b3 { background: var(--fixture-good); }
.rcell.weak { box-shadow: inset 0 0 0 2px var(--danger); }

.foldout { margin-top: 12px; background: var(--surface-raised);
           border: 1px solid var(--line); border-radius: var(--radius-lg); padding: 12px 14px; }
.foldout summary { font-size: 16px; font-weight: 700; cursor: pointer; }
.foldout .body { margin: 10px 0 0; font-size: 15px; line-height: 22px; }

.cork.slim { padding: 14px; }
.cork-headline { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
.pin-card.live { box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25), 0 0 0 2px var(--turf); cursor: pointer; }

@media (min-width: 760px) {
  .headtohead { grid-template-columns: 200px minmax(0, 1fr) 200px; align-items: start; }
  .headtohead > .vcard { grid-row: auto; }
  .h2h-mid { grid-column: auto; grid-row: auto; }
  .rotpair { grid-template-columns: 1fr 1fr; }
}

/* The flexible fixture strip: shares whatever width it is given, so it can
   sit inside a 148px card without clipping. Same bins and tokens as the
   chevron strip; the bin word hides when the cell is too narrow and stays in
   the aria-label, so the colour is never the only carrier. */
.fxmini { display: flex; gap: 2px; }
.fxm { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.fxm-o { font-family: var(--font-mono); font-size: 10px; line-height: 12px;
         color: var(--ink-muted); text-align: center; overflow: hidden; }
.fxm-c { height: 22px; border-radius: 4px; display: flex; align-items: center;
         justify-content: center; overflow: hidden; }
.fxm-w { font-size: 9px; font-weight: 700; white-space: nowrap; }
.fxm-c.b0 { background: var(--fixture-hard); color: var(--on-fill); }
.fxm-c.b1 { background: var(--fixture-tough); color: var(--ink); }
.fxm-c.b2 { background: var(--fixture-fair); color: var(--ink); }
.fxm-c.b3 { background: var(--fixture-good); color: var(--on-fill); }
@media (max-width: 759px) { .fxm-w { display: none; } }

/* ── Deadline: the changing room ──────────────────────────────────────────
   Five lockers to a page. One at a time on a phone with chevrons, a swipe
   and dots; all five side by side on a desktop with no carousel at all. */

/* ⚠ THE PHOTOGRAPH IS A BAND IN THE FLOW, NOT THE PAGE BACKDROP, and rule 1
   is scoped rather than relaxed: data-scene stays on <body> so the plating
   gate keeps running, the page-wide layer is switched off, and the same
   image and the same wash are painted inside the band. */
body[data-scene="deadline"]::before,
body[data-scene="deadline"]::after { background-image: none; background: none; }

/* ⚠ THE ARROWS ARE THE OTHER WAY ROUND FROM REVIEW'S. There the pack's
   phone render has none and the desktop has them; here the phone shows one
   locker at a time and the desktop shows all five, so the phone needs them
   and the desktop has nothing to page through. */
.scene-band .pitch-arrow { display: flex; }

.scene-band {
  position: relative; overflow: hidden; margin: 12px 0;
  border-radius: var(--radius-lg); padding: 14px 10px;
  background: var(--surface);
}
.scene-band::before,
.scene-band::after { content: ""; position: absolute; inset: 0; z-index: 0; }
.scene-band::before {
  background-image: var(--scene); background-size: cover; background-position: center;
  filter: saturate(var(--scene-sat));
}
.scene-band::after { background: var(--scene-wash); }
/* ⚠ EVERY SCENE RUNS OFF THE RIGHT EDGE, which is Amr's instruction of
   2026-10-06: "they should cover the entire screen on the right". Review's
   pitch already did it with a -32px margin against the page's 32px gutter;
   Office, Plan, Deadline and the press conference stopped 32px short, so the
   photograph read as a picture ON the page rather than as the room the page
   is in. The right corners square off with it -- a rounded corner half a
   pixel off the viewport looks like a mistake rather than a decision.

   ⚠ IT HAS TO LIVE HERE, AFTER `.scene-band`'s OWN `margin: 12px 0`. Written
   up with the masthead it was correct, specific enough, and beaten: the
   shorthand three hundred lines below reset margin-right to 0 every time.
   Nothing reported it -- the measurement just kept saying 32px short. */
@media (min-width: 760px) {
  .scene-band {
    margin-right: -32px;
    border-top-right-radius: 0; border-bottom-right-radius: 0;
  }
}
.lockers { position: relative; z-index: 1; display: flex; gap: 10px; }

/* ══ pace to the target ══════════════════════════════════════════
   ⚠ ONE SCALE FOR EVERY BAR, OR THE BARS SAY NOTHING. Each row is a player
   against the same six points, on a track twice that wide, so six is always
   the half-way mark and the line at six is in the same place on every row. A
   bar scaled to its own value would make a 1 and a 13 the same length and
   the block would be decoration. */
.pace-card { margin-bottom: var(--space-2); }
.pace-head { margin: var(--space-4) 0 var(--space-1); }
.pace-list { display: grid; row-gap: 3px; }
.pace-row {
  display: grid; grid-template-columns: 82px minmax(0, 1fr) 26px 46px;
  align-items: center; column-gap: var(--space-2);
  font-size: 12px; line-height: 17px;
}
.pace-who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pace-track {
  position: relative; height: 8px; border-radius: 4px;
  background: var(--line); overflow: hidden;
}
/* the target, at the half-way mark, on every row */
.pace-track::after {
  content: ""; position: absolute; left: 50%; top: -2px; bottom: -2px;
  width: 1px; background: var(--ink-muted); opacity: 0.55;
}
.pace-fill { display: block; height: 100%; background: var(--ink-muted); border-radius: 4px; }
.pace-fill.met { background: var(--turf); }
.pace-n { font-family: var(--font-mono); font-weight: 700; text-align: right; }
.pace-x { font-size: 10px; color: var(--ink-muted); white-space: nowrap; }

/* ⚠ THE OFFICE'S OWN COLUMN, AT REVIEW'S WIDTH. Amr, 2026-10-07: a panel on
   the left the same size as Review's, with the photograph running from the
   right edge up to it. 460px is not a round number chosen here -- it is the
   figure `.review-grid` uses, and the point of the instruction is that the
   two pages line up when a reader moves between them.

   ⚠ THE BLEED MOVES TO THE GRID CHILD. `.scene-band` carries `margin-right:
   -32px` so the room reaches the window's edge; inside a grid that margin
   still applies to the band, which is the column on the right, so the bleed
   survives the restructure. On a phone the grid is one column and the panel
   stacks under the room, where the extra block already was. */
/* ⚠ ON A PHONE THE ROOM COMES FIRST, which is where the extra block already
   was. One column puts the panel above the scene in DOM order, and the scene
   is what the page is -- 782px of gameweek table before you reach the room
   is the subject below the fold. The desktop block flips it back, because
   there the panel is beside the room rather than before it. */
.office-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; }
.office-grid > .scene-band { order: 1; }
.office-grid > .office-panel { order: 2; }
.office-panel { min-width: 0; }
@media (min-width: 760px) {
  .office-grid {
    grid-template-columns: 460px minmax(0, 1fr);
    gap: 24px; align-items: start;
  }
  .office-grid > .office-panel { order: 1; }
  .office-grid > .scene-band { order: 2; margin-top: 0; margin-bottom: 0; }

  /* ⚠ THE PAGE USED TO GROW AND SHRINK AS YOU TAPPED THE STAFF, and this is
     the fix. Amr, 2026-10-08: "office page gets longer and shorter because
     the left box keeps resizing."

     The mechanism was three facts meeting. The band's height is DERIVED --
     `aspect-ratio: 880/745` on a fixed column -- so it never moves. The
     panel's height was PURE CONTENT: `.office-panel` carried `min-width: 0`
     and nothing else, no height rule anywhere in either stylesheet. And they
     share one implicit grid row, so the row took whichever was taller.
     `office.js` rebuilds `extraSlot` on every staff click, and the personas
     are not the same size: the Assistant adds a weak-links block AND a
     14-row chip chart, Finance adds one value panel. Tap Assistant, the page
     grows; tap Finance, it shrinks.

     ⚠ `align-self: stretch` ON THE PANEL ONLY, NOT `align-items: stretch` ON
     THE GRID. Stretching every item would set a used height on the BAND too,
     and a used height beats `aspect-ratio` -- which would silently break the
     one thing the ratio gate measures. The band keeps `start` and keeps its
     ratio; only the panel fills the row.

     ⚠ AND THE CONTENT GOES OUT OF FLOW, which is what actually stops the
     row moving. A stretched panel still CONTRIBUTES its content height to the
     row, so a tall persona would still grow the page. `.panel-scroll` is
     absolutely positioned, so the panel has no intrinsic height at all: the
     row is the band's height, always, whoever is speaking.

     ⚠ `inset: 0`, NEVER `height: 100%`. Recorded 40 lines down this file
     from the same fault: the percentage did not resolve in this chain and a
     card stayed 311px inside a 267px box. `inset: 0` against a positioned
     parent is a definite box whatever the chain above it is doing.

     Review solved the identical fault with `min-height` on its two sub-cards
     (see `.rail .rail-top`). That works when you know the content's size;
     this panel's content changes with the speaker, so it needs the box to be
     definite rather than the content to be predictable. */
  .office-grid > .office-panel { align-self: stretch; position: relative; }
  .office-panel > .panel-scroll {
    position: absolute; inset: 0;
    overflow: auto; overscroll-behavior: contain;
    display: flex; flex-direction: column; gap: 16px;
    min-height: 0; padding-right: 4px;
  }
  /* the same fade the screen and the lockers use, for the same reason: no CSS
     selector can ask whether a box overflows, so JS sets `.more` after layout */
  .office-panel > .panel-scroll.more {
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent);
            mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent);
  }
}

/* ⚠ ON A PHONE THE PANEL IS ORDINARY FLOW. There is one column, nothing to
   match a height to, and an absolutely positioned child would collapse the
   panel to nothing. The desktop block above is the only place this applies. */
@media (max-width: 759px) {
  .office-panel > .panel-scroll {
    position: static; display: flex; flex-direction: column; gap: 16px;
  }
}

/* ── Office: the report on a screen in the room, the staff beside it ──────
   ⚠ THE BAND IS THE SCENE, so everything inside it needs the z-index that
   lifts it off the two painted layers -- the same rule `.lockers` keeps. */
/* Plan's room: the pickers, the two cards and the read, lifted off the
   band's two painted layers exactly as `.lockers` and `.office-room` are.

   ⚠ AND ON AN OPAQUE PANEL, WHICH THE SCENE CHECK INSISTED ON BEFORE I HAD
   LOOKED. Transparent, the picker labels and the card eyebrows -- "Put him
   up against", "Over the next", "Your player" -- sat straight on the
   boardroom photograph: nine text nodes, which the gate named the moment it
   started ending its walk at the band. plan-scene.png puts the whole block
   on one light panel inside the room, which is both what the render shows
   and what the rule requires. */
.plan-room { position: relative; z-index: 1; display: flex;
             flex-direction: column; gap: 12px;
             background: var(--surface-raised); border: 1px solid var(--line);
             border-radius: var(--radius-lg); padding: 8px; }
/* ⚠ TWO BOXES PADDING THE SAME CONTENT. The band pads and the room inside it
   pads again, which at 390 took 54px off the pair of cards -- 278 where
   plan-phone.png spends 332 of its 390. The render's inset is about 29px a
   side in total, so the two together get that and no more. */
[data-scene-band="plan"] { padding: 10px 6px; }
@media (min-width: 760px) {
  /* ⚠ THE ROOM IS THE PHOTOGRAPH'S OWN SCREEN. plan.jpg is a boardroom with
     one display on the wall; scanned at a brightness threshold of 40 it runs
     x 7.1-93.6% and y 10.8-52.5%, straight to within a pixel on every row and
     column sampled. The band is pinned to the picture's own square for the
     same reason Office's is: `cover` crops, and a crop moves every one of
     those percentages. */
  [data-scene-band="plan"] { aspect-ratio: 1024 / 1024; padding: 0; }
  .plan-room {
    position: absolute; left: 7.1%; top: 10.8%; width: 86.5%; height: 41.7%;
    padding: 14px; overflow: auto;
  }
}

.office-room { position: relative; z-index: 1; display: grid;
               grid-template-columns: minmax(0, 1fr); gap: 12px; }
.office-rail .rail-head { padding: 0 0 6px; letter-spacing: 0.04em; }
.office-rail { background: var(--surface-raised); border: 1px solid var(--line);
               border-radius: var(--radius-lg); padding: 10px 12px; }
@media (min-width: 760px) {
  /* The render gives the screen the width and the staff a column beside it. */
  /* ⚠ THE ROOM'S OWN TWO SCREENS, MEASURED OFF office.jpg. The photograph
     is a boardroom with a projector screen on the wall and a monitor on the
     desk, and Amr's instruction is that the elements go IN them rather than
     over them. Scanned at a 42 and 55 brightness threshold, the wall screen
     runs x 20.2-80.0% and y 19.1-51.1%, the desk monitor x 35.3-64.6% and
     y 57.4-76.0%, each straight-edged to within a pixel across every row and
     column sampled.

     ⚠ THE BAND IS PINNED TO THE PHOTOGRAPH'S OWN SQUARE. `cover` crops
     whatever does not fit, and a crop moves every one of those percentages;
     1024x1024 is what makes them mean what they measure. */
  /* ⚠ THE PHOTOGRAPH IS CROPPED TO THE SCREENS, AND EVERY y MOVED WITH IT.
     Amr, 2026-10-07: "crop from the top to show the light reflection, from
     the bottom to start from the screen stand — the whole focus is the
     screens, keep the width as is". The asset was 1024x1024 of boardroom,
     most of it a dark ceiling above the lights and a foreground chair below
     the table; the room now runs y 6% to 81% of the original, which is 1024
     x 768 and keeps the downlights at the top and the monitor's stand at the
     foot.

     ⚠ AND THE SLOTS ARE RE-DERIVED, NOT NUDGED. Both are percentages
     measured off the old picture -- the wall screen at top 19.1% and the
     desk monitor at 57.4% -- and a crop moves every one of them while
     leaving the arithmetic correct and pointing at the wrong part of the
     room. Each new figure is (old - 0.06) / 0.75, which is the crop written
     down: 19.1 -> 17.5, 32.0 -> 42.7, 57.4 -> 68.5, 18.6 -> 24.8. The x
     figures are untouched because the width was. */
  /* ⚠ CROPPED AGAIN, TO THE SCREENS, Amr 2026-10-08: take the space either
     side of the wall screen down to a border with the lights in it, and the
     dead wall above it down to the downlights. Measured off the picture
     rather than guessed -- the left light pillar runs x 1-10%, the screen x
     20-80%, the right pillar x 89-99%, and the downlights y 5-7%. x 7-93%
     and y 3-100% keeps a slice of each pillar as a lit border and loses the
     rest.

     ⚠ THE GAIN IS IN THE RATIO, NOT THE PIXELS. The band's width is set by
     the column; its height is width / ratio. Taking the picture from 1.333
     to 1.181 makes the band 606px tall instead of 537 at the same width, so
     the wall screen goes from 428x229 to 498x267 and the monitor from
     210x133 to 244x155. That is 36% more room for the message and enough
     for the staff rail to carry a mood again.

     ⚠ AND EVERY FIGURE IS RE-DERIVED, for the second time in two days: x is
     (old - 0.07) / 0.86 and y is (old - 0.03) / 0.97. A crop leaves every
     percentage arithmetically correct and pointing at the wrong part of the
     room. */
  [data-scene-band="office"] { aspect-ratio: 872 / 981; padding: 0; }

/* ⚠ A LIGHTER WASH FOR A DARK PLATE, because the global one was tuned against
   a light photograph. `--scene-wash` is `rgba(245,244,239,0.36)` -- 36% of the
   page colour laid over the scene so content reads against it. On Amr's
   manager's office, which is a dark room lit by cabinet strips, that much
   cream turns the whole plate milky and throws away the contrast the picture
   was generated for.

   ⚠ AND IT COSTS NOTHING IN LEGIBILITY, because nothing reads off the
   photograph. `scenes: no data is drawn straight on a photograph` already
   forces every text node inside a band onto an opaque plate, so the wash is
   doing aesthetic work only. Dropped to 0.12 here and the saturation left
   alone. */
body[data-scene="office"] { --scene-wash: rgba(245, 244, 239, 0.12); }
  .office-room { display: block; position: absolute; inset: 0; }
  /* ⚠ RE-DERIVED 2026-10-08 FOR A NEW PLATE, and the old numbers are in the
     history rather than here. The scene changed from `office.jpg` 880x745 to
     Amr's manager's office 902x1175, because the screen was too small for the
     content: the Assistant needed TWELVE pages of a 267px box.

     Measured, not estimated. The panel was found as the largest connected
     light region (`scratchpad/screenfind3.py`), bounding box 205,200 576x687
     of 902x1175, and the box was drawn back onto the photograph and LOOKED AT
     before these numbers were written -- an earlier maximal-rectangle fit
     reported a 52x96 board on a plate that is nearly all board, and the only
     thing that caught it was looking.

     left 205/902, top 200/1175, width 576/902 — straight from the measurement.

     ⚠ THE HEIGHT IS CUT SHORT OF THE PANEL ON PURPOSE. The panel runs to
     y~890 but the desk monitor stands IN FRONT of it from y~765 at the left,
     so a slot using the full height would put our plate over an object the
     photograph has nearer the camera. 555/1175 stops above it. */
  /* ⚠ RE-DERIVED 2026-10-09 FOR AMR'S THIRD PLATE. He sent a boardroom with
     one large wall screen. It arrived 1152x1099 PNG with the top 118 rows
     FULLY TRANSPARENT -- which would have shown the page through the ceiling
     -- so it is cropped to 1152x981 and flattened before anything else.

     Measured, then LOOKED AT. The glass was found as the contiguous bright
     run through the screen's mid-row and mid-column, stable from threshold 55
     to 85 at x 239-914, y 205-661 of the raw plate (y 87-543 after the crop).
     The rectangle was then drawn back onto the photograph in green and
     inspected: it sits inside the bezel, on the glass. Arithmetic could not
     have told me that, and this plate's predecessor is where a
     maximal-rectangle fit once reported a 52x96 board on a wall that is
     nearly all board.

     ⚠ AND IT IS CROPPED AT THE SIDES, 140px off each, FOR A MEASURED REASON.
     The band's width is fixed by the grid, so band height = width / ratio: at
     the plate's native 1.174 the band came out 611px and the screen 420x283,
     which is SMALLER than the portrait plate it replaced — and Amr had just
     asked for twelve figures to go on it. They needed 427px.

     Trimming the side shelves takes the plate to 872x981 (0.889), the band to
     807 and the screen to 555x374: +75% of area, and wider, so the labels stop
     wrapping. Six crops were measured before choosing, and the two finalists
     were rendered side by side and looked at — this one cuts the desk monitor's
     left edge, which the other kept, and the screen is 15% larger for it. */
  .office-screen {
    position: absolute; left: 11.35%; top: 8.87%; width: 77.41%; height: 46.48%;
    overflow: auto;
  }
  /* ── the staff inbox, on the desk monitor ──────────────────────────────
     Amr, 2026-10-09: "layed out just like how inboxes are laid out, so on the
     left a pinned menu which most of it will be cut out as the laptop screen
     is cut out from the left. what should appear perfectly is the summary of
     the message with the persona image... it will be easy for a user to
     navigate the personas from the laptop without having to keep scrolling up
     and down."

     ⚠ THE BOX IS THE GLASS'S BOUNDING BOX, MEASURED. x 0-248.6, y 590.7-826.5
     of an 872x981 plate: left 0%, top 60.21%, width 28.51%, height 24.04%.
     The left edge really is 0 -- the monitor is clipped by the frame, which
     is what makes the pinned menu read as cut off rather than as missing.
     The TILT is not here; `fitGlass` in office.js maps this rectangle onto the
     measured quad with a matrix3d, because the top and bottom edges have
     different slopes and no CSS rotate reproduces that.

     ⚠ 204x194 AT 1440, which is a tenth of the wall screen. Five rows at 38px
     is a portrait, a name, a mood and one truncated line. Nothing else fits,
     and trying would produce a row nobody can read. */
  .office-room > .office-rail {
    position: absolute; left: 0; top: 60.21%; width: 28.51%; height: 24.04%;
    margin: 0; padding: 0; overflow: hidden;
    border: 0; border-radius: 0;
    /* ⚠ OPAQUE, AND NOT ONLY FOR LOOKS. `scenes: no data is drawn straight on
       a photograph` walks up from every text node for an opaque background and
       STOPS at `.scene-band`. The glass is near-black in the photograph, so
       this both satisfies the gate and is what the screen would actually look
       like with something on it. */
    background: var(--screen-face);
    transform-origin: 0 0;
  }
  /* the pinned menu is absolute, so it does not take a flex slot */
  /* the pinned menu: scenery, carrying nothing, mostly off the glass */
  .office-room > .office-rail .rail-pin {
    position: absolute; left: 0; top: 0; bottom: 0; width: 34px;
    background: var(--screen-chrome); border-right: 1px solid var(--screen-line);
    display: flex; flex-direction: column; gap: 7px; padding: 9px 0 0 8px;
  }
  .office-room > .office-rail .pin-bar {
    display: block; height: 5px; width: 18px; border-radius: 2px;
    background: var(--screen-bar);
  }
  .office-room > .office-rail .pin-bar:first-child {
    width: 12px; background: var(--screen-bar-on);
  }
  .office-room > .office-rail .rail-head { display: none; }
  .office-room > .office-rail { display: flex; flex-direction: column; }
  .office-room > .office-rail > .rail-slot { flex: 1 1 auto; min-height: 0; }
  .office-room > .office-rail .staff-strip {
    margin-left: 18px; padding: 0; gap: 0; height: 100%;
  }
  /* ⚠ A MESSAGE ROW, NOT A TAB. Portrait, then the name and the mood on one
     line, then the summary every report already builds on a second. */
  .office-room > .office-rail .staff {
    display: grid; width: auto; min-height: 0; height: 20%;
    grid-template-columns: 22px minmax(0, 1fr);
    grid-template-areas: "face name" "face sum";
    column-gap: 7px; row-gap: 0; align-content: center;
    padding: 0 7px 0 6px; border: 0; border-radius: 0;
    background: transparent; text-align: left;
    border-bottom: 1px solid var(--screen-line);
  }
  .office-room > .office-rail .staff:last-child { border-bottom: 0; }
  .office-room > .office-rail .staff .face {
    grid-area: face; width: 22px; height: 22px; border-radius: 50%;
  }
  .office-room > .office-rail .staff .staff-name {
    grid-area: name; font-size: 10.5px; line-height: 13px; font-weight: 650;
    color: var(--screen-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .office-room > .office-rail .staff .staff-sum {
    grid-area: sum; display: block; font-size: 8.5px; line-height: 11px;
    color: var(--screen-ink-dim);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* ⚠ THE MOOD IS A DOT PLUS ITS WORD, AND THE WORD IS NOT OPTIONAL.
     `voices: every one carries a mood beside the name` requires `.staff-mood`
     to be present and non-empty on all five, so shrinking it away would turn
     a passing check into a failing one -- correctly. */
  .office-room > .office-rail .staff .staff-mood {
    grid-area: name; justify-self: end; margin-left: 6px;
    font-size: 8px; line-height: 13px; font-weight: 600;
    max-width: 48%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    color: var(--screen-ink-dim);
    /* ⚠ THE PILL HAS TO BE UNSET, NOT JUST RECOLOURED. The desktop rail rule
       above gives `.staff-mood` `background: var(--chip)` with muted ink — a
       light pill, which on near-black glass rendered as a white blob with
       invisible text on three of the five rows. Caught in a screenshot; the
       computed colour alone looked fine. */
    background: transparent; padding: 0; border-radius: 0;
  }
  .office-room > .office-rail .staff .staff-mood.turf   { color: var(--screen-good); }
  .office-room > .office-rail .staff .staff-mood.danger { color: var(--screen-bad); }
  /* the open message, as an inbox marks one */
  .office-room > .office-rail .staff.on {
    background: var(--screen-row-on);
    box-shadow: inset 2px 0 0 var(--turf);
  }
  .office-room > .office-rail .staff:hover { background: var(--screen-row-hover); }
  .office-room > .office-rail .staff:focus-visible {
    outline: 2px solid var(--screen-good); outline-offset: -2px;
  }
  .office-rail .staff { background: transparent; }
  .office-rail .staff.on { background: var(--turf-soft); }
  /* ⚠ TWO LINES IN THE RAIL, NOT THREE THINGS ON ONE. Name, figure and
     mood abreast in a 360px column cut every figure -- "2 hits · armba…",
     "£4.6m bank · awa…". office-scene.png stacks them: the name with the
     mood hung at the right, and the figure on its own line underneath. The
     portrait spans both. */
  .office-rail .staff {
    display: grid; align-items: center; column-gap: var(--space-4); row-gap: 1px;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "face name mood" "face sum  sum";
  }
  .office-rail .staff .face { grid-area: face; }
  .office-rail .staff .staff-name { grid-area: name; }
  .office-rail .staff .staff-mood { grid-area: mood; margin-left: 0; text-align: right; }
  .office-rail .staff .staff-sum { grid-area: sum; }
  /* ⚠ THE MONITOR IS 352x223 AND FIVE STAFF HAVE TO STAND IN IT. At the
     rail's own sizes a row is 78px and three of the five showed; the type
     comes down until all five do, which is the same trade the locker tablet
     makes. Smaller than this and the mood words stop being readable, so this
     is the floor rather than a preference. */
  /* ⚠ THE MONITOR IS 210x133 NOW, NOT 352x223. The left panel took 460px
     off the room on 2026-10-07 and the rail is a percentage of what is
     left, so every row lost 40% of its width. The name and the mood
     collided -- "Board" printed over "Reviewing your record" -- and two of
     the five showed.

     The summary line goes INSIDE THE RAIL only. It is the same figure the
     screen prints in full the moment you select the member, so in the rail
     it was a truncated copy of something a tap away; dropping it is what
     lets all five stand in the monitor at a readable size. The grid drops
     to two areas with it. */
  /* ⚠ FIVE ROWS AND A HEADING DO NOT GO IN 133px, so the heading goes. The
     monitor's face is a fixed share of a photograph and the photograph lost
     460px to the panel; the budget is what it is. Measured: the head wrapped
     to 35px, the five rows came to 204, and the box is 131. Dropping the
     head and taking the portrait to 20px leaves 124 in 131 with all five
     standing. What the rail is FOR survives without the sentence -- five
     faces in a list on a screen, one of them lit, is a picker. */
  /* ⚠ THE PAGER SITS ON THE SCREEN, NOT UNDER IT. It is part of the message
     it moves through, and under the photograph it would read as page
     controls for the whole room. */
  /* ⚠ THE CARD FILLS THE SCREEN AND THE PAGER IS PINNED TO ITS FOOT. In
     normal flow the nav bar is the last thing in a card taller than the
     screen, so the control that exists to stop you scrolling was itself
     below the fold. The head and the pager hold their places; only the
     page body moves. */
  /* ⚠ PINNED, NOT `height: 100%`. The percentage did not resolve -- every
     other declaration in this block took and that one did not, so the card
     stayed 311px in a 267px screen and the pager it was meant to hold stayed
     below the fold. `inset: 0` against a positioned parent is a definite
     box whatever the chain above it is doing. */
  /* ⚠ THE SCREEN'S FRAME, THE SAME SHAPE THE REPORT USED TO HAVE. Pinned head
     is absent here (a slide carries its own), so it is body + pager only.

     ⚠ `inset: 0`, NEVER `height: 100%` — recorded 40 lines up this file from
     the same fault: the percentage did not resolve in this chain and a card
     stayed 311px inside a 267px box. */
  /* ⚠ ONLY THE FRAME IS DESKTOP-ONLY. The PLATE is declared outside this
     media query -- see `.slides` near the card rules -- because
     `scenes: no data is drawn straight on a photograph` runs at 390px, and a
     plate that only exists from 760px leaves every slide sitting on the
     photograph on a phone. That is exactly how this failed the first time. */
  .office-screen .slides {
    position: absolute; inset: 0; display: flex; flex-direction: column;
    margin: 0; overflow: hidden;
  }
  .office-screen .slides > .slide-body {
    flex: 1 1 auto; overflow: auto; min-height: 0;
  }
  .office-screen .slides > .rnav-bar { flex: none; }

  .office-screen .report {
    position: absolute; inset: 0; display: flex; flex-direction: column;
    margin: 0; overflow: hidden;
  }
  .office-screen .report-head { flex: none; }
  .office-screen .rpages { flex: 1 1 auto; overflow: auto; min-height: 0; }
  .office-screen .rnav-bar { flex: none; }
  .rpages { min-height: 0; }
  /* ⚠ --space-3 DOES NOT EXIST. The scale is 1, 2, 4, 6 -- 4, 8, 16, 24px --
     and I wrote a token between two of them. An undefined custom property
     makes the WHOLE declaration invalid at computed-value time, so `gap` and
     `margin-top` took their initial values and the bar still looked
     plausible; tests/test_css_tokens_exist.py is the thing that noticed, and
     it exists because this stylesheet has shipped that fault before. */
  .rnav-bar {
    display: flex; align-items: center; justify-content: flex-end;
    gap: var(--space-2); margin-top: var(--space-2);
    border-top: 1px solid var(--line); padding-top: var(--space-2);
  }
  .rnav-at { flex: 1 1 auto; text-align: left; }
  .rnav {
    width: 26px; height: 26px; border-radius: 50%; flex: none;
    border: 1px solid var(--line); background: var(--surface-raised);
    color: var(--ink); font: 600 15px/1 var(--font-sans); cursor: pointer;
  }
  .rnav[aria-disabled="true"] { opacity: 0.35; pointer-events: none; }
  .rnav:focus-visible { outline: 2px solid var(--turf); outline-offset: 1px; }

  /* ⚠ A STRIP, NOT A CHART. The screen has about 160px under the head and
     over the pager; fourteen weeks of dot-and-interval is the panel's job.
     What fits here is the shape -- which weeks are good relative to each
     other -- and the best one lit. Bars start at 12% so a worst week is a
     mark rather than nothing. */
  /* the chip's name does not wrap; the line about it gives way instead */
  .chead { display: flex; align-items: baseline; justify-content: space-between;
           gap: var(--space-2); margin-bottom: var(--space-2); }
  .chead .strong { flex: none; white-space: nowrap; }
  .chead .micro { text-align: right; min-width: 0; }
  /* ⚠ 54px, MEASURED DOWN FROM 92. The page came out 52px past the screen
     -- head 34, strip 105 with its labels, footer 46 and the gaps, against
     about 160 of body. The strip is the part that can give: it carries
     shape, not value, and shape survives being short. */
  .cstrip { display: flex; align-items: flex-end; gap: 3px; height: 42px; }
  .cstrip-col { flex: 1 1 0; display: flex; flex-direction: column;
                align-items: center; justify-content: flex-end; height: 100%; }
  .cstrip-bar { display: block; width: 100%; border-radius: 2px 2px 0 0;
                background: var(--line); }
  .cstrip-col.best .cstrip-bar { background: var(--turf); }
  .cstrip-gw { font-size: 9px; line-height: 13px; color: var(--ink-muted); }
  .cstrip-col.best .cstrip-gw { color: var(--turf); font-weight: 700; }
  .cfoot { margin: var(--space-2) 0 0; font-size: 12px; line-height: 16px; }

  /* the same fade the locker tablets use, for the same reason */
  .office-screen.more, .office-rail.more {
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent);
            mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent);
  }
  .office-rail .rail-head { display: none; }
  /* ⚠ THE MOOD IS BACK, AS A CHIP, BECAUSE THE CROP PAID FOR IT. At 196px
     the name and the mood fought and the names cut to "B…" and "A…"; the
     tighter crop of 2026-10-08 makes the monitor 244 wide and there is room
     for both if the mood stops being prose and becomes a mark. Faint on
     purpose -- Amr asked for "a small faint chip": the rail is a picker and
     the mood is a hint about who to pick, not the message itself, which is
     printed in full the moment you choose someone.

     ⚠ THE NAME STILL WINS WHEN THEY FIGHT. `minmax(0, auto)` lets the chip
     give way first, so a long mood ellipsises and the name never does. */
  .office-rail .staff { grid-template-areas: "face name mood"; min-height: 23px;
                        grid-template-columns: auto auto minmax(0, 1fr); }
  .office-rail .staff .staff-mood {
    justify-self: end; max-width: 100%;
    font-size: 9px; line-height: 13px; font-weight: 600;
    padding: 0 5px; border-radius: 7px;
    background: var(--chip); color: var(--ink-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .office-rail .staff + .staff { margin-top: 1px; }
  .office-rail .staff .face { width: 20px; height: 20px; }
  .office-rail .staff .staff-name { font-size: 12px; line-height: 15px; }
  .office-rail .staff .staff-mood { font-size: 10px; line-height: 15px; }
  .office-rail .staff-strip { padding: 0; gap: 0; }
  .office-rail .staff .staff-sum { display: none; }
  .office-rail .staff .staff-mood { white-space: nowrap; }
  .office-rail .staff .staff-name { overflow: hidden; text-overflow: ellipsis;
                                    white-space: nowrap; }
  .office-rail { padding: 3px 6px; }
  .office-rail .rail-head { padding: 0 0 3px; font-size: 10px; }
  .office-rail .staff {
    column-gap: var(--space-2); padding: 1px 5px;
    /* ⚠ min-height WAS THE BINDING CONSTRAINT, not the type. The rows came to
       45px each because `.staff` carries `min-height: 44px` -- a thumb-sized
       tap target, which is right in a full-width rail and is the whole budget
       here. The portrait is 26px and two lines of this type are 30px, so 32
       is the content's own height rather than a number chosen to fit.

       ⚠ AND 32 BECAME 23 WHEN THE PANEL TOOK 460px, 2026-10-07. The budget
       halved with the room: 131px of monitor for five staff. One line of
       13px type is 16 and a 20px portrait is 20, so 23 is again the
       content's own height and not a number chosen to fit -- it is just a
       smaller content. Five rows at 23 with 1px between them is 119 in
       131. */
    min-height: 23px;
  }
  .office-rail .staff + .staff { margin-top: 1px; }
  .office-rail .staff .face { width: 20px; height: 20px; }
  /* line-heights set, not inherited: the rows came to 43px against a 38px
     budget purely on leading, which is the cheapest 5px on the screen. */
  .office-rail .staff .staff-name { font-size: 13px; line-height: 16px; }
  /* and the wall screen takes the same treatment, for the same reason */
  .office-screen .report { padding: 12px 14px; }
  .office-screen .report-head .face { width: 42px; height: 42px; }
  /* ⚠ THE MOOD LINE IS THE BINDING PAGE, not the paragraphs. With the tiles
     compacted the first page still came to exactly 0px of headroom, and the
     mood sentence is the half of it that is prose: two lines at the page's
     own size. A point down and the page has room for a longer mood next
     week, which is the only thing that varies here. */
  .office-screen .mood-line { margin: 4px 0 8px; font-size: 13px; line-height: 18px; }
  /* ⚠ THE TILES COME DOWN TO WHAT THE SCREEN HAS LEFT. The head takes 52px
     of 267 and the pager 38, so the page body is about 160 -- and the mood
     line is two of those at 40px. 28px figures with 13px labels made a tile
     95px tall and the row was cut across the middle. Measured down until
     three tiles and the mood stand in the body together. */
  .office-screen .mtiles { gap: 7px; margin-bottom: 0; }
  .office-screen .mtile { padding: 6px 8px; }
  .office-screen .mtile-k { font-size: 11px; line-height: 14px; }
  .office-screen .mtile-v { font-size: 21px; line-height: 25px; }
  .office-screen .mtile-n { font-size: 10px; line-height: 13px; }
  /* ⚠ THE LONGEST PARAGRAPH SETS THIS, not the average one. With the body
     at its page size the Board's third chunk and Finance's second ran 4px
     past the screen -- four pixels of a sentence, which is exactly the fault
     the paging was added to remove. Sized so every page of every member of
     staff clears it; check_layout.js walks all of them. */
  .office-screen .para { padding: 0; }
  .office-screen .body { font-size: 15px; line-height: 21px; }
}

.locker { flex: 1 1 0; min-width: 0; display: none; }
.locker.on { display: block; }

.lk-strip {
  display: flex; align-items: center; justify-content: center;
  height: 30px; border: 2px solid var(--line); border-radius: 4px;
}
/* ⚠ THE CODE IS THE IDENTIFICATION, NOT A LABEL ON THE COLOUR. It sits on
   white so it holds its contrast against any of the twenty club primaries,
   36 of whose 190 pairs fall under the separation floor. */
.lk-code {
  background: var(--club-pill); color: var(--ink); border-radius: 4px;
  padding: 2px 10px; font-size: 16px; font-weight: 800;
}
.lk-rail { position: relative; display: flex; justify-content: center; padding: 10px 0 0; }
/* ⚠ ON THE SLEEVE, IN THE SHIRT'S OWN PERCENTAGES. `top: 34px; right: 10px`
   was measured against `.lk-rail` when that was a flow box 10px tall; the rail
   is the whole bay now, so the badge sat 34px below the top of the cabinet --
   above the nameplate, outside the locker, on the back wall. The jersey asset
   puts the right sleeve at about a quarter of the way down, so that is where
   an armband goes, and it follows the shirt to every width for free.

   ⚠ 13%, NOT 3%: THE BOX IS WIDER THAN THE GARMENT. The jersey is a PNG
   with transparent surround, so the shirt's own edge is not its element's
   edge -- at 3% the C sat in the gap between the sleeve and the bay wall,
   on nothing at all. The sleeve hem is about 88% across the asset. */
.lk-band { position: absolute; z-index: 3; top: 25%; right: 13%;
           padding: 3px 7px; border-radius: 4px; font: 700 12px/1 system-ui, sans-serif; }
/* ⚠ ONE BAY AT NARROW WIDTHS, AND THE PANEL BELOW THE SCENE. Under 760px the
   carousel draws a single locker, and it was floating over a backdrop of five
   -- the shirt over one bay's woodwork and the panel over another's.
   deadline-phone.jpg is the middle bay with its neighbours showing either
   side, cropped from the same photograph at x 30-70%, y 8-82%. Inside that
   crop the bay runs 28.5-70.8%, the nameplate 8.2-21.8%, the rail 33.8%.

   ⚠ AND THE TILE SITS UNDER THE BAND, which is what changing-room-phone.png
   does. A full-height single bay is four times taller than it is wide: shown
   whole at page width the band came out 1,420px on a 390px phone, a locker
   you scroll past rather than look at. */
.lk-stage { position: relative; }
.lk-tiles { margin-top: 10px; }
.lk-tile { display: none; }
.lk-tile.on { display: block; }

@media (max-width: 759px) {
  [data-scene-band="deadline"] {
    --scene: url("../img/scenes/deadline-phone.jpg");
    aspect-ratio: 461 / 707; padding: 0;
  }
  .lockers { position: absolute; inset: 0; display: block; }
  .locker.on { position: absolute; left: 28.5%; width: 42.3%; top: 0; bottom: 0; }
  .locker .lk-strip {
    position: absolute; left: 0; right: 0; top: 8.2%; height: 13.6%;
    margin: 0; display: flex; align-items: center; justify-content: center;
  }
  .locker .lk-rail { position: absolute; inset: 0; padding: 0; }
  /* ⚠ SAME WIDTH-FIRST RULE AS THE DESKTOP BAY, AND HERE IT WAS WORSE.
     `height: 62%` made the shirt 256px wide inside a 151px opening, so it
     hung half out of the locker on both sides -- a garment wider than the
     cupboard it is in. The opening is 17% of the scene wide and 47% tall,
     a ratio of 0.44 against a jersey's 0.75, so no shirt can ever reach
     this floor: it hangs in the upper part of the opening and the lower
     part stays as the photograph has it, with the tablet below the band. */
  .locker .lk-shirt {
    position: absolute; top: 31.9%; width: 96%; height: auto;
    aspect-ratio: 364 / 483; max-width: 96%;
    left: 50%; transform: translateX(-50%); padding: 28% 10% 0;
  }
}

/* ⚠ THE JERSEY IS THE PACK'S PHOTOGRAPH, IN TWO LAYERS. design/assets ships
   it with the hanger attached; split, the cloth can take a colour and the
   wood cannot. Its own 364x483 is the proportion, so nothing here is a
   guess about how a shirt is shaped.

   ⚠ --kit-tint IS THE WHOLE COLOURING MECHANISM. Unset, the cloth prints as
   the white photograph it is. Set to a colour, `multiply` turns white into
   that colour and the grey folds into darker shades of it, so the garment
   keeps its shading instead of becoming a silhouette. Solid colours only --
   a white photo cannot become stripes -- and very dark values crush the
   folds, which is why the palette is chosen rather than free. */
.lk-shirt {
  position: relative; width: 100%; max-width: 190px;
  aspect-ratio: 364 / 483;
  padding: 26% 8px 8px;
  text-align: center;
  filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.28));
}
.lk-cloth, .lk-wood {
  position: absolute; inset: 0; z-index: 0;
  background-position: center top; background-repeat: no-repeat;
  background-size: 100% 100%;
}
/* ⚠ THE TINT IS MASKED TO THE CLOTH, NOT PAINTED BEHIND IT. The first
   version set background-color and blended with multiply, which fills the
   whole BOX: the jersey's transparent surround came out as a white
   rectangle sitting over the locker. A masked overlay tints only where the
   garment actually is, and multiply still keeps the folds. */
.lk-cloth { background-image: url("../img/kit/jersey-cloth.png"); }
.lk-cloth::after {
  content: ""; position: absolute; inset: 0;
  background: var(--kit-tint); mix-blend-mode: multiply;
  -webkit-mask-image: url("../img/kit/jersey-cloth.png");
          mask-image: url("../img/kit/jersey-cloth.png");
  -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
.lk-wood  { background-image: url("../img/kit/jersey-hanger.png"); z-index: 2; }

/* Above the cloth, below the hanger. */
/* ⚠ SET ON THE ROOT FROM localStorage, so every shirt on the page changes
   together and nothing has to be re-rendered. White is the multiply no-op, so
   an unset choice leaves the photograph exactly as it was shot. */
:root[data-kit="crimson"] { --kit-tint: var(--kit-crimson); --kit-ink: var(--on-turf); --kit-ink-muted: var(--on-turf); }
:root[data-kit="royal"]   { --kit-tint: var(--kit-royal);   --kit-ink: var(--on-turf); --kit-ink-muted: var(--on-turf); }
:root[data-kit="teal"]    { --kit-tint: var(--kit-teal);    --kit-ink: var(--on-turf); --kit-ink-muted: var(--on-turf); }
:root[data-kit="violet"]  { --kit-tint: var(--kit-violet);  --kit-ink: var(--on-turf); --kit-ink-muted: var(--on-turf); }
:root[data-kit="sky"]     { --kit-tint: var(--kit-sky); }
:root[data-kit="amber"]   { --kit-tint: var(--kit-amber); }
:root[data-kit="coral"]   { --kit-tint: var(--kit-coral); }
:root[data-kit="lime"]    { --kit-tint: var(--kit-lime); }

.lk-name, .lk-num, .lk-pos { position: relative; z-index: 1; }
.lk-name { display: block; font-size: 11px; font-weight: 600; letter-spacing: 0.05em;
           color: var(--kit-ink); overflow: hidden; white-space: nowrap; }
/* ⚠ NO UNIT ON THIS NUMBER. per_gameweek.absolute_valid is false: it orders
   players within a position and must not be read as FPL points. */
.lk-num { display: block; font-size: 56px; line-height: 60px; font-weight: 700;
          color: var(--kit-ink); font-variant-numeric: tabular-nums; }
.lk-pos { display: block; font-size: 13px; font-weight: 700; letter-spacing: 0.08em;
          color: var(--kit-ink-muted); }
/* ⚠ THE CHEVRONS SIT OVER THE SHIRT, NOT THE MIDDLE OF THE BAND. The band
   holds the locker AND its touchscreen, so a button centred on it lands on
   the figures and covers two of them. 26% puts it on the rail, where there
   is nothing to hide. */
.scene-band .pitch-arrow { top: 26%; }

/* The touchscreen in the locker. The turf ring means "in the eleven"; its
   absence means the bench, and the tile is the same height either way so the
   locker does not change shape between the two. */
.tile { position: relative; z-index: 1; margin-top: 10px; border-radius: 22px; padding: 4px; }
.tile.starting { background: var(--turf); }
.bezel { position: relative; background: var(--ink); border-radius: 18px; padding: 8px; }
.camera { position: absolute; top: 3px; left: 50%; transform: translateX(-50%);
          width: 4px; height: 4px; border-radius: 50%; background: var(--ink-muted); }
.panel { background: var(--surface); border-radius: 10px; padding: 12px; }
.tname { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin-bottom: 6px; }
.tname-n { font-size: 20px; font-weight: 700; }
.tname-p { font-size: 13px; font-weight: 700; letter-spacing: 0.04em; color: var(--ink-muted); }
.tfx { margin-left: auto; font-size: 13px; font-weight: 700; display: flex; align-items: center; gap: 6px; }
.binchip { padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 700; }
.binchip.b0 { background: var(--fixture-hard); color: var(--on-fill); }
.binchip.b1 { background: var(--fixture-tough); color: var(--ink); }
.binchip.b2 { background: var(--fixture-fair); color: var(--ink); }
.binchip.b3 { background: var(--fixture-good); color: var(--on-fill); }

.tfig { display: flex; flex-wrap: wrap; align-items: baseline;
        justify-content: space-between; gap: 2px 10px;
        min-height: 32px; border-top: 1px solid var(--line); }
.tfig:first-of-type { border-top: 0; }
.tfig-k { flex: 1 1 60%; min-width: 0; font-size: 15px; line-height: 20px; color: var(--ink-muted); }
.tfig-sub { color: var(--ink-muted); }
.tfig-v { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* ⚠ THE ONE THAT DIFFERS IS THE ONE WORTH READING. The minutes model has six
   buckets, so twelve of fifteen share a value; the three that do not are
   drawn in full ink and the shared ones stay muted. */
.tfig-v.lone { color: var(--ink); }
.tfig-k + .tfig-v { color: var(--ink); }
.held { color: var(--amber); font-weight: 700; }
.tnote { font-size: 13px; line-height: 18px; color: var(--ink-muted); padding: 2px 0 6px; }
.thinstrip { margin: 8px 0 0; padding: 4px 8px; border-radius: 4px;
             background: var(--danger-soft); color: var(--danger);
             font-size: 12px; font-weight: 700; text-align: center; }

/* ⚠ SPANS, NOT DISABLED BUTTONS. A disabled button reads as a control
   someone could enable; there is nothing here to enable. */
.statebox { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; min-height: 30px; }
.pill { padding: 5px 12px; border-radius: 999px; font-size: 13px; font-weight: 700;
        background: var(--surface-raised); border: 1px solid var(--line); color: var(--ink); }
.pill.on { background: var(--turf); color: var(--on-turf); border-color: var(--turf); }

.dots { display: flex; justify-content: center; gap: 6px; margin: -4px 0 12px; }
.dot { width: 8px; height: 8px; border-radius: 999px; border: 0; padding: 0;
       background: var(--line); cursor: pointer; }
.dot.on { width: 20px; background: var(--turf); }

.teamsheet { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
.teamsheet .btn { display: inline-block; margin-top: 8px; }
/* The legend is prose about what the colours mean and it sits on the page,
   not in a card, so it needs the same plate every other direct child gets. */
.legendwrap {
  margin-top: 14px; padding: 14px 16px;
  background: var(--surface-raised); border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}

@media (min-width: 760px) {
  /* ⚠ FIVE BAYS ON SCREEN, AND THE ARROWS STILL DRAWN. They used to be
     hidden here because the three position tabs did the paging; Amr removed
     those on 2026-10-06, so these are now the only way to reach the other ten
     players. A cabinet of five with no way past it is a squad of five. */
  .locker { display: block; }
  .scene-band .pitch-arrow { display: flex; }
  /* and the open bay is marked, because the board below follows it */
  .locker.on .lk-strip { box-shadow: 0 0 0 3px var(--turf); border-radius: 4px; }
  .locker.on .lk-tile, .lk-tile.on { outline: 2px solid var(--turf); outline-offset: -1px; }

  /* ⚠ THE CABINET IN THE PHOTOGRAPH IS THE LAYOUT, measured off
     deadline.jpg (1152x956) rather than approximated by a flex row. Scanning
     the nameplate band gives five bays at 3.7, 22.7, 41.4, 60.0 and 78.8 per
     cent, each about 17 wide; scanning down a bay gives the nameplate at
     14.1-24.1%, the hanging rail at 33.0%, and the floor of the opening at
     80.1%. The band takes the picture's own aspect so `cover` crops nothing,
     which is the only thing that lets those be CSS percentages -- the same
     reason Review's pitch needed cropping.

     Before this the shirts hung above the drawn rail, the club strips sat
     over the woodwork rather than in the nameplate, and the tiles ran out of
     the cabinet and past the bench. */
  [data-scene-band="deadline"] { aspect-ratio: 1152 / 956; padding: 0; }
  .lockers { position: absolute; inset: 0; display: block; gap: 0; }
  .locker { position: absolute; top: 0; bottom: 0; }
  .locker:nth-of-type(1) { left: 3.7%;  width: 17.4%; }
  .locker:nth-of-type(2) { left: 22.7%; width: 17.1%; }
  .locker:nth-of-type(3) { left: 41.4%; width: 16.9%; }
  .locker:nth-of-type(4) { left: 60.0%; width: 17.2%; }
  .locker:nth-of-type(5) { left: 78.8%; width: 17.0%; }

  /* The club's colours go in the nameplate, which is what it is for. */
  .locker .lk-strip {
    position: absolute; left: 0; right: 0; top: 14.1%; height: 10.0%;
    margin: 0; display: flex; align-items: center; justify-content: center;
  }
  /* ⚠ THE HANGER HOOKS OVER THE RAIL, AND THE SUM DEPENDS ON THE SHIRT'S
     HEIGHT. In the asset the hanger's bar sits at about 5.3% of the
     jersey's own height, so top = rail - 5.3% x height. Rail 33.0%, shirt
     26.7% of the band -> 31.6%. Change the shirt's size and this number is
     wrong again: the first time round it was 30.4% for a 49.6% shirt, and
     shortening the shirt lifted the hook 14px clear of the bar. */
  .locker .lk-rail { position: absolute; left: 0; right: 0; top: 0; bottom: 0; padding: 0; }
  /* ⚠ THE WIDTH DRIVES IT, NOT THE HEIGHT, AND THAT IS NOT A PREFERENCE.
     Set as `height: 49.6%; width: auto; max-width: 96%`, the clamp won: an
     explicit height with a clamped width beats `aspect-ratio`, so the shirt
     rendered 229x566 against its own 229x304 and the cloth was stretched to
     half again its height. The bay is the narrow dimension, so the bay is
     the one to set -- and then the opening's height is what is left over
     for the tablet, instead of the two fighting for it. */
  .locker .lk-shirt {
    position: absolute; top: 31.6%; width: 96%; height: auto;
    aspect-ratio: 364 / 483; max-width: 96%;
    left: 50%; transform: translateX(-50%);
    /* ⚠ THE TEXT GOES ON THE TORSO. Zeroed, the name and figure printed up
       in the hanger. The shoulders are the asset's top ~28%, so the padding
       is what puts the lettering on the back of the shirt. */
    padding: 28% 10% 0;
  }
  /* ⚠ THE TILES ARE LAID BACK OVER THE CABINET. They live after the band in
     the DOM so the phone can flow them underneath it; here they are pulled
     up by exactly the band's height and placed in their own bay, which reads
     as the panel standing inside the locker.

     ⚠ AND SEVEN ROWS DO NOT FIT, WHICH IS A REAL CONSTRAINT. The lower half
     of an opening is 25% of the band and this build's wording wants 43-51%
     in a 230px bay, so the rest scrolls. The ways out are fewer figures in
     the locker or the pack's terser labels, and both are Amr's call -- the
     wording ruling currently keeps these sentences. */
  /* `.lk-stage` is the positioned box the band and the tiles share, so
     `inset: 0` here is exactly the band -- no custom property carrying a
     height around, and nothing to go undefined.

     ⚠ THE STAGE TAKES THE BAND'S RATIO AND THE BAND FILLS IT, which is the
     difference between exact and nearly. Left to flow, the stage was the
     band plus its 12px margins, so `inset: 0` was a box 7px taller than the
     photograph and every tile stood 7px through the floor. Percentages are
     only the scene's percentages if the box they measure IS the scene. */
  /* ⚠ THE STAGE TAKES THE BLEED, NOT THE BAND. On Deadline the band is
     `inset: 0` inside this box, so a negative margin on the band does
     nothing -- the stage is what decides where the photograph ends. Widening
     it leaves every bay percentage true, because they are percentages of the
     band and the band is still the whole stage. */
  .lk-stage {
    aspect-ratio: 1152 / 956; margin: 12px -32px 12px 0;
  }
  .lk-stage > [data-scene-band="deadline"] {
    border-top-right-radius: 0; border-bottom-right-radius: 0;
  }
  .lk-stage > [data-scene-band="deadline"] { position: absolute; inset: 0; margin: 0; }
  .lk-tiles { position: absolute; inset: 0; margin: 0; pointer-events: none; }
  .lk-tile {
    display: block; position: absolute; pointer-events: auto;
    bottom: 19.9%; top: 59%; margin: 0; overflow: auto; padding: 3px;
    scrollbar-width: thin;
  }
  /* ⚠ THE FADE IS A MASK, NOT AN OVERLAY, AND ONLY ON THE ONES THAT SCROLL.
     An ::after would scroll away with the content it is meant to be hinting
     at; a mask belongs to the element and stays at its bottom edge. The class
     is set from JS after layout because no CSS selector can ask whether a box
     overflows. Without it the last row simply stopped mid-sentence, which
     reads as a defect rather than as a panel with more in it. */
  .lk-tile.more {
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 20px), transparent);
            mask-image: linear-gradient(to bottom, #000 calc(100% - 20px), transparent);
  }
  .lk-tile:nth-of-type(1) { left: 3.7%;  width: 17.4%; }
  .lk-tile:nth-of-type(2) { left: 22.7%; width: 17.1%; }
  .lk-tile:nth-of-type(3) { left: 41.4%; width: 16.9%; }
  .lk-tile:nth-of-type(4) { left: 60.0%; width: 17.2%; }
  .lk-tile:nth-of-type(5) { left: 78.8%; width: 17.0%; }
  .lk-tile .panel { padding: 6px 7px; }
  .lk-tile .tfig { padding: 2px 0; font-size: 11px; line-height: 14px; }
  .lk-tile .tfig-v { font-size: 11px; }
  .lk-tile .tname-n { font-size: 13px; }
  .lk-tile .tname-p, .lk-tile .tfx { font-size: 10px; }
  .lk-tile .statebox, .lk-tile .pill { font-size: 10px; padding: 2px 6px; }
  /* ⚠ THE BRACE ABOVE THIS LINE CLOSED THE MEDIA QUERY EARLY, and the seven
     rules below it have been applying at EVERY width since 41d19a2. The one
     that showed was `.scene-band .pitch-arrow { display: none }`: correct at
     desktop, where five bays are on screen at once and there is nothing to
     page through, and ruinous on a phone, where the arrows and the dots were
     the only way to reach the other fourteen players. Amr found it by opening
     the page. The stylesheet still parsed, every rule was valid, and the
     layout gate passed 146 of 146 -- it asks what things look like at two
     widths, and a missing control looks like nothing at all. */
  /* ⚠ THE DOTS STAY OFF AT DESKTOP AND THE ARROWS DO NOT. Fifteen dots under
     a cabinet showing five is a control that says nothing you cannot already
     see; the arrows are how you reach the other ten. This pair used to say
     `display: none` to both, from back when the position tabs did the paging. */
  .dots { display: none; }
  .lk-num { font-size: 40px; line-height: 44px; }
  .tfig { min-height: 24px; }
  .tfig-k, .tfig-v { font-size: 13px; }
  .tname-n { font-size: 16px; }
  .panel { padding: 8px; }
}

/* ── The Office: the season standing band and the squad board ─────────── */
.standing {
  margin: 12px 0; padding: 14px 16px;
  background: var(--surface-raised); border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}
.standing-row { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 15px; line-height: 22px; }
.moverow { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 10px 0; font-size: 15px; }

/* The two build stamps are data -- two run ids and two times -- so they sit
   on a plate like everything else on a scene page. */
.page.office > .prov {
  margin-top: 14px; padding: 12px 16px;
  background: var(--surface-raised); border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}

/* ⚠ ITEM 17: IT IS A WHITE BOARD AND THE PITCH IS DRAWN ON IT. office.png
   puts the markings behind the cards in thin blue marker -- a halfway line, a
   centre circle, the two boxes. Decoration, and also the thing that makes four
   rows of cards read as a formation rather than as four lists. Drawn with
   gradients on the container so there is no asset to ship and nothing to go
   missing; `aria-hidden` on the elements that carry it, so a screen reader is
   not told about a drawing. */
/* ⚠ THE FIRST SET OF MARKINGS WERE NOISE, NOT A PITCH. A centre circle at
   `circle at 50% 50%` with a fixed 70px radius is a small arc floating in a
   1000px box, and a halfway line down the middle cut straight through the
   cards. Both were technically "pitch markings" and neither read as one.
   What the pack actually draws is the goal end behind the keeper: a penalty
   box and a six-yard box at the top, the board's border, and nothing else.
   Proportional to the board rather than a fixed radius, so it stays a box at
   any width. */
.pboard {
  position: relative; margin-top: 10px; padding: 14px 12px 16px;
  border: 1px solid var(--pitch-line); border-radius: 8px;
  background: var(--surface-raised);
  overflow: hidden;
}
/* ⚠ THE PITCH MARKINGS ARE GONE, AFTER THREE ATTEMPTS, AND THE REASON IS
   WORTH KEEPING. The pack's board has the pitch drawn on it in blue marker,
   so three versions were built: a centre circle and halfway line (a stray arc
   and a line through the cards), then a goal end reaching into the keeper's
   row (a rectangle stopping mid-card), then a goal end sized to the head
   band. The third was the closest and still read, at 1126px on the live
   page, as two empty table cells cutting through "THE WHOLE SQUAD" --
   indistinguishable from a broken header.

   A decoration that reads as a defect is worse than no decoration, and this
   board does not need it: the blue border, the dashed row separators, the
   position labels and fifteen cards pinned in their club's colours already
   say tactics board. Matching a render by shipping something that looks
   broken is matching the wrong thing.

   Kept deliberately rather than retried: three variants is enough evidence
   that CSS rectangles on this container do not read as a pitch at the widths
   this page is used at. If it is wanted, it wants an SVG drawn to the
   board's own proportions, not another border trick. */
.pboard-head, .pboard-row { position: relative; }
.pboard-head {
  display: flex; justify-content: space-between; align-items: baseline;
  margin-bottom: 6px; padding-bottom: 6px;
  border-bottom: 1px solid var(--pitch-line);
}
.pboard-row { display: flex; align-items: flex-start; gap: 8px; padding: 10px 0;
              border-top: 1px dashed var(--pitch-line); }
.pboard-row:first-of-type { border-top: 0; }
.pboard-k { flex: none; width: 34px; font-size: 12px; font-weight: 700; color: var(--ink-muted); }
.pboard-c { display: flex; flex-wrap: wrap; gap: 10px; flex: 1 1 auto; }
.pcard {
  position: relative; width: 168px; padding: 14px 8px 8px; text-align: left;
  background: var(--surface-raised); border: 1px solid var(--line); border-radius: 8px;
  /* lifted off the board, as a pinned card is */
  box-shadow: 0 1px 3px rgba(20, 24, 28, 0.13);
}
/* ⚠ THE KIT, FROM THE GENERATED BLOCK. The bar carries `data-club` and no
   inline colour, so a striped club gets its gradient from the stylesheet --
   an inline background would beat every rule and render all seven of them
   plain, which is the fault already recorded against the roundel. */
.club-bar {
  position: absolute; left: 0; right: 0; top: 0; height: 6px;
  border-radius: 7px 7px 0 0;
}
/* the red magnet holding the card to the board */
.pc-magnet {
  position: absolute; top: -5px; left: 50%; width: 11px; height: 11px;
  margin-left: -5.5px; border-radius: 50%;
  background: radial-gradient(circle at 35% 32%,
    var(--magnet-lit) 0 22%, var(--magnet) 60%, var(--magnet-dark) 100%);
  box-shadow: 0 1px 2px rgba(20, 24, 28, 0.35);
}
.pc-head { display: flex; align-items: baseline; gap: 6px; margin-bottom: 6px; }
.pc-meta { margin-left: auto; font-family: var(--font-mono); font-size: 10px;
           color: var(--ink-muted); white-space: nowrap; }
.pc-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px;
           background: var(--line); border: 1px solid var(--line); border-radius: 4px; }
.pc-cell { background: var(--surface-raised); padding: 3px 5px; min-width: 0; }
.pc-k { font-size: 9px; line-height: 12px; color: var(--ink-muted);
        overflow: hidden; text-overflow: ellipsis; }
.pc-v { font-family: var(--font-mono); font-size: 12px; line-height: 16px;
        font-weight: 600; min-height: 16px; }
/* ⚠ THE RING MEANS "IN THE ELEVEN YOU SENT", and nothing else on this card
   says so. It is the same signal as Deadline's turf ring on the tile. */
.pcard.starting { box-shadow: inset 0 0 0 2px var(--turf); }
.pcard-n { font-size: 13px; font-weight: 700; overflow: hidden;
           text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.pcard-p { font-family: var(--font-mono); font-size: 11px; color: var(--ink-muted); }


/* ── the shared table component ───────────────────────────────
   ⚠ MOVED HERE FROM season.css, WHICH ONLY ONE PAGE LOADS. Every screen
   draws FPL.sortable tables and four of the five were getting no table
   styling at all -- the visible symptom was a header reading
   "Owned by% of managers", because `.hnote` carries `display: block` and was
   never loaded. A component in a per-screen stylesheet is a component three
   screens do not have. */
.panel-sub { color: var(--ink-muted); font-size: 13px; line-height: 1.45; }

.tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
.tbl th, .tbl td { padding: 6px 8px; text-align: right; border-bottom: 1px solid var(--line); white-space: nowrap; }
.tbl th:first-child, .tbl td:first-child { text-align: left; }
/* ⚠ WHAT `align: 'l'` ASKS FOR, FINALLY HONOURED. The option emitted no class
   and only the first cell was overridden, so every other column that asked to
   be left-aligned was right-aligned and nothing said so. */
.tbl th.l, .tbl td.l { text-align: left; }
.tbl th { color: var(--ink-muted); font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
.tbl td.n { font-family: var(--font-mono); }
.tbl tr.flagged td:first-child::after { content: " ●"; color: var(--amber); }

.sortable { cursor: pointer; user-select: none; white-space: nowrap; }
.sortable:hover { color: var(--ink); }
.sortable.on { color: var(--ink); }
.sortable .arrow { margin-left: 3px; }

/* ⚠ THE CAVEAT RIDES IN THE HEADER. It is smaller than the label but it is
   IN the column, not under the table -- a sorting reader has already scrolled
   past anything beneath. */
.hnote {
  display: block; font-weight: 400; text-transform: none; letter-spacing: 0;
  font-size: 10px; color: var(--amber);
}
.pen { color: var(--turf); font-weight: 600; }
.flagmark { color: var(--amber); }

.withbadge { display: inline-flex; align-items: center; gap: 6px; }
.tbl .badge { width: 16px; height: 16px; object-fit: contain; flex: 0 0 auto; }

/* ── the icon row (house-style.png, panel 2) ──────────────────────────────
   Seven rows under a shirt: icon, label, bar, note. The icon cell is empty
   on the second row of a pair, so boot/his and boot/club read as one group
   without repeating the glyph. */
/* ⚠ TWO COLUMNS UNDER HIS AND CLUB, as player-card.png places it; the seven
   stacked rows were house-style.png's specimen listing. The money row spans
   both, which is what the render does with it. */
.iconrow { margin-top: 10px; display: grid; gap: 4px 12px;
           grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.irow-h { font-size: 9px; font-weight: 700; letter-spacing: 0.08em;
          text-transform: uppercase; color: var(--ink-muted); text-align: center; }
.irow-wide { grid-column: 1 / -1; }

/* ── the line from zero. See FPL.lineFromZero for the ±6 and where it came
   from. The zero tick is dead centre, because the quantity is signed and a
   tick anywhere else would make one direction look longer than it is. */
.zline { position: relative; display: block; width: 100%; height: 12px; }
.zline-t { position: absolute; left: 0; right: 0; top: 5.5px; height: 1px;
           background: var(--line); }
.zline-0 { position: absolute; left: 50%; top: 1px; width: 2px; height: 10px;
           margin-left: -1px; background: var(--ink); }
.zline-b { position: absolute; top: 3px; height: 6px; border-radius: 1px; }
.zline-b.pos { left: 50%; background: var(--turf); }
.zline-b.neg { right: 50%; background: var(--danger); }
/* Thin evidence: muted, SAME LENGTH. */
.zline-b.pos.thin { background: var(--turf-soft); }
.zline-b.neg.thin { background: var(--danger-soft); }
/* Clipped at the edge, and it has to look clipped rather than merely long. */
.zline.over .zline-b { border-radius: 1px 0 0 1px; }
.zline .dash { position: absolute; left: 50%; top: 4px; margin-left: 3px; }

.irow {
  display: grid; grid-template-columns: 24px 42px minmax(56px, 1fr) minmax(0, 1.6fr);
  gap: 8px; align-items: center; min-height: 22px;
}
.irow-i { display: flex; align-items: center; justify-content: center; color: var(--ink); }
.irow-i svg { width: 18px; height: 18px; }
.irow-k { font-size: 11px; color: var(--ink-muted); }
.irow-b { display: flex; align-items: center; }
.irow-b .bar { width: 100%; }
/* ⚠ THE NOTE IS NOT OPTIONAL. Every bar in this row either carries a figure
   or says which absence it is waiting on -- "scale pending" and "not in
   feed" are different states and a bare hatched bar cannot tell them apart. */
.irow-n { font-size: 11px; line-height: 14px; color: var(--ink-muted); }
@media (min-width: 760px) {
  .irow { grid-template-columns: 24px 46px minmax(64px, 1fr) minmax(0, 1.5fr); }
  .irow-k, .irow-n { font-size: 12px; }
}

/* ── the player detail page, built to player-detail.png ──────────────────
   ⚠ NO SCENE. The render stands it on the plain surface: no photograph, no
   band. It is the one screen that is all figures. */
.pbar { display: flex; align-items: baseline; gap: var(--space-4); flex-wrap: wrap;
        padding-bottom: 10px; border-bottom: 1px solid var(--line); }
.pbar .back { font-weight: 650; }
.pbar-pick { margin-left: auto; flex-direction: row; align-items: center; gap: 8px; }
.phero { display: flex; gap: var(--space-6); align-items: flex-start; flex-wrap: wrap; }
.phero-l { display: flex; gap: var(--space-4); align-items: flex-start; min-width: 0; flex: 1 1 320px; }
.phero-l .display { margin: 0; }
.phero-r { flex: 0 1 300px; min-width: 0; background: var(--surface);
           border: 1px solid var(--line); border-radius: var(--radius-md); padding: 10px 12px; }
.phero-fx { display: flex; align-items: center; gap: 8px; font-size: 19px; margin: 2px 0 6px; }
/* Three panels abreast at desktop, stacked below it, as the render has them. */
.pcols { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; }
@media (min-width: 900px) { .pcols { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.prow { display: flex; align-items: flex-start; gap: var(--space-4);
        padding: 7px 0; border-top: 1px solid var(--line); }
.card-head + .prow, .psec + .prow { border-top: 0; }
.prow-k { flex: 1 1 auto; min-width: 0; font-size: 14px; }
.prow-sub { font-size: 11px; line-height: 15px; color: var(--ink-muted); }
.prow-v { flex: none; font-family: var(--font-mono); font-size: 15px; font-weight: 700;
          text-align: right; }
/* ⚠ A SECTION RULE, NOT A SECOND CARD. The render divides "measured this
   season" from the modelled rows above it inside one panel, because the two
   belong to one subject and are different KINDS of number. */
.psec { margin: 12px 0 2px; font-size: 11px; font-weight: 700; letter-spacing: 0.05em;
        text-transform: uppercase; color: var(--ink-muted); }
.pset { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 6px; }
.pset-c { border: 1px solid var(--line); border-radius: var(--radius-md);
          padding: 6px 8px; text-align: center; min-width: 0; }
.pset-k { font-size: 10px; line-height: 14px; color: var(--ink-muted); }
.pset-v { font-family: var(--font-mono); font-size: 15px; font-weight: 700; min-height: 20px; }
.pfx { display: flex; align-items: center; gap: 8px; padding: 6px 0;
       border-top: 1px solid var(--line); font-size: 14px; }
.card-head + .pfx { border-top: 0; }
.pfx-gw { flex: none; width: 42px; font-family: var(--font-mono); font-size: 12px;
          color: var(--ink-muted); }
.pfx-o { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
         white-space: nowrap; }
.pfx-v { flex: none; font-family: var(--font-mono); font-weight: 700; }
.pfoot { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; }
.pfoot .btn { margin-left: auto; }

/* Office's board card, turned over: the player-card front.

   ⚠ WIDER THAN THE SQUAD FACE, AND THE NOTE TAKES ITS OWN LINE. The render's
   card is about 250px and its notes are one word -- "fair", "tough", "–" --
   so its two columns hold icon, label, bar and word on one line. This build's
   notes are the fuller ones the wording ruling keeps ("tackles, blocks,
   saves: not in feed"), and at 168px split in two they collided: Clinical ran
   straight through Attack. Keeping the render's two-column grouping and
   giving the note the line underneath holds both -- the layout the pack drew
   and the words this build says. */
.pcard[data-face="front"] { width: 250px; }
.pc-front { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.pc-front .irow {
  grid-template-columns: 20px minmax(0, 1fr);
  grid-template-areas: "icon label" "bar bar" "note note";
  gap: 2px 6px; align-items: center; margin-bottom: 6px;
}
.pc-front .irow-i { grid-area: icon; }
.pc-front .irow-i svg { width: 15px; height: 15px; }
.pc-front .irow-k { grid-area: label; font-size: 11px; font-weight: 650; color: var(--ink); }
.pc-front .irow-b { grid-area: bar; }
.pc-front .irow-n { grid-area: note; font-size: 10px; line-height: 13px; }
.pc-eyebrow { font-size: 9px; font-weight: 700; letter-spacing: 0.06em;
              text-transform: uppercase; color: var(--ink-muted); }
.pc-front .iconrow { width: 100%; }
.pc-flip {
  display: block; width: 100%; margin-top: 6px; padding: 3px 6px;
  font: 600 11px/16px system-ui, sans-serif; color: var(--ink-muted);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-md); cursor: pointer;
}
.pc-flip:hover { color: var(--ink); }

/* ── the press conference, built to confirmation-scene.png ───────────────
   Desktop: the two cards float over the room, the checklist in a rail.
   Phone: everything stacks, which is what confirmation-phone.png does. */
.ta-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; }
@media (min-width: 900px) {
  /* ⚠ THE ROOM GOES RIGHT AND THE CHECKLIST LEFT, the same way round as
     Review. It was the only screen left with the photograph on the left and
     the reading matter on the right, which is the arrangement Amr asked to
     be flipped -- and it stopped 508px short of the edge, the widest miss on
     any screen. `order` rather than re-writing announced.js: the DOM order
     is team news, bench, then what to do on FPL, and that is the order it
     should be read in on a phone, where there is one column. */
  .ta-grid { grid-template-columns: 460px minmax(0, 1fr); align-items: start; }
  .ta-grid > .scene-band { order: 2; }
  .ta-grid > :not(.scene-band) { order: 1; }
}
.ta-room { position: relative; z-index: 1; display: flex; gap: 12px;
           flex-wrap: wrap; align-items: flex-start; min-height: 260px; }
.ta-room > .card { flex: 1 1 280px; min-width: 0; }
.ta-h { font-size: 12px; font-weight: 700; letter-spacing: 0.05em;
        text-transform: uppercase; color: var(--turf); margin-bottom: 4px; }
.ta-row { display: flex; align-items: baseline; gap: 10px; padding: 6px 0;
          border-top: 1px solid var(--line); }
.ta-h + .ta-row { border-top: 0; }
.ta-k { flex: none; width: 34px; font-family: var(--font-mono); font-size: 11px;
        color: var(--ink-muted); }
.ta-v { flex: 1 1 auto; min-width: 0; font-weight: 700; }
.ta-r { flex: none; font-family: var(--font-mono); font-size: 11px; color: var(--ink-muted); }
.ta-sum .ta-v { font-weight: 400; color: var(--ink-muted); }
.amberink { color: var(--amber); font-weight: 700; }
.ta-step { display: flex; gap: 12px; padding: 10px 0; border-top: 1px solid var(--line); }
.ta-n { flex: none; width: 26px; height: 26px; border-radius: 50%;
        background: var(--turf-soft); color: var(--turf);
        display: flex; align-items: center; justify-content: center;
        font-weight: 700; font-size: 13px; }
.ta-sb { min-width: 0; }
.ta-st { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ta-go { display: block; text-align: center; margin: 12px 0 8px; }

/* ── joining the club, built to onboarding.png ───────────────────────────
   Six steps down the side at desktop, stacked above the step on a phone. */
.ob-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 20px; }
@media (min-width: 900px) {
  .ob-grid { grid-template-columns: 280px minmax(0, 1fr); align-items: start; }
}
.ob-rail .display { margin: 0 0 2px; font-size: 26px; }
.ob-steps { list-style: none; margin: 14px 0 0; padding: 0; }
.ob-step a { display: flex; gap: 10px; align-items: flex-start; padding: 8px 10px;
             border-radius: var(--radius-md); text-decoration: none; color: inherit; }
.ob-step.on a { background: var(--turf-soft); }
.ob-step a:hover { background: var(--surface); }
.ob-n { flex: none; width: 24px; height: 24px; border-radius: 50%;
        border: 1px solid var(--line); display: flex; align-items: center;
        justify-content: center; font-size: 12px; font-weight: 700;
        color: var(--ink-muted); background: var(--surface-raised); }
.ob-step.on .ob-n { background: var(--turf); border-color: var(--turf); color: var(--on-turf); }
.ob-t { display: block; font-weight: 700; font-size: 14px; }
.ob-s { display: block; font-size: 12px; color: var(--ink-muted); }
.ob-not { margin-top: 16px; }
.ob-main { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.ob-main .display { margin: 0; }
/* The scene is the step's own room; the bubble sits inside it on a plate. */
/* ⚠ center top, NOT center. The band's default crop takes the middle of the
   image, which on the signing photo is a patch of shirt fabric: the crest and
   the name the render leads with are both above it. These six are portrait
   scenes with their subject at the top. */
.ob-band { min-height: 320px; display: flex; align-items: flex-end; }
.ob-band::before { background-position: center top; }
.ob-room { position: relative; z-index: 1; width: 100%; }
.ob-say { display: flex; gap: 12px; align-items: flex-end; }
.ob-say .face { border-radius: 50%; object-fit: cover; flex: none;
                width: 64px; height: 64px; }
.ob-bub { background: var(--surface-raised); border: 1px solid var(--line);
          border-radius: var(--radius-lg); padding: 12px 14px; min-width: 0; }
.ob-bub .body { margin: 4px 0 0; }
.ob-who { font-size: 13px; }
.ob-thumbs { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
             gap: 10px; margin-top: 8px; }
.ob-thumb { display: block; padding: 28px 8px 8px; text-align: center;
            background: var(--surface); border: 1px solid var(--line);
            border-radius: var(--radius-md); text-decoration: none;
            font-size: 13px; font-weight: 650; color: var(--ink); }
.ob-thumb:hover { border-color: var(--turf); }
.ob-nav { display: flex; gap: 10px; align-items: center; justify-content: space-between; }

/* Onboarding step 1: the sign-on field. Live; its button is not. */
.ob-lab { display: block; margin-top: 10px; font-size: 13px; font-weight: 700; }
.ob-id { display: block; width: 100%; max-width: 320px; margin-top: 4px;
         height: 44px; padding: 0 12px; font-family: var(--font-mono);
         font-size: 16px; border: 1px solid var(--line);
         border-radius: var(--radius-md); background: var(--surface-raised); }
.ob-id[aria-invalid="true"] { border-color: var(--danger); }
.ob-go { display: block; margin-top: 10px; }
.ob-go[disabled] { opacity: 0.55; cursor: not-allowed; }

/* Onboarding step 1: the sign-on field. Live; its button is not. */
.ob-lab { display: block; margin-top: 10px; font-size: 13px; font-weight: 700; }
.ob-id { display: block; width: 100%; max-width: 320px; margin-top: 4px;
         height: 44px; padding: 0 12px; font-family: var(--font-mono);
         font-size: 16px; border: 1px solid var(--line);
         border-radius: var(--radius-md); background: var(--surface-raised); }
.ob-id[aria-invalid="true"] { border-color: var(--danger); }
.ob-go { display: block; margin-top: 10px; }
.ob-go[disabled] { opacity: 0.55; cursor: not-allowed; }

/* The club strip inside the roundel. Cropped to the torso so the sleeves do
   not shrink the pattern, and under the code pill, which keeps its contrast. */
.club-kit { position: absolute; inset: 0; width: 100%; height: 100%;
            object-fit: cover; object-position: 50% 32%; border-radius: 50%; }
.club-circle { position: relative; overflow: hidden; }
.club-pill { position: relative; z-index: 1; }

/* ══ the line-up board ── a tablet lying flat under the lockers ═══════════
   ⚠ THE DESIGN FOLDER HAS NO TABLET, so this is drawn rather than placed --
   assets/ ships clipboard-blank.png and jersey-blank.png and nothing else.
   Amr's rule is to use the pack's elements where they exist and otherwise
   stick to what is already here, so the bezel is this stylesheet's own and
   the grass is the pitch this app already uses on Review. */
.lk-section { position: relative; }
.lu-slot { position: sticky; bottom: 10px; z-index: 40; margin-top: 12px; }
.lu-pad {
  background: var(--device); border-radius: 16px; padding: 9px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.34);
}
.lu-screen {
  display: grid; grid-template-columns: minmax(0, 1fr) 232px;
  border-radius: 9px; overflow: hidden; background: var(--surface-raised);
}
.lu-pitch {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 2px; padding: 8px 6px; min-height: 150px;
  background-image: url("../img/scenes/review.jpg");
  background-size: cover; background-position: center 30%;
}
.lu-pitch.lu-broken { display: block; background: var(--chip); padding: 12px; }
.lu-col { display: flex; flex-direction: column; justify-content: space-evenly;
          align-items: center; gap: 4px; }
.lu-man { display: flex; flex-direction: column; align-items: center; gap: 2px;
          max-width: 100%; }
.lu-man .club-circle { width: 26px; height: 26px; }
.lu-name {
  font-size: 10px; line-height: 12px; font-weight: 700; color: var(--on-turf);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.85);
}
/* the bay you have open, marked on the board -- this is the whole point of it
   updating as you move from locker to locker */
.lu-man.on .club-circle { box-shadow: 0 0 0 3px var(--turf),
                                    0 0 0 5px var(--surface-raised); }
.lu-man.on .lu-name { color: var(--on-turf); background: var(--turf);
                      border-radius: 3px; padding: 0 4px; }
.lu-side { padding: 8px 10px; display: flex; flex-direction: column; gap: 6px;
           border-left: 1px solid var(--line); min-width: 0; }
.lu-head { display: flex; align-items: baseline; gap: 8px; }
.lu-gw { font-weight: 800; font-size: 15px; }
.lu-shape { font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted); }
.lu-k { font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase;
        color: var(--ink-muted); }
.lu-sub { display: flex; align-items: baseline; gap: 6px; font-size: 12px;
          line-height: 16px; }
.lu-sub.on { background: var(--turf-soft); border-radius: 3px; }
/* ⚠ NOT `.lu-slot`: that is the sticky wrapper, and a bench number carrying
   `position: sticky; bottom: 10px` would have crawled out of its own row. */
.lu-n { font-family: var(--font-mono); font-size: 11px;
        color: var(--ink-muted); min-width: 14px; }
.lu-subname { font-weight: 650; overflow: hidden; text-overflow: ellipsis;
              white-space: nowrap; }
.lu-guess { color: var(--amber); font-weight: 800; cursor: help; }
.lu-foot { margin-top: auto; }
.lu-row { display: flex; justify-content: space-between; gap: 8px;
          font-size: 12px; line-height: 17px; }
/* ⚠ THE QUALIFIER TRAVELS WITH THE NAME, AND "travels with" HAS TO MEAN
   UNBREAKABLY. Left to wrap, the captain's "last week" came off the end of
   its own row and landed beside the vice's name on the line below -- so the
   board read as though Gakpo were last week's and Haaland were this week's,
   which is the exact claim the qualifier exists to prevent. The name and the
   tag are one inline-flex run that cannot break, and each row is an atomic
   chunk that wraps whole or not at all. */
.lu-row .lu-v {
  font-weight: 700; display: inline-flex; align-items: baseline;
  gap: 5px; white-space: nowrap;
}
.lu-when {
  display: inline-block; padding: 1px 4px; flex: none;
  border-radius: 3px; background: var(--amber-soft); color: var(--amber);
  font-size: 9px; font-weight: 700; letter-spacing: 0.02em;
  text-transform: uppercase; vertical-align: 1px;
}
.lu-note { font-size: 10px; line-height: 13px; color: var(--ink-muted); margin: 4px 0 0; }

@media (max-width: 759px) {
  /* clear of the bottom tab bar, which is fixed furniture 72px tall */
  .lu-slot { bottom: 78px; }
  .lu-screen { grid-template-columns: minmax(0, 1fr); }
  /* ⚠ THE SIDE PANEL GOES TWO-UP ON A PHONE. Stacked under the pitch at its
     desktop sizes the board came to 450px of an 844px screen and covered the
     locker it is meant to summarise -- a sticky panel has to leave the thing
     it is stuck to visible. Bench on the left, armband and chip on the right. */
  /* ⚠ SMALL ENOUGH NOT TO COVER THE LOCKER IT SUMMARISES. Pinned 78px above
     the tab bar at its two-column size the board's top sat at 423px, and the
     shirt's figure -- the one number the screen exists to show -- ran 500 to
     560. It was under the board at the top of the page, which is the first
     thing anyone sees. Everything here is set to get the board under 210px
     so that stops being true; measured, not estimated. */
  .lu-side {
    border-left: 0; border-top: 1px solid var(--line);
    display: flex; flex-direction: column; gap: 2px; padding: 5px 8px;
  }
  .lu-head { gap: 6px; }
  /* bench and armband run as wrapped lines rather than two stacked lists */
  .lu-subs, .lu-foot { display: flex; flex-wrap: wrap; align-items: baseline;
                       gap: 0 9px; margin-top: 0; }
  /* each key-and-value pair wraps as one; half a pair on the end of a line is
     how the captain's qualifier ended up reading as the vice's */
  .lu-sub, .lu-row { flex: 0 0 auto; white-space: nowrap; }
  .lu-sub, .lu-row { font-size: 11px; line-height: 14px; gap: 4px; }
  .lu-row { justify-content: flex-start; }
  .lu-gw { font-size: 13px; }
  /* ⚠ THE LAST-TIME-OUT NOTE COMES OFF THE PHONE BOARD, and this is a size
     decision rather than an editorial one. The board has to stay under about
     190px or it covers the figure on the open shirt at the top of the page --
     measured, not guessed. What the note adds is WHO had the armband last
     week; what it does not carry is the claim, which is "not chosen yet" and
     stays on the board at every width. The names are still on the page, in
     the captaincy section below the lockers. */
  .lu-note { display: none; }
  /* ⚠ THE PITCH TURNS THROUGH NINETY DEGREES ON A PHONE, which is where the
     height comes from. Four columns means the midfield's five stack
     vertically and the pitch is five rows tall whatever the min-height says;
     four ROWS puts five across the 358px width instead and the pitch is four
     rows tall. It is also the way round every phone football app draws it. */
  .lu-pitch {
    grid-template-columns: minmax(0, 1fr); gap: 0;
    min-height: 0; padding: 3px;
  }
  .lu-col { flex-direction: row; justify-content: space-evenly; gap: 2px; }
  .lu-man { flex: 0 1 auto; min-width: 0; gap: 0; }
  .lu-man .club-circle { width: 20px; height: 20px; }
  .lu-man .club-circle { width: 22px; height: 22px; }
  .lu-name { font-size: 9px; line-height: 11px; }
}

/* ══ the kit picker, on onboarding step 5 ════════════════════════ */
.kit-row { display: flex; flex-wrap: wrap; gap: 10px; margin: 10px 0 8px; }
.kit-dot {
  width: 40px; height: 40px; border-radius: 50%; padding: 0;
  border: 2px solid var(--line); cursor: pointer;
  /* ⚠ THE SWATCH IS THE COLOUR AS THE SHIRT WILL WEAR IT, which is the point
     of showing one. The jersey is white photography under `multiply`, and
     multiplying white by the colour gives the colour -- so a flat fill of the
     same token is what the brightest part of the cloth actually becomes. */
}
.kit-dot:hover { border-color: var(--ink-muted); }
.kit-dot.on {
  border-color: var(--ink);
  box-shadow: 0 0 0 3px var(--surface-raised), 0 0 0 5px var(--ink);
}
.kit-dot:focus-visible { outline: 3px solid var(--turf); outline-offset: 3px; }
.ob-kit .note-sm { margin-top: 6px; }

/* ══ Review's gameweek card ── answer, then standing, then breakdown ═════ */
.gw-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 16px; padding-bottom: 12px; margin-bottom: 4px;
  border-bottom: 1px solid var(--line);
}
.gw-score { display: flex; align-items: baseline; gap: 7px; }
/* the figure a manager came for, at the size that says so */
.gw-pts { font-size: 46px; line-height: 48px; font-weight: 800;
          font-variant-numeric: tabular-nums; }
.gw-unit { font-size: 15px; color: var(--ink-muted); }
/* ⚠ THE RANK KEEPS ITS WIDTH. Left to share, a chip week's explanation
   pushed "7,888,024" off the card's right edge -- a seven-figure number
   truncated to "7,888,0" is worse than no number. The headline side shrinks
   and wraps; this side does not. */
/* the right-hand block, whatever it holds: rank on Fielded, what you really
   scored on the two counterfactuals, the ordering score on Analyst */
.gw-side, .gw-rank { text-align: right; flex: none; }
.gw-head > div:first-child { min-width: 0; flex: 1 1 auto; }
.gw-rank-v { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums;
             white-space: nowrap; }
.gw-move { font-size: 14px; font-weight: 700; white-space: nowrap; }
/* ⚠ UP IS GREEN WHATEVER THE ARITHMETIC DID. A rank improving means the
   number went DOWN, so the colour follows the meaning and not the sign. */
.gw-move.up { color: var(--turf); }
.gw-move.down { color: var(--danger); }
.gw-move.flat { color: var(--ink-muted); }
/* a side row that is a sentence rather than a figure: same column, no caps,
   and allowed to wrap because a verdict is longer than a rank */
.gw-move.note { font-size: 12px; font-weight: 500; line-height: 16px;
                color: var(--ink-muted); white-space: normal; }
.gw-wk { margin-top: 2px; white-space: nowrap; }
.gw-chip-line { margin-top: 8px; display: flex; align-items: baseline; gap: 7px;
                flex-wrap: wrap; }

/* ══ the pitch wears the pack's jersey ═══════════════════════════
   ⚠ THE PHOTOGRAPH, NOT THE DRAWN SILHOUETTE. Amr, 2026-10-07: use our blank
   jersey on this page. The stroked path was built when there was no asset;
   design/assets ships jersey-blank.png and the locker has been wearing it
   since the changing room was rebuilt, so the pitch was the last place still
   drawing its own.

   ⚠ AND IT TAKES THE CHOSEN COLOUR WITH IT. The tint is masked to the cloth
   exactly as the locker's is -- `background-color` with `blend-mode` fills
   the whole box and turns the jersey's transparent surround into a
   rectangle, which is a fault this stylesheet has already had once. */
/* ⚠ THE BENCH WEARS THE SAME GARMENT. Amr, 2026-10-07: "use our jerseys
   for the subs too". The four bench shirts were still the stroked
   silhouette the pitch stopped using when the photograph arrived, so one
   card showed two different kits for one squad. Everything below is written
   once for both; only the sizes differ, and they differ because the
   garments are 66px and 64px wide. */
.pitch-slot .shirt-u .shirt-shape,
.bench-row .shirt-u .shirt-shape { display: none; }
/* ⚠ THE LETTERING IS PINNED TO THE GARMENT, NOT LAID OUT ABOVE IT, and
   that is a correctness fix as much as Amr's placement one. The name sat
   below `padding: 27% ...` on this box -- and a PERCENTAGE PADDING RESOLVES
   AGAINST THE CONTAINING BLOCK'S WIDTH, which here is `.shirt-u`, which is
   the SLOT. The slot is 86px wide with five in a row and 150px with one, so
   the same declaration put the name at 26.5% of the shirt on a midfielder
   and 46.3% on the keeper. A twenty-point swing, on every render, and both
   look like a shirt. DE-085's mechanism again: one side of the relation
   written down, the other decided elsewhere.

   Taken out of the flow and given `top` in percent, the reference is this
   box's own height -- and this box is the photograph, at the photograph's
   own ratio. 21% is the roundel's bottom edge, which is where Amr asked for
   the name: where it sits on the back of a real shirt. */
.pitch-slot .shirt-u .shirt-body,
.bench-row .shirt-u .shirt-body {
  aspect-ratio: 364 / 483; width: 100%; padding: 0;
  background-image: url("../img/kit/jersey-cloth.png");
  background-size: 100% 100%; background-repeat: no-repeat;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.22));
}
.pitch-slot .shirt-u .shirt-body::after,
.bench-row .shirt-u .shirt-body::after {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: var(--kit-tint); mix-blend-mode: multiply;
  -webkit-mask-image: url("../img/kit/jersey-cloth.png");
          mask-image: url("../img/kit/jersey-cloth.png");
  -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
/* ⚠ 14% AND 10% ARE THE TORSO'S EDGES IN THE ASSET, not a margin. The
   jersey's body runs between them; outside is sleeve, and lettering that
   crosses the seam reads as a misprint. */
.pitch-slot .shirt-u .shirt-name, .pitch-slot .shirt-u .shirt-num,
.bench-row .shirt-u .shirt-name, .bench-row .shirt-u .shirt-num {
  position: absolute; z-index: 1; color: var(--kit-ink);
  width: auto; margin: 0; padding: 0;
  overflow: hidden; text-overflow: clip; white-space: nowrap;
  text-align: center;
}
/* ⚠ THE ROUNDEL'S BOTTOM IS A FIXED 18px AND THE SHIRT IS NOT, so "just
   below the roundel" is a different percentage at each width. The jersey is
   88px tall at desktop and 62px on a phone, so the same -18px overlap puts
   the disc's edge at 20.5% of one and 29% of the other -- and a name pinned
   at 21% sat BEHIND the disc on the phone, which is how RAYA, MITCHELL and
   CALAFIORI shipped with half a name each. One number cannot serve two
   garments; check_layout.js measures the clearance at both widths. */
.pitch-slot .shirt-u .shirt-name { left: 13%; right: 13%; top: 27.5%; }
.pitch-slot .shirt-u .shirt-num  { left: 9%; right: 9%; top: 43%; }
@media (min-width: 760px) {
  .pitch-slot .shirt-u .shirt-name { top: 23%; }
  .pitch-slot .shirt-u .shirt-num  { top: 35%; }
}
/* ⚠ THE BENCH'S NUMBERS, WORKED THE SAME WAY AND NOT COPIED. The garment is
   64px wide, so 85px tall; the disc is 38px, which is 59% of the width and
   inside the 55-60% Amr set for the phone; the overlap is 55% of the disc,
   21px, which puts its bottom edge at 25% of the shirt. The lettering
   starts below that. Every one of those four numbers is the pitch's rule
   applied to a different-sized garment, which is why none of them is the
   pitch's number. */
.bench-row .shirt-u .shirt-club { margin-bottom: -21px; }
.bench-row .shirt-u .shirt-name { left: 13%; right: 13%; top: 27%;
                                  font-size: 8px; line-height: 10px;
                                  letter-spacing: 0; font-weight: 700; }
.bench-row .shirt-u .shirt-num  { left: 9%; right: 9%; top: 41%;
                                  font-size: 24px; line-height: 26px; }
.gw-vs { font-size: 14px; font-weight: 700; margin-top: 1px; }
.gw-vs.up { color: var(--turf); }
.gw-vs.down { color: var(--danger); }
.gw-vs.flat { color: var(--ink-muted); }

/* >>> club roundels: generated by tools/gen_club_css.py, do not edit <<< */
/* Source: tools/club_colours.json, from the Gaffa FC design pack.
   Hand-written by design, not from the feed; re-checked each August.
   Regenerate with: python3 tools/gen_club_css.py */
:root {
  --club-ars: #db0007; --club-ars-2: #ffffff; --club-ars-ink: #ffffff;
  --club-avl: #7a1e3c; --club-avl-2: #95bfe5; --club-avl-ink: #ffffff;
  --club-bha: #0057b8; --club-bha-2: #ffffff; --club-bha-ink: #ffffff;
  --club-bou: #da291c; --club-bou-2: #111111; --club-bou-ink: #ffffff;
  --club-bre: #e30613; --club-bre-2: #ffffff; --club-bre-ink: #ffffff;
  --club-che: #034694; --club-che-2: #ffffff; --club-che-ink: #ffffff;
  --club-cov: #6cc3ec; --club-cov-2: #ffffff; --club-cov-ink: #111111;
  --club-cry: #c4122e; --club-cry-2: #1b458f; --club-cry-ink: #ffffff;
  --club-eve: #003399; --club-eve-2: #ffffff; --club-eve-ink: #ffffff;
  --club-ful: #ffffff; --club-ful-2: #111111; --club-ful-ink: #111111;
  --club-hul: #f5a12d; --club-hul-2: #111111; --club-hul-ink: #111111;
  --club-ips: #0044a9; --club-ips-2: #ffffff; --club-ips-ink: #ffffff;
  --club-lee: #ffffff; --club-lee-2: #1d428a; --club-lee-ink: #111111;
  --club-liv: #c8102e; --club-liv-2: #ffffff; --club-liv-ink: #ffffff;
  --club-mci: #6cabdd; --club-mci-2: #ffffff; --club-mci-ink: #111111;
  --club-mun: #da020e; --club-mun-2: #ffffff; --club-mun-ink: #ffffff;
  --club-new: #111111; --club-new-2: #ffffff; --club-new-ink: #ffffff;
  --club-nfo: #dd0000; --club-nfo-2: #ffffff; --club-nfo-ink: #ffffff;
  --club-sun: #eb172b; --club-sun-2: #ffffff; --club-sun-ink: #111111;
  --club-tot: #ffffff; --club-tot-2: #132257; --club-tot-ink: #111111;
}
/* fill and ink, per club, from the pack */
.club-circle[data-club="ARS"] { background: var(--club-ars); color: var(--club-ars-ink); }
.club-circle[data-club="AVL"] { background: var(--club-avl); color: var(--club-avl-ink); }
.club-circle[data-club="BHA"] { background: var(--club-bha); color: var(--club-bha-ink); }
.club-circle[data-club="BOU"] { background: var(--club-bou); color: var(--club-bou-ink); }
.club-circle[data-club="BRE"] { background: var(--club-bre); color: var(--club-bre-ink); }
.club-circle[data-club="CHE"] { background: var(--club-che); color: var(--club-che-ink); }
.club-circle[data-club="COV"] { background: var(--club-cov); color: var(--club-cov-ink); }
.club-circle[data-club="CRY"] { background: var(--club-cry); color: var(--club-cry-ink); }
.club-circle[data-club="EVE"] { background: var(--club-eve); color: var(--club-eve-ink); }
.club-circle[data-club="FUL"] { background: var(--club-ful); color: var(--club-ful-ink); }
.club-circle[data-club="HUL"] { background: var(--club-hul); color: var(--club-hul-ink); }
.club-circle[data-club="IPS"] { background: var(--club-ips); color: var(--club-ips-ink); }
.club-circle[data-club="LEE"] { background: var(--club-lee); color: var(--club-lee-ink); }
.club-circle[data-club="LIV"] { background: var(--club-liv); color: var(--club-liv-ink); }
.club-circle[data-club="MCI"] { background: var(--club-mci); color: var(--club-mci-ink); }
.club-circle[data-club="MUN"] { background: var(--club-mun); color: var(--club-mun-ink); }
.club-circle[data-club="NEW"] { background: var(--club-new); color: var(--club-new-ink); }
.club-circle[data-club="NFO"] { background: var(--club-nfo); color: var(--club-nfo-ink); }
.club-circle[data-club="SUN"] { background: var(--club-sun); color: var(--club-sun-ink); }
.club-circle[data-club="TOT"] { background: var(--club-tot); color: var(--club-tot-ink); }
/* ⚠ VERTICAL STRIPES, AND THE CODE GETS A PILL BACK. There is no single
   background to contrast against, so the pack prints the three letters on
   a white label over the stripes. Plain discs do not need one. */
.club-circle[data-club="BHA"] {
  background: repeating-linear-gradient(90deg,
    var(--club-bha) 0 7.5%, var(--club-bha-2) 7.5% 15%);
}
.club-circle[data-club="BOU"] {
  background: repeating-linear-gradient(90deg,
    var(--club-bou) 0 7.5%, var(--club-bou-2) 7.5% 15%);
}
.club-circle[data-club="BRE"] {
  background: repeating-linear-gradient(90deg,
    var(--club-bre) 0 7.5%, var(--club-bre-2) 7.5% 15%);
}
.club-circle[data-club="CRY"] {
  background: repeating-linear-gradient(90deg,
    var(--club-cry) 0 7.5%, var(--club-cry-2) 7.5% 15%);
}
.club-circle[data-club="HUL"] {
  background: repeating-linear-gradient(90deg,
    var(--club-hul) 0 7.5%, var(--club-hul-2) 7.5% 15%);
}
.club-circle[data-club="NEW"] {
  background: repeating-linear-gradient(90deg,
    var(--club-new) 0 7.5%, var(--club-new-2) 7.5% 15%);
}
.club-circle[data-club="SUN"] {
  background: repeating-linear-gradient(90deg,
    var(--club-sun) 0 7.5%, var(--club-sun-2) 7.5% 15%);
}
/* a ring for the fills the page cannot be told from */
.club-circle[data-club="FUL"], .club-circle[data-club="LEE"], .club-circle[data-club="TOT"] {
  border-color: var(--line);
}
/* the same kits, as a bar across the top of a squad card */
.club-bar[data-club="ARS"] {
  background: var(--club-ars);
}
.club-bar[data-club="AVL"] {
  background: var(--club-avl);
}
.club-bar[data-club="BHA"] {
  background: repeating-linear-gradient(90deg,
    var(--club-bha) 0 10px, var(--club-bha-2) 10px 20px);
}
.club-bar[data-club="BOU"] {
  background: repeating-linear-gradient(90deg,
    var(--club-bou) 0 10px, var(--club-bou-2) 10px 20px);
}
.club-bar[data-club="BRE"] {
  background: repeating-linear-gradient(90deg,
    var(--club-bre) 0 10px, var(--club-bre-2) 10px 20px);
}
.club-bar[data-club="CHE"] {
  background: var(--club-che);
}
.club-bar[data-club="COV"] {
  background: var(--club-cov);
}
.club-bar[data-club="CRY"] {
  background: repeating-linear-gradient(90deg,
    var(--club-cry) 0 10px, var(--club-cry-2) 10px 20px);
}
.club-bar[data-club="EVE"] {
  background: var(--club-eve);
}
.club-bar[data-club="FUL"] {
  background: var(--club-ful);
}
.club-bar[data-club="HUL"] {
  background: repeating-linear-gradient(90deg,
    var(--club-hul) 0 10px, var(--club-hul-2) 10px 20px);
}
.club-bar[data-club="IPS"] {
  background: var(--club-ips);
}
.club-bar[data-club="LEE"] {
  background: var(--club-lee);
}
.club-bar[data-club="LIV"] {
  background: var(--club-liv);
}
.club-bar[data-club="MCI"] {
  background: var(--club-mci);
}
.club-bar[data-club="MUN"] {
  background: var(--club-mun);
}
.club-bar[data-club="NEW"] {
  background: repeating-linear-gradient(90deg,
    var(--club-new) 0 10px, var(--club-new-2) 10px 20px);
}
.club-bar[data-club="NFO"] {
  background: var(--club-nfo);
}
.club-bar[data-club="SUN"] {
  background: repeating-linear-gradient(90deg,
    var(--club-sun) 0 10px, var(--club-sun-2) 10px 20px);
}
.club-bar[data-club="TOT"] {
  background: var(--club-tot);
}
.club-bar[data-club="FUL"], .club-bar[data-club="LEE"], .club-bar[data-club="TOT"] {
  box-shadow: inset 0 -1px 0 var(--line);
}
.club-circle[data-club="BHA"] .club-pill, .club-circle[data-club="BOU"] .club-pill, .club-circle[data-club="BRE"] .club-pill, .club-circle[data-club="CRY"] .club-pill, .club-circle[data-club="HUL"] .club-pill, .club-circle[data-club="NEW"] .club-pill, .club-circle[data-club="SUN"] .club-pill {
  background: var(--surface-raised); color: var(--ink);
  border-radius: 2px; padding: 0 2px;
  font-size: 8px; line-height: 11px;
}
/* <<< club roundels: end generated >>> */

/* ══ the pitch jersey, read as a jersey ═══════════════════════════
   Amr, 2026-10-07: the figure overflowed the shirt, the name belongs across
   the shoulders the way a real one does, and the captain wears an armband.

   ⚠ NO LICENSED FACE. The shirt lettering is the same system stack the rest
   of the app uses, condensed by letter-spacing rather than by loading a font
   nobody has a licence for. */
@media (min-width: 760px) {
  /* ⚠ THE LONGEST SURNAME IN THE SQUAD IS THE CONSTRAINT, not the average
     one. CALAFIORI is nine characters; at 8px over a 46px torso it was cut to
     CALAF, which is what a clipped name looks like and not what a jersey
     looks like. The torso is the middle 72% of the asset, so the type is
     sized to put nine characters inside it. */
  /* ⚠ THE TORSO IS 72% OF THE SHIRT, NOT 72% OF WHAT IS LEFT AFTER PADDING.
     `.shirt-name { width: 72% }` measures its own content box, and the body
     carries 12px of horizontal padding -- so the name got 72% of 52px rather
     than of 76, which is 38px, and nine characters cannot go in 38px at any
     size worth reading. Taken out of the padded flow and pinned to the
     asset's own torso edges, it gets the 55px the jersey actually has. */
  .pitch-slot .shirt-u .shirt-name {
    font-size: 8px; line-height: 10px; letter-spacing: 0;
    font-weight: 700;
  }
  /* ⚠ THE FIGURE IS CLAMPED TO THE TORSO, which is what it was not. A
     two-digit score at 20px in a 64px shirt ran over both side seams; the
     torso is the middle 72% of the asset, so the type has to fit 46px. */
  .pitch-slot .shirt-u .shirt-num {
    font-size: 22px; line-height: 24px; letter-spacing: -0.02em;
  }
}
/* ══ the captain's armband ═══════════════════════════════════════
   ⚠ EVERY NUMBER HERE IS MEASURED OFF jersey-cloth.png's ALPHA CHANNEL, and
   that is the only reason percentages mean anything. .shirt-body carries the
   photograph at `background-size: 100% 100%` over a box pinned to the
   asset's own 364:483, so 1% of this box is 1% of the image -- and the
   sleeve is a real place in it rather than a guess. The first version of
   this band was a guess (left 3%, top 30%) and landed in open air beside
   the chest, where the gate reported it correctly as painted.

   The sleeve runs from a cuff at (1%, 41.5%) up to the shoulder at
   (17.9%, 30%), so a band across it is rotated 47.9deg -- the normal to
   that axis. The rectangle below is the largest one whose every interior
   point is opaque cloth on the sleeve side of the raglan seam; the search
   that found it, and the proof it is still on the sleeve, are in
   tests/test_armband_on_sleeve.py.

   ⚠ AND IT IS SCOPED TO THE PITCH, because the pitch is the only place a
   captain is drawn and the bench's shirt is a DIFFERENT GARMENT -- a stroked
   path with its own sleeve in its own coordinates. One set of percentages
   cannot sit on both. check_layout.js fails if an armband ever appears
   outside .pitch-slot rather than letting it render unplaced. */
.shirt-armband { display: none; }
/* ⚠ WIDER, WITH A C IN IT -- AND THIS IS THE BIGGEST THE SLEEVE WILL TAKE.
   Amr, 2026-10-07. The previous band was 52x22 in the asset's pixels, which
   is 9.4x3.9 on a 66px shirt: no letter goes in 3.9px. Growing it along the
   arm toward the shoulder is the only direction with room, because the
   sleeve is a wedge that widens that way; growing it toward the cuff put a
   corner off the cloth at every size tried, measured at alpha 0.

   57x41 is where it stops. Every point of it is opaque cloth (alpha 253 at
   worst) and its inner corner crosses the estimated raglan seam by 9px of
   the asset -- 1.6px on screen -- which is a band sitting at the top of the
   arm rather than on the back. tests/test_armband_on_sleeve.py allows that
   9px and nothing like the hundreds a band on the shoulder blade would
   need, so the test still fails for the thing it was written to catch. */
.pitch-slot .shirt-u .shirt-armband {
  display: flex; align-items: center; justify-content: center;
  position: absolute; z-index: 2;
  left: 5.60%; top: 32.50%; width: 15.60%; height: 8.50%;
  transform: rotate(47.9deg);
  background: var(--armband); border: 1px solid var(--ink); border-radius: 1px;
}
/* ⚠ THE LETTER IS TURNED BACK. The band follows the arm at 47.9 degrees,
   which is right for a band and wrong for a letter: a C on its side at six
   pixels is a shape, not a letter. */
.shirt-armband-c {
  transform: rotate(-47.9deg);
  font: 800 5px/1 var(--font-sans); color: var(--ink);
  letter-spacing: 0;
}
@media (min-width: 760px) { .shirt-armband-c { font-size: 6px; } }

/* ⚠ ROUNDELS UP 13%, ON AMR'S INSTRUCTION. 38 -> 43, and the pitch's own
   26 -> 29, so the club reads at a glance without the disc crowding the
   shoulders it sits on. */
.club-circle { width: 49px; height: 49px; }
.shirt-u.sm .club-circle { width: 38px; height: 38px; }
@media (min-width: 760px) {
  .pitch-slot .shirt-u .club-circle { width: 33px; height: 33px; }
}

/* provenance, one line, with the detail on hover rather than in a card */
.prov-mark { font-family: var(--font-mono); font-size: 11px;
             color: var(--ink-muted); margin: 14px 0 0; }
.prov-mark .prov-k { color: var(--ink-muted); opacity: 0.7; }
/* the left column holds its height as the arrows move through the season */
@media (min-width: 760px) {
  .rail .rail-top { min-height: 368px; }
  .rail .bench-card { min-height: 308px; }
}
/* ⚠ THE ANALYST'S PANEL IS A PLATE ON THE PITCH, NOT TEXT ON IT. It had no
   rule at all, so it sat in static flow under the panel's own ::before and
   ::after -- the photograph and its wash, both at z-index 0 and both
   covering it. The block measured 716x124 and was painted nowhere. Opaque
   and above them, like every other piece of text this site puts on a
   scene. */
.analyst-panel { display: flex; align-items: center; justify-content: center; }
.ap-in {
  position: relative; z-index: 2; max-width: 460px; margin: 0 20px;
  background: var(--surface-raised); border-radius: var(--radius-lg);
  padding: 20px 22px;
}
.ap-in .title { margin: 0 0 8px; }
.ap-in .note-sm { margin: 0; }

/* ══ the tooltip bubble ══════════════════════════════════════════
   One node on body, moved to whatever is hovered or focused. Fixed, so no
   `overflow: hidden` reaches it; see FPL.tips for why a ::after could not
   be used here. Pointer events are ON so the pointer can travel onto it. */
.tip {
  position: fixed; z-index: 90; left: 0; top: 0; max-width: 232px;
  background: var(--ink); color: var(--surface-raised);
  padding: 6px 9px; border-radius: 8px;
  font: 500 12px/16px var(--font-sans); text-align: left;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.26);
  opacity: 0; visibility: hidden; transform: translateY(3px);
  transition: opacity 90ms ease, transform 90ms ease, visibility 0s linear 90ms;
}
.tip.on { opacity: 1; visibility: visible; transform: none; transition-delay: 0s; }
/* ⚠ A TABLE NEEDS ROOM THAT A SENTENCE DOES NOT. FPL's breakdown runs to
   eight rows on a goalkeeper, and 232px of sentence width would wrap every
   label onto two lines. */
.tip.rich { max-width: 300px; padding: 9px 11px; }
.bd-h { display: flex; align-items: baseline; justify-content: space-between;
        gap: 14px; margin-bottom: 6px; }
.bd-who { font-weight: 700; }
.bd-tot { font-family: var(--font-mono); font-size: 13px; font-weight: 700; }
.bd-t { border-collapse: collapse; width: 100%; }
.bd-t td { padding: 2px 0; font-size: 12px; line-height: 16px; white-space: nowrap; }
.bd-v { font-family: var(--font-mono); text-align: right; padding-left: 14px !important;
        opacity: 0.68; }
.bd-p { font-family: var(--font-mono); text-align: right; padding-left: 12px !important;
        font-weight: 700; }
/* ⚠ THE TARGET SAYS IT IS ASKABLE. An icon that holds an explanation and
   looks like an icon that does not is the same problem as a link with no
   underline. */
[data-tip] { cursor: help; }
[data-tip]:focus-visible { outline: 2px solid var(--turf); outline-offset: 1px; border-radius: 3px; }

/* match events, under the shirt where the position label used to be */
.ev-row { display: inline-flex; align-items: center; gap: 4px; }
.ev { display: inline-flex; align-items: center; gap: 1px; line-height: 1; }
.ev-n { font-size: 9px; font-weight: 800; color: var(--ink); }
.cap-row { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap;
           justify-content: center; }
