  :root {
    /* Each palette colour is defined ONCE, as channels, with the hex form
       derived from it. Translucent uses then read rgba(var(--x-rgb), a)
       rather than restating the channels by hand.

       That hand-restating is what went wrong: the repalette moved the tokens
       and left nineteen rgba() literals sitting on the OLD values -- the
       results bars, the leaderboard's own-row highlight, the modal backdrop,
       the top-pick gradient. One of them, the stats chart's green, had been
       found and fixed on its own, which is how a repalette usually goes.
       There is now no second copy to miss. */
    --ink-rgb:    28, 28, 25;
    --ink:        rgb(var(--ink-rgb));
    --paper-rgb:  240, 238, 232;
    --paper:      rgb(var(--paper-rgb));
    --cream-rgb:  218, 220, 211;
    --cream:      rgb(var(--cream-rgb));
    /* Hover for anything resting on --cream: the same colour ~7% darker.
       Was hardcoded #dccfae in four places, picked against the old warm
       cream; against this one it read as a hue shift rather than a press --
       red went UP while blue fell 37. A token so the next repalette is one
       edit, not four. */
    --cream-hover: #cbcdc4;
    --gold-rgb:   173, 139, 61;
    --gold:       rgb(var(--gold-rgb));
    /* The lineup's highlighted row (.slot.active), by request, 26 September:
       a lighter gold than --gold, which stays the perfect pick's colour. */
    --gold-light: #C8BC99;
    /* Dark grey, solid: ink at 70% on the paper -- the Versus round and pick
       row's colour, and the Draft Board button's (26 September). */
    --gray-dark: #5C5B57;
    /* The trophy only. --gold is right for a tint behind text and too weak for
       a stroked icon sitting ON one of those tints.

       Deeper, not brighter -- brighter was the instinct and it is the wrong
       axis. The icon sits on a LIGHT background, so lifting it closes the gap:
       #d8a83a measured 1.02 against the green fill, which is invisible, and
       --gold itself only 1.50. This is 2.50 there and 3.88 on bare cream. */
    --gold-deep: #8a6410;
    --rust-rgb:   169, 76, 63;
    --rust:       rgb(var(--rust-rgb));
    --green-rgb:  78, 103, 80;
    --green:      rgb(var(--green-rgb));
    /* Hover for the green button: --green about 20% darker, which holds 6.0:1
       against the --cream type sitting on it. */
    --green-deep: #3e5340;
    --red-rgb:    184, 52, 28;
    --red:        rgb(var(--red-rgb));
    --muted-rgb:  116, 119, 111;
    --muted:      rgb(var(--muted-rgb));
    --rule:       #1f1d1a;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }

  /* The user agent hides [hidden] at zero specificity, so ANY author rule that
     declares a display outranks it -- which makes el.hidden = true silently
     inert on every class in this file that sets one. It is not a trap you
     notice: the attribute goes on, the DOM agrees the thing is hidden, and it
     is still on screen.

     It had already cost the modal backdrop (permanently over the page, eating
     every click) and the debug bar. It went on to cost the forgot-password link
     -- visible in signup mode -- and the new-password form, which stayed up
     after it was submitted. And it had the leaderboard's Show More the whole
     time: a dead button on every day with ten players or fewer.

     Those each carried their own copy of this rule. One at the top covers the
     class of bug instead, including for whatever declares a display next.
     scripts/check.js fails the build if this line goes missing. */
  [hidden] { display: none !important; }
  html, body { background: var(--paper); color: var(--ink); }
  body {
    font-family: 'Newsreader', Georgia, serif;
    min-height: 100vh;
    background-image:
      radial-gradient(circle at 12% 18%, rgba(0,0,0,0.04) 1px, transparent 1.5px),
      radial-gradient(circle at 78% 62%, rgba(0,0,0,0.03) 1px, transparent 1.5px),
      radial-gradient(circle at 40% 88%, rgba(0,0,0,0.03) 1px, transparent 1.5px);
    background-size: 220px 220px, 180px 180px, 260px 260px;
  }
  /* HEADER */
  header.masthead {
    border-bottom: 3px double var(--ink);
    background: var(--paper);
    position: sticky;
    top: 0;
    z-index: 40;
    padding: 0;
  }
  .masthead-inner {
    max-width: 1100px;
    margin: 0 auto;
    padding: 18px 32px 12px;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 18px;
    position: relative;
  }
  .masthead .brand-img {
    height: 40px;
    width: auto;
    display: block;
  }
  .brand-link {
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    display: block;
    transition: opacity 0.15s;
  }
  .brand-link:hover { opacity: 0.7; }

  /* Account entry point, pinned to the right of the masthead. Reads "Sign In"
     for a guest and "Sign Out" once they are signed in. With no border the
     text itself is the right edge, so the negative margin pulls it out to the
     page margin while the padding still gives it a comfortable tap target. */
  .header-account {
    margin-left: auto;
    margin-right: -14px;
    flex-shrink: 0;
    background: none;
    color: var(--ink);
    border: none;
    padding: 7px 14px;
    font-family: 'Anton', sans-serif;
    font-size: 15px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    cursor: pointer;
    white-space: nowrap;
    transition: color 0.12s;
  }
  .header-account:hover { color: var(--rust); }

  /* Hamburger menu button — aligned to the same left edge as main content */
  .menu-toggle {
    position: relative;
    width: 32px;
    height: 26px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    flex-shrink: 0;
  }
  .menu-toggle span {
    display: block;
    width: 100%;
    height: 4px;
    background: var(--ink);
    border-radius: 0;
  }
  .menu-toggle:hover span {
    background: var(--rust);
  }

  /* Menu overlay — full-screen ink-colored panel that slides in from the left */
  .menu-overlay {
    /* Every row in the menu hangs off this. It was written out as 28px in four
       rules and 20px in four more, and the legal links came in at 28 while the
       items around them were already at 20. */
    --menu-gutter: 28px;
    position: fixed;
    inset: 0;
    background: var(--ink);
    color: var(--paper);
    z-index: 100;
    transform: translateX(-100%);
    transition: transform 0.28s ease-out;
    overflow-y: auto;
    visibility: hidden;
  }
  .menu-overlay.active {
    transform: translateX(0);
    visibility: visible;
  }
  .menu-bar {
    display: flex;
    align-items: center;
    gap: 18px;
    padding: 18px 24px 14px;
    border-bottom: 1px solid rgba(255,255,255,0.08);
  }
  .menu-close {
    background: none;
    border: none;
    color: var(--paper);
    font-size: 32px;
    line-height: 1;
    cursor: pointer;
    padding: 0 6px;
    font-family: inherit;
  }
  .menu-close:hover { color: var(--rust); }
  .menu-brand {
    font-family: 'Anton', Impact, sans-serif;
    font-size: 22px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
  }
  .menu-brand .dot { color: var(--rust); }
  .menu-nav {
    padding: 14px 0 80px;
    max-width: 520px;
  }
  .menu-item {
    display: block;
    padding: 14px var(--menu-gutter);
    font-family: 'Newsreader', Georgia, serif;
    font-weight: 700;
    font-size: 21px;
    color: var(--paper);
    text-decoration: none;
    cursor: pointer;
    transition: color 0.12s, background 0.12s;
  }
  .menu-item:hover {
    color: var(--rust);
    background: rgba(255,255,255,0.04);
  }
  .menu-section-head {
    padding: 18px var(--menu-gutter) 8px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 11px;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: rgba(var(--paper-rgb), 0.45);
  }
  .menu-divider {
    height: 1px;
    background: rgba(255,255,255,0.08);
    margin: 8px 28px;
  }
  /* The "Ace" section gets a subtle card treatment */
  .menu-group-pro {
    margin: 0 16px;
    padding: 4px 0 8px;
    border: 1px solid rgba(255,255,255,0.12);
    border-radius: 4px;
    background: rgba(255,255,255,0.025);
  }
  .menu-group-pro .menu-section-head {
    padding-top: 14px;
    padding-left: 16px;
    color: var(--paper);
  }
  .menu-group-pro .menu-item {
    padding-left: 16px;
    padding-right: 16px;
  }

  /* Not built yet. Greyed rather than hidden, because the point of leaving them
     on the menu is to say what is coming -- and greyed rather than disabled
     because an <a> has no disabled state, so the router refuses the click and
     the cursor says so first. */
  .menu-item.is-soon {
    color: rgba(var(--paper-rgb), 0.38);
    cursor: default;
  }
  .menu-item.is-soon:hover {
    color: rgba(var(--paper-rgb), 0.38);
    background: none;
  }
  /* !important because .menu-cta sets its own colour the same way. */
  .menu-item.menu-cta.is-soon,
  .menu-item.menu-cta.is-soon:hover {
    color: rgba(var(--paper-rgb), 0.38) !important;
    background: none !important;
  }
  .menu-soon {
    margin-left: 10px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 10px;
    font-weight: 400;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: rgba(var(--paper-rgb), 0.32);
    white-space: nowrap;
  }

  /* Where the landing page's Privacy / Terms went. */
  /* One per line, on the same gutter every menu item sits on, so they read as
     the last two rows of the menu rather than as a centred footer. */
  .menu-legal {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
    padding: 14px var(--menu-gutter) 24px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 11px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(var(--paper-rgb), 0.35);
  }
  .menu-legal a { color: rgba(var(--paper-rgb), 0.55); text-decoration: none; }
  .menu-legal a:hover { color: var(--paper); text-decoration: underline; }
  .menu-cta {
    color: var(--rust) !important;
  }
  .menu-cta:hover {
    background: rgba(var(--rust-rgb), 0.12) !important;
  }
  .masthead .tag {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.18em;
    color: var(--muted);
  }
  /* LAYOUT */
  main {
    max-width: 1100px;
    margin: 0 auto;
    padding: 28px 32px 80px;
  }
  .screen { display: none; }
  .screen.active { display: block; animation: fadein 0.35s ease; }

  /* HOME LANDING */
  #home.active {
    min-height: calc(100vh - 80px);
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding-top: max(0px, calc(8vh - 105px));
  }
  .home-inner {
    text-align: center;
    max-width: 720px;
    padding: 0 20px;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  .home-logo-img {
    width: 100%;
    max-width: 480px;
    height: auto;
    display: block;
  }
  /* The ladder's first rule measured against HEIGHT rather than width, and it
     exists because the home screen gained a second button.

     Below about 1312px tall the padding-top above resolves to 0, so this stack
     -- logo, tagline, both buttons -- ends at a fixed 775px whatever the
     viewport does. The daily button alone ended at 708 and cleared a 720px
     laptop by 12px; Unlimited Rounds under it ended at 775 and missed by 55.
     Which put the paid feature's only front door off the bottom of the screen
     on the most ordinary laptop there is.
     The logo is the lever: Logo-Stacked.png is a square canvas, so every pixel
     off its width is a pixel off the stack. 480 - 400 buys 80 and lands the
     button 25px inside a 720px viewport.
     BEFORE the 700px block on purpose. Media queries carry no specificity, so
     source order decides, and the phone's own 300px logo has to win over this
     one -- a 375x812 phone matches both. */
  @media (max-height: 820px) {
    .home-logo-img { max-width: 400px; }
    /* Retuned with it, per the note below: the dead tail scales with the image
       and this margin does not. 400px of artwork trails ~92px of nothing for a
       visible gap near 100, a quarter of which is 25, so 9 - 25. */
    .home-tagline { margin-top: -16px; }
  }
  .home-tagline {
    font-family: 'Newsreader', Georgia, serif;
    font-style: italic;
    font-size: clamp(23px, 2.75vw, 30px);
    /* Negative, because the gap is not this margin's doing. Logo-Stacked.png is
       a square canvas and the artwork stops 288 of its 1254 rows short of the
       bottom, so at 480px wide it trails 110px of nothing -- 120px of visible
       gap, of which the old 10px margin was a twelfth. A quarter of 120 is 30,
       so 10 - 30. The number is per breakpoint because the tail scales with the
       image and the margin does not. */
    margin-top: -20px;
    color: var(--muted);
    font-weight: 500;
  }
  /* Quiet by design: it sits under the one button that matters and must not
     compete with it. Muted, small caps, and only underlined on hover. */
  /* One width for both home buttons, set rather than left to the label: the
     daily's reads Play Today's Challenge, See Your Results or Return to Draft
     depending on the day, and a button sized to its words would change width
     under the player's thumb. 400px clears the longest of them, 380px at 22px,
     and min() hands a phone the column instead. The side padding is only a
     floor now -- the width does the sizing and the label centres in it.
     Height is the shared 56px rule at the end of the stylesheet. */
  .home-play {
    margin-top: 30px;
    width: min(100%, 400px);
    background: var(--ink);
    color: var(--paper);
    border: 2px solid var(--ink);
    padding: 0 16px;
    font-family: 'Anton', sans-serif;
    font-size: 22px;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    cursor: pointer;
    /* Matches .sb-action / .play-again */
    border-radius: 14px;
    transition: background 0.12s;
  }
  /* The old hover paired a 2px lift with a growing offset shadow; with the
     shadow gone the lift alone just reads as a jitter, so hover deepens the
     fill to the palette's other red instead. */
  .home-play:hover:not(:disabled) {
    /* Same ink-to-rust hover as .play-again, the other filled ink button */
    background: var(--rust);
  }
  .home-play:disabled {
    background: var(--muted);
    cursor: wait;
  }
  /* Ace unlimited rounds. Outlined rather than filled, because the daily is
     the button that matters on this screen and a second ink slab would
     compete with it -- the paid feature must not out-shout the free one.
     Hidden outright for everyone else: an Ace-only door that is visible and
     dead is worse than no door. */
  .home-unlimited {
    margin-top: 14px;
    /* Held to the daily button's width -- see .home-play. */
    width: min(100%, 400px);
    background: transparent;
    color: var(--ink);
    border: 2px solid rgba(var(--ink-rgb), 0.35);
    padding: 0 16px;
    font-family: 'Anton', sans-serif;
    /* The daily button's size -- see .home-play. */
    font-size: 22px;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    cursor: pointer;
    border-radius: 14px;
    transition: border-color 0.12s, background 0.12s;
  }
  .home-unlimited:hover {
    border-color: var(--ink);
    background: rgba(var(--ink-rgb), 0.06);
  }
  .home-versus { margin-top: 12px; }
  /* A guest's Unlimited and Versus doors (is-locked): on hover the label gives
     way to Account Log In Required (27 September). Hover-capable screens only --
     on a phone the tap asks them to sign in instead. */
  .home-unlimited .hb-locked { display: none; }
  @media (hover: hover) {
    .home-unlimited.is-locked:hover .hb-label { display: none; }
    .home-unlimited.is-locked:hover .hb-locked { display: inline; }
  }

  /* PRELUDE — week-in-history teaser */
  #prelude.active {
    /* 181px = masthead (73) + main padding (28 top + 80 bottom), so the page is
       exactly one viewport tall with no dead scroll under the closer. */
    min-height: calc(100vh - 181px);
    display: flex;
    align-items: flex-start;
    justify-content: center;
    /* Full width of main. The old 20% side padding cut the column to well under
       half the viewport, so the teaser lines wrapped enough to push the closer
       below the fold. The mobile block sets its own padding, so this is
       desktop/tablet only. */
    padding: 10px 0 60px;
  }
  .prelude-inner {
    width: 100%;
  }
  /* Equal side inset on the four teaser lines. The divider on
     .prelude-header keeps the full container width, so the text block sits
     centred inside it instead of running flush to the left rule end. */
  .prelude-list {
    list-style: none;
    padding: 0 28px;
    margin: 0 0 30px;
  }
  .prelude-item {
    font-family: 'Newsreader', Georgia, serif;
    /* Desktop only — the mobile block sets its own size. Trimmed 10% to leave
       breathing room under the closer. */
    font-size: clamp(30px, 3.27vw, 40px);
    line-height: 1.4;
    padding: 12px 0;
    opacity: 0;
    color: var(--ink);
    animation: prelude-fadein 0.7s ease forwards;
  }
  .prelude-item em {
    font-style: italic;
    font-weight: 700;
    color: var(--rust);
  }
  @keyframes prelude-fadein {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  .prelude-header {
    font-family: 'Anton', sans-serif;
    font-size: clamp(40px, 6.75vw, 68px);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    text-align: center;
    margin-top: 30px;
    padding-top: 28px;
    border-top: 3px double var(--ink);
    line-height: 1.05;
    opacity: 0;
    transform: translateY(12px);
    transition: opacity 0.8s ease, transform 0.8s ease;
  }
  .prelude-header.revealed {
    opacity: 1;
    transform: translateY(0);
  }
  .prelude-header strong {
    color: var(--rust);
    font-weight: inherit;
    display: block;
    margin-top: 6px;
  }
  @keyframes fadein { from { opacity:0; transform: translateY(6px);} to { opacity:1; transform:none;} }

  /* SPLASH (loading) */
  #splash { text-align: center; padding: 80px 20px; }
  #splash .big {
    font-family: 'Anton', sans-serif;
    font-size: 84px;
    line-height: 0.9;
    letter-spacing: 0.02em;
    text-transform: uppercase;
  }
  #splash .big em { color: var(--rust); font-style: normal; }
  #splash .sub {
    margin-top: 18px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 13px;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--muted);
  }
  #splash .err { color: var(--red); margin-top: 24px; max-width: 600px; margin-left: auto; margin-right: auto; font-family: 'Inter', system-ui, sans-serif; font-size: 13px; }

  /* GAME HEADER (the matchup banner) */
  .matchup {
    border: 2px solid var(--ink);
    background: var(--cream);
    padding: 18px 24px;
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: end;
    gap: 32px;
    margin-bottom: 22px;
    position: sticky;
    top: 100px;
    z-index: 30;
    box-shadow: 0 12px 18px -10px rgba(0,0,0,0.25);
  }
  /* The paper this banner stands on, painted upward to meet the masthead.

     It was 24px, which reached 82 against a masthead ending at 73 — a 9px band
     belonging to neither, where the roster and pool heads surfaced on their way
     under the banner and disappeared again. Exactly the band .reveal-head had,
     and it was closed there without anyone noticing this header has the same
     shape: sticky, z-30, under a z-40 masthead.

     120 is past the furthest this banner ever sticks from the top, so the strip
     always reaches whatever the masthead's height happens to be. The overshoot
     costs nothing — the masthead is z-40 against this z-30, so whatever the
     strip paints over it is painted over in turn, and above the masthead there
     is nothing but the same paper. That also retires the two breakpoint
     overrides below, which existed only to re-guess this number for a shorter
     masthead. */
  .matchup::before {
    content: "";
    position: absolute;
    left: -2px;
    right: -2px;
    bottom: calc(100% + 2px);
    height: 120px;
    background: var(--paper);
    pointer-events: none;
  }
  /* Paper-colored band below the sticky banner — sits below the bottom border.

     30, not 22, since the matchups panel came out from between this and the
     section heads. It has to reach whichever head pins below it, and the banner
     has two heights: 113 normally and 141 on a playoff week, so its bottom edge
     is at 213 or 241 against heads pinned at 242. 30 covers both — 213+30 clears
     242 by a pixel, and the playoff case starts a pixel above the head already.
     Anything shorter leaves a band belonging to neither, which pool rows scroll
     up through and vanish into. */
  .matchup::after {
    content: "";
    position: absolute;
    left: -2px;
    right: -2px;
    top: calc(100% + 2px);
    height: 30px;
    background: var(--paper);
    pointer-events: none;
  }
  .matchup .m-right { text-align: right; }
  /* Long postseason labels ("2002 · CONFERENCE CHAMPIONSHIPS") must clip at the
     timer rather than wrap and grow this sticky banner. min-width:0 lets the
     1fr grid column shrink below its content — grid items default to
     min-width:auto, which would otherwise refuse to. */
  .matchup .m-left { min-width: 0; }
  .matchup .label {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 15px;
    letter-spacing: 0.25em;
    text-transform: uppercase;
    color: var(--muted);
  }
  .matchup .value {
    font-family: 'Anton', sans-serif;
    font-size: 53px;
    line-height: 1;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .matchup .timer {
    font-family: 'Anton', sans-serif;
    font-size: 53px;
    line-height: 1;
    color: var(--ink);
    letter-spacing: 0.02em;
  }
  /* A round name that is clipped on one line stacks onto two (fitSeasonLabel),
     standing exactly as tall as the year beside it, which keeps its size.
     Measured in Anton: capitals rise 0.86em, "2024" 0.87em. Two lines at
     line-height 1 run from the first line's cap top to the second's baseline
     in 1.86 of their own size, so 0.87 / 1.86 = 0.468em of the year. As an
     inline-block its baseline is its LAST line's, which puts the second line on
     the year's baseline and the first line's tops level with the year's. */
  .matchup .value.two-line .wk-round {
    display: inline-block;
    font-size: 0.468em;
    line-height: 1;
    letter-spacing: 0.03em;
    vertical-align: baseline;
  }
  .matchup .value.two-line .wk-line { display: block; }
  .matchup .timer.warning { color: var(--rust); }
  .matchup .timer.danger { color: var(--red); animation: pulse 0.6s infinite alternate; }
  /* An untimed round (unlimited play) runs no clock, so the clock is not shown --
     but its column keeps its space. HIDDEN, NOT display:none: the banner is
     bottom-aligned and TIME REMAINING over the clock is its tallest column, so it
     sets the row's height. The playoff padding that clears the badge was sized
     with it there; taken out of the layout (as it was on 14 September) the row
     lost ~18px and the badge sat on the week title, at every width. Hidden, the
     banner is the daily's to the pixel, and a long round name clips where it
     does there. */
  .matchup.is-untimed .m-right { visibility: hidden; }
  @keyframes pulse { from { transform: scale(1);} to { transform: scale(1.06);} }

  /* The playoff banner is the regular week's height (by request, 26 September):
     no extra padding, and a badge small enough for the room a regular banner
     already has above the title's capitals -- 40px on desktop, 33 on phones.
     27px here, 21 at 700 and down. placePlayoffBadge centres it in that room,
     measured from the title's caps, so no breakpoint carries a figure. Its
     stars are drawn and centred in their boxes, and Anton's capitals sit on
     their line box's centre (0.495em at line-height 1), so flex centring puts
     the stars' centres on the text's. */
  .playoff-badge {
    position: absolute;
    top: 6px;
    left: 24px;
    background: var(--rust);
    color: var(--paper);
    padding: 5px 13px;
    font-family: 'Anton', sans-serif;
    font-size: 12px;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    border: 2px solid var(--ink);
    display: none;
    align-items: center;
    justify-content: center;
    gap: 8px;
    line-height: 1;
    white-space: nowrap;
  }
  .playoff-badge .badge-star { width: 13px; height: 13px; flex: none; display: block; }
  .playoff-badge .badge-text {
    line-height: 1;
    /* The letter-spacing trails the last letter; give it back on the left so
       the text sits centred between the stars. */
    padding-left: 0.2em;
  }
  .matchup.is-playoff {
    background: #f3dccd;
    border-top-color: var(--rust);
    border-bottom-color: var(--rust);
    border-left-color: var(--rust);
    border-right-color: var(--rust);
  }
  .matchup.is-playoff .playoff-badge { display: flex; }

  /* The cards below are the RESULTS page's week-matchup panel. The draft used to
     open the same grid from a collapsible band between the banner and the board;
     that came out, and .info-panels went with it. */
  .matchups-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 12px 18px;
  }
  @media (max-width: 900px) {
    .matchups-grid { grid-template-columns: repeat(2, 1fr); }
  }
  @media (max-width: 540px) {
    .matchups-grid { grid-template-columns: 1fr; }
  }
  .matchup-card {
    background: var(--paper);
    border: 1px solid rgba(0,0,0,0.15);
    padding: 8px 10px;
    display: flex; flex-direction: column; gap: 4px;
  }
  .matchup-card .matchup-row {
    display: flex; align-items: center; gap: 8px;
    line-height: 1.1;
  }
  .matchup-card .team-name {
    font-family: 'Anton', sans-serif;
    font-size: 16px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }
  .team-logo {
    width: 22px; height: 22px;
    object-fit: contain;
    flex-shrink: 0;
  }
  .team-logo-fallback {
    width: 22px; height: 22px;
    flex-shrink: 0;
    background: var(--cream);
    border-radius: 50%;
    display: inline-block;
  }
  .team-logo.inline {
    width: 18px; height: 18px;
    vertical-align: -4px;
    margin-right: 4px;
  }

  /* ROSTER + POOL */
  .game-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 22px;
  }
  @media (max-width: 880px) { .game-grid { grid-template-columns: 1fr; } }

  .roster {
    border: 2px solid var(--ink);
    background: var(--paper);
  }
  .roster-head {
    padding: 12px 24px;
    border-bottom: 2px solid var(--ink);
    background: var(--ink);
    color: var(--paper);
    font-family: 'Anton', sans-serif;
    font-size: 20px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    display: flex; align-items: center; justify-content: space-between;
    position: sticky;
    /* Was 316, below a pinned matchups panel that is no longer there. 242 is
       where that panel used to pin, which is a pixel clear of the banner at its
       tallest -- see the arithmetic on .matchup::after. */
    top: 242px;
    z-index: 28;
  }
  .roster-head .roster-count {
    font-family: 'Anton', sans-serif;
    font-size: 20px;
    letter-spacing: 0.05em;
  }
  .slot {
    padding: 14px 24px;
    display: flex; align-items: center; gap: 12px;
    border-bottom: 1px solid var(--rule);
    cursor: pointer;
    transition: background 0.12s;
    min-height: 84px;
  }
  .slot:last-child { border-bottom: none; }
  .slot:hover { background: var(--cream); }
  .slot.active { background: var(--gold-light); color: var(--ink); }
  .slot .pos {
    font-family: 'Anton', sans-serif;
    font-size: 22px;
    width: 56px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
  }
  .slot-team-logo {
    width: 36px; height: 36px;
    object-fit: contain;
    flex-shrink: 0;
  }
  .slot-team-logo.placeholder {
    background: transparent;
    display: inline-block;
  }
  .slot .pick {
    flex: 1;
    font-family: 'Newsreader', Georgia, serif;
    font-size: 18px;
  }
  .slot .pick .player-name { font-weight: 700; display: block; font-size: 18px; line-height: 1.2; }
  .slot .pick .meta { font-family: 'Inter', system-ui, sans-serif; font-size: 12px; color: var(--muted); letter-spacing: 0.08em; text-transform: uppercase; margin-top: 2px; }
  /* --muted on --gold measures 1.42:1 -- unreadable on the card being replaced.
     Ink at 85% keeps it lighter than the matchup's team (5.32:1) and readable. */
  .slot.active .pick .meta,
  .slot.active .mu-opp { color: rgba(var(--ink-rgb), 0.85); }
  .slot .pick.empty { color: var(--muted); font-style: italic; }
  .slot .clear {
    background: none; border: 1.5px solid var(--ink);
    font-family: 'Inter', system-ui, sans-serif; font-size: 16px;
    width: 32px; height: 32px;
    padding: 0;
    cursor: pointer;
    line-height: 1;
    color: var(--ink);
    display: flex; align-items: center; justify-content: center;
    flex-shrink: 0;
  }
  .slot .clear:hover { background: var(--ink); color: var(--paper); }

  .pool {
    border: 2px solid var(--ink);
    background: var(--paper);
    min-height: 500px;
    display: flex; flex-direction: column;
  }
  .pool-head {
    padding: 12px 16px;
    border-bottom: 2px solid var(--ink);
    display: flex; justify-content: space-between; align-items: center;
    background: var(--cream);
    position: sticky;
    top: 242px;   /* with .roster-head; see there */
    z-index: 28;
  }
  .pool-head h2 {
    font-family: 'Anton', sans-serif;
    font-size: 20px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
  }
  .pool-head .filter {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 13px;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    color: var(--muted);
  }
  .pool-search {
    padding: 10px 16px;
    border-bottom: 1px solid var(--rule);
    background: var(--paper);
  }
  .pool-search input {
    width: 100%;
    padding: 8px 10px;
    border: 1px solid var(--ink);
    background: var(--paper);
    font-family: 'Newsreader', Georgia, serif;
    font-size: 14px;
  }
  .pool-list {
    overflow-y: auto;
    height: 588px;
  }
  .pool-row {
    padding: 14px 24px;
    border-bottom: 1px solid rgba(0,0,0,0.08);
    cursor: pointer;
    /* Who on the left, the week's matchup on the right. */
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 16px;
    transition: background 0.1s;
    min-height: 84px;
  }
  .pool-row .pool-who { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
  /* Their team first and in ink, the opponent after it in grey. Inter is loaded
     at 400 to 700, so 500 is a real weight rather than a faked bold. */
  .pool-row .pool-matchup {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 14.6px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    white-space: nowrap;
    text-align: right;
  }
  /* Shared by the pool rows and the roster cards (matchupHtml). */
  .mu-team { color: var(--ink); font-weight: 500; }
  .mu-opp { color: var(--muted); margin-left: 0.6em; white-space: nowrap; }
  .pool-row:hover { background: var(--cream); }
  .pool-row .pos-tag {
    font-family: 'Anton', sans-serif;
    font-size: 14px;
    padding: 2px 6px;
    background: var(--ink); color: var(--paper);
    text-align: center;
    letter-spacing: 0.05em;
  }
  .pool-row .nm {
    font-family: 'Newsreader', Georgia, serif;
    font-size: 18px;
    font-weight: 700;
    line-height: 1.2;
  }
  .pool-row .meta {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 12px;
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    margin-top: 2px;
  }
  .pool-row .team-age {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 13px;
    color: var(--muted);
    letter-spacing: 0.1em;
    text-align: right;
  }
  .pool-empty { padding: 30px 16px; color: var(--muted); font-style: italic; text-align: center; font-family: 'Inter', system-ui, sans-serif; font-size: 12px; }

  /* Versus, from data on GAME (see renderRoster). A player who cannot be taken --
     drafted by another team, or one who would leave a team unable to finish --
     is greyed rather than removed, so the pool reads like a draft board, and the
     matchup's cell says why, ellipsed if a typed name is long. A committed slot's
     pool is shown to look at only. A committed lineup slot has no ×, no tap. */
  .pool-row.is-unavailable, .pool-row.is-view-only { cursor: default; }
  .pool-row.is-view-only:hover { background: none; }
  /* Greyed out, row and all (by request, 26 September), and hover leaves it so. */
  .pool-row.is-unavailable, .pool-row.is-unavailable:hover { background: rgba(var(--ink-rgb), 0.07); }
  .pool-row.is-unavailable > :not(.pool-note) { opacity: 0.4; }
  .pool-row .pool-note { overflow: hidden; text-overflow: ellipsis; min-width: 0; max-width: 14em; color: var(--muted); }
  /* The reason (who drafted them) starts where MATCHUP does (by request, 26
     September) -- it spanned the age column until then. The age cell stays,
     empty, to hold its column; fitPoolNotes goes to two lines sooner. */
  .pool-row.is-unavailable .pool-note { max-width: none; }
  /* Text and tail on one line (Versus: JOSE – ROUND 2): the text ellipses, the
     tail never does, and fitPoolNotes shortens the tail first. */
  .pool-row .pool-note-split { display: flex; align-items: baseline; white-space: nowrap; }
  .pool-row .pool-note-text { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .pool-row .pool-note-tail { flex: none; white-space: pre; }
  /* Where even the short tail would cut the text: text over tail (fitPoolNotes). */
  .pool-row .pool-note-split.is-two-line { flex-direction: column; align-items: stretch; }
  .slot.is-locked { cursor: default; }
  .slot.is-locked:not(.active):hover { background: none; }

  /* The turn (#board-turn, from GAME.turn): a plain row between the banner and
     the board, in ink on the paper (no box, by request, 26 September) -- `who`
     on the left, filling what the lines on the right leave. Versus puts the
     round there and the pick on the right; whose pick it is heads the lineup.
     One line each at every width: the left ellipses before anything wraps.
     Hidden unless a mode sets it. */
  .board-turn {
    display: flex;
    align-items: center;
    gap: 14px;
    margin: 0 0 12px;
    padding: 0 2px;
    /* Dark grey (by request, 26 September): ink at 70%, well darker than --muted,
       so the round and pick sit back from the lineup's title under them. */
    color: rgba(var(--ink-rgb), 0.7);
  }
  /* Versus's round on the left and pick on the right, in one type (by request,
     26 September): 75% of the 34px name the row used to carry -- 25.5px, and
     21px (75% of 28) at 470 and down. */
  .board-turn-who,
  .board-turn-lines {
    font-family: 'Anton', sans-serif;
    font-size: 25.5px;
    line-height: 1.1;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    white-space: nowrap;
  }
  .board-turn-who {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .board-turn-lines {
    flex: 0 0 auto;
    text-align: right;
  }
  @media (max-width: 470px) {
    .board-turn { gap: 10px; margin-bottom: 8px; }
    .board-turn-who, .board-turn-lines { font-size: 21px; }
  }

  /* The banner's buttons (#matchup-actions, from GAME.bannerActions), stacked
     one over another (by request, 26 September), each the width of the widest,
     over the clock's column. That column is visibility:hidden for an untimed
     round so the banner keeps its height; the row opts back in and sits centred
     on it, out of the flow, so the banner's height never changes. Its right
     edge and the width it holds are set by alignBoardTurn. */
  .matchup .m-right { position: relative; }
  /* A button's height, also read by lowerSeasonLabel, which sets the year on
     where the lower one ends even on a board that draws none. */
  .matchup { --banner-btn-h: 40px; }
  .matchup-actions {
    visibility: visible;
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    flex-direction: column;
    align-items: stretch;
    /* alignBoardTurn stretches the stack to the banner's inside height; the
       buttons share it out evenly, the same space above, between and below. */
    justify-content: space-evenly;
  }
  /* From the user's mockup (26 September): no border, the icon on the left and
     the label centred in the rest; a light button on the page's paper colour
     and a dark one in --gray-dark. On phones only the icon shows (below). */
  .matchup-action {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    column-gap: 10px;
    min-height: var(--banner-btn-h);
    padding: 0 16px 0 14px;
    background: var(--paper);
    color: var(--ink);
    border: none;
    border-radius: 10px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    white-space: nowrap;
    cursor: pointer;
  }
  .matchup-action .ma-label { text-align: center; }
  .matchup-action .ma-icon { display: flex; }
  .matchup-action .ma-icon svg { display: block; }
  .matchup-action.is-dark { background: var(--gray-dark); color: var(--paper); }
  .matchup-action.is-light:hover:not(:disabled) { background: var(--cream-hover); }
  .matchup-action.is-dark:hover:not(:disabled) { background: var(--ink); }
  /* A button a mode passes as disabled (bannerActions' disabled). Versus leaves
     Undo out before the first pick instead of disabling it. */
  .matchup-action:disabled { opacity: 0.35; cursor: default; }
  /* Phones: the icon alone, in a 44 x 30 button (the label stays on the title
     and aria-label). Two are 60px of the phone banner's 66.5 inside. */
  @media (max-width: 700px) {
    .matchup { --banner-btn-h: 30px; }
    .matchup-action {
      grid-template-columns: auto;
      justify-content: center;
      width: 44px;
      padding: 0;
    }
    .matchup-action .ma-label { display: none; }
  }

  /* Desktop-only pieces of the draft board. The (min-width: 881px) block below
     shows them; below that the board is exactly the stacked layout above. */
  .pool-tabs, .pool-cols, .roster-cols, .pool-row .pool-pos, .pool-row .pool-age, .slot .slot-matchup { display: none; }
  /* Mobile-only pieces; the (max-width: 880px) board block shows them. */
  .pool-back, .slot .slot-chev { display: none; }

  /* DRAFT BOARD, DESKTOP (881px and up) -- 14 September, from the user's
     reference layout, rendered in this palette. The roster on the left as a
     lineup table; the pool on the right under position tabs, one row per player,
     picked by clicking the row with the same cream hover as ever -- no add
     button. Phones and tablets keep the layout above; they get a design of their
     own. min-width, so nothing here can collide with the max-width blocks.

     The pool ends level with the roster at any length: its list is height 0
     and grows, so it adds nothing to the grid row, the row takes the roster's
     height, and the list fills what the tabs and the column heads leave. */
  @media (min-width: 881px) {
    .game-grid { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); }

    /* Not sticky here. Pinned at 242 the two heads sat ~20px below where the
       board puts them at the top of the page, over the column heads under them
       -- the old 84px slots had hidden the same overlap. This board is one
       screen tall and the pool scrolls inside itself, so nothing needs pinning. */
    .roster-head, .pool-head { position: static; }
    /* YOUR LINEUP starts where POS does, and the count ends where the × column
       does: the columns' and rows' 16px, not the 24 the head had (8px off until
       17 September). */
    .roster-head { padding-left: 16px; padding-right: 16px; }
    /* And so the paper band under the banner reaches the board and stops. Its 30px
       was sized to meet heads pinned at 242; with nothing pinned it painted over
       the top of both panels' borders, which read as no border above the pool's
       tabs. 22 is the banner's margin below it -- the <=880 block's number, for
       the same reason. */
    .matchup::after { height: 22px; }

    .roster-cols, .pool-cols {
      display: grid;
      align-items: center;
      column-gap: 12px;
      padding: 7px 16px;
      background: var(--cream);
      border-bottom: 1px solid var(--rule);
      font-family: 'Inter', system-ui, sans-serif;
      font-size: 11px;
      letter-spacing: 0.14em;
      text-transform: uppercase;
      color: var(--muted);
    }
    /* The roster row's four tracks: position, name, matchup, and the clear
       button's 32px. MATCHUP's width is the pool's, so the two read alike. */
    .roster-cols,
    .slot { grid-template-columns: 56px minmax(0, 1fr) 124px 32px; column-gap: 12px; }
    .slot { display: grid; align-items: center; }
    .slot .pos { width: auto; }
    /* One line: the name, then the matchup in its own column. Position and age
       are not repeated under the name -- the POS column already says one. */
    .slot .pick .meta { display: none; }
    .slot .pick .player-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .slot .slot-matchup {
      display: block;
      font-family: 'Inter', system-ui, sans-serif;
      font-size: 13.5px;
      line-height: 1.2;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      text-align: left;
      white-space: nowrap;
    }
    /* The rows' own columns, with the list's scrollbar allowed for on the right
       (--pool-scrollbar, measured in renderPool) so AGE and MATCHUP sit exactly
       over theirs. Left-justified, like the cells under them. */
    .pool-cols {
      grid-template-columns: 48px minmax(0, 1fr) 56px 124px;
      column-gap: 14px;
      padding-right: calc(16px + var(--pool-scrollbar, 0px));
    }

    /* One pitch on both sides -- 56px, border included -- so the roster's lines
       and the pool's run straight across the board. The pool is the roster's
       height, which makes it exactly seven rows tall, and it snaps a row at a
       time, so the two stay in line as it scrolls. */
    .slot, .pool-row {
      box-sizing: border-box;
      height: 56px;
      min-height: 0;
      padding-top: 0;
      padding-bottom: 0;
    }
    .slot { padding-left: 16px; padding-right: 16px; }

    .pool { min-height: 0; }
    .pool-head { padding: 0; background: var(--ink); }
    .pool-head h2, .pool-head .filter { display: none; }
    .pool-tabs { display: flex; width: 100%; }
    .pool-tab {
      flex: 1;
      padding: 12px 6px;
      background: transparent;
      border: none;
      border-right: 1px solid rgba(var(--paper-rgb), 0.18);
      color: var(--paper);
      font-family: 'Anton', sans-serif;
      font-size: 20px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      cursor: pointer;
      transition: background 0.12s;
    }
    .pool-tab:last-child { border-right: none; }
    .pool-tab:hover { background: rgba(var(--paper-rgb), 0.12); }
    .pool-tab.active, .pool-tab.active:hover { background: var(--rust); }

    .pool-list {
      height: 0;
      flex: 1 1 auto;
      scroll-snap-type: y mandatory;
    }
    .pool-row {
      grid-template-columns: 48px minmax(0, 1fr) 56px 124px;
      column-gap: 14px;
      padding-left: 16px;
      padding-right: 16px;
      align-items: center;
      scroll-snap-align: start;
      /* A wheel tick or a swipe stops at the next row rather than flying past
         several, which is what keeps the pool's lines on the roster's. */
      scroll-snap-stop: always;
    }
    /* Every cell is one line at line-height 1.2, so all four boxes centre on the
       row's middle. The meta line under the name is desktop-hidden: position
       and age have columns. */
    .pool-row .pool-pos,
    .pool-row .pool-age { display: block; line-height: 1.2; }
    /* Type matches the roster row's cell for cell, so the two sides read as one
       table: position 22px (.slot .pos), name 18px (.slot .pick .player-name),
       age and matchup 13.5px (.slot .slot-matchup). Change one, change both. */
    .pool-row .pool-pos {
      font-family: 'Anton', sans-serif;
      font-size: 22px;
      letter-spacing: 0.06em;
    }
    .pool-row .pool-who { justify-content: center; }
    .pool-row .nm { font-size: 18px; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .pool-row .meta { display: none; }
    .pool-row .pool-age {
      font-family: 'Inter', system-ui, sans-serif;
      font-size: 13.5px;
      letter-spacing: 0.08em;
      color: var(--muted);
    }
    .pool-row .pool-matchup { font-size: 13.5px; line-height: 1.2; text-align: left; }
  }

  /* SUBMIT */
  .submit-bar {
    margin-top: 22px;
    margin-bottom: 80px;
    display: flex; justify-content: space-between; align-items: center;
    border-top: 3px double var(--ink); padding-top: 18px;
  }
  .submit-bar .progress {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 12px;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    color: var(--muted);
  }
  /* The line never takes two lines, at any width. From 920px it is the long
     wording, 25% larger -- measured, not guessed: the longest, "Not sent yet --
     check your connection, then hit Kickoff!", is 619px at 15px against a 199px
     button, which fits with a 16px gap from 913px up. Below that the long
     wording wraps even at 12px (that message, to about 780px; at 11px on a
     phone, "Fill all 7 slots" too), so setProgressText's short wording shows
     instead, at 12px (11px in the 700px block). Every short message is at most
     31 characters, which fits a 320px phone. Re-measure for a longer one. */
  .submit-bar .progress .progress-short { display: none; }
  @media (min-width: 920px) {
    .submit-bar .progress { font-size: 15px; }
  }
  @media (max-width: 919px) {
    .submit-bar .progress .progress-long { display: none; }
    .submit-bar .progress .progress-short { display: inline; }
  }
  .submit-btn {
    background: var(--rust); color: var(--paper);
    padding: 18px 42px;
    font-family: 'Anton', sans-serif;
    font-size: 28px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    cursor: pointer;
    border: 2px solid var(--ink);
    /* Matches .home-play / .play-again / .sb-action */
    border-radius: 14px;
    transition: background 0.12s;
  }
  /* The old lift paired a 2px nudge with a growing offset shadow; with the
     shadow gone it just reads as a jitter, so both states deepen the fill
     instead. :active is kept so a tap still shows feedback on touch. */
  .submit-btn:hover:not(:disabled) { background: var(--red); }
  .submit-btn:active:not(:disabled) { background: var(--red); }
  .submit-btn:disabled { background: var(--muted); cursor: not-allowed; }

  /* REVEAL SCREEN */
  .reveal-head {
    text-align: center;
    padding: 16px 0 14px;
    border-bottom: 4px double var(--ink);
    /* The space under the double rule, halved by request (11 October): 15px at
       rest, and the band below with it (::after), which is what a results
       board or Versus's standings land beneath. */
    margin-bottom: 15px;
    background: var(--paper);
    position: sticky;
    top: 100px;
    z-index: 30;
  }
  /* The paper the header stands on, painted upward to meet the masthead.

     It was 24px, and the header sticks 100px down a 73px masthead, which left a
     9px band belonging to neither: rows sliding under the header surfaced in it
     and disappeared again. The number was only ever right for one masthead
     height, and there are three of those across the breakpoints.

     120 is past the furthest the header ever sticks from the top, so the strip
     always reaches. The overshoot costs nothing: the masthead is sticky at z-40
     against this header's 30, so whatever the strip paints over the masthead is
     painted over in turn -- and above the masthead there is nothing but the same
     paper. That also retires the two breakpoint overrides, which existed to
     re-guess this number for a shorter masthead. */
  .reveal-head::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;
    height: 120px;
    background: var(--paper);
    pointer-events: none;
  }
  .reveal-head::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: calc(100% + 4px);
    height: 9px;
    background: var(--paper);
    pointer-events: none;
  }
  .reveal-head .kicker {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 12px;
    letter-spacing: 0.25em;
    text-transform: uppercase;
    color: var(--muted);
  }
  .reveal-head h1 {
    font-family: 'Anton', sans-serif;
    font-size: 48px;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    margin-top: 2px;
    line-height: 1;
  }
  .reveal-list { max-width: 800px; margin: 0 auto; }
  .reveal-row {
    border-bottom: 1px solid var(--rule);
    padding: 25px 0 25px 40px;
    display: grid;
    grid-template-columns: 70px 1fr auto;
    gap: 20px;
    align-items: center;
    opacity: 0;
    transform: translateY(10px);
    animation: revealin 0.8s forwards;
  }
  @keyframes revealin { to { opacity: 1; transform: none; } }
  .reveal-row .slot-label {
    font-family: 'Anton', sans-serif;
    font-size: 33px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    text-align: center;
  }
  .reveal-row .player-info {
    font-family: 'Newsreader', Georgia, serif;
  }
  .reveal-row .player-info .nm {
    font-size: 28px;
    font-weight: 700;
    display: block;
    line-height: 1.1;
  }
  .reveal-row .player-info .opp-line {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 15px;
    color: var(--muted);
    margin-top: 8px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    line-height: 1.3;
    min-height: 18px;
  }
  .reveal-row .player-info .stat-line {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 15px;
    color: var(--ink);
    margin-top: 4px;
    letter-spacing: 0.04em;
    line-height: 1.5;
    min-height: 22px;
    transition: opacity 0.4s ease;
  }
  .reveal-row .player-info .stat-line .elite-stat {
    font-weight: 900;
    color: var(--rust);
  }
  .stat-chip {
    display: inline-block;
    white-space: nowrap;
  }
  .stat-sep {
    white-space: normal;
  }
  .reveal-row .points {
    font-family: 'Anton', sans-serif;
    font-size: 42px;
    text-align: right;
    color: var(--ink);
    min-width: 70px;
    transition: opacity 0.4s ease, transform 0.4s ease;
  }
  .reveal-row .points.points-revealed {
    animation: pointsPop 0.5s ease;
  }
  @keyframes pointsPop {
    0%   { transform: scale(0.7); opacity: 0; }
    60%  { transform: scale(1.15); opacity: 1; }
    100% { transform: scale(1); opacity: 1; }
  }
  .reveal-row .stat-line.stats-revealed {
    animation: statsIn 0.5s ease;
  }
  @keyframes statsIn {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: none; }
  }
  .reveal-row.top-pick { background: linear-gradient(90deg, rgba(var(--gold-rgb), 0.18), transparent 60%); }
  .reveal-row.empty .player-info .nm { color: var(--muted); font-style: italic; font-weight: 500; }
  /* An empty slot never gets an opponent or a stat line — the reveal only fills
     those for a real pick. Left in place their reserved min-heights make
     .player-info taller than its visible text, which pushes "— empty —" above
     the position label and the score. Collapsing them lets the row centre. */
  .reveal-row.empty .opp-line,
  .reveal-row.empty .stat-line { display: none; }

  /* ---- REVEAL SIDE BY SIDE ----
     Your pick and the week's best pick for each slot on one row, the slot label
     centred between them. Sizes run 15% under the single-column reveal above
     (name 28 -> 24, meta 15 -> 12.75, points 42 -> 36, label 33 -> 28, row
     padding 25 -> 21), and the stat line is held to two lines.

     The stat line is a direct child of each side rather than of .player-info,
     so it can span the side's whole width under the name and the score -- the
     width is what lets two lines carry a full stat line on a phone. Declared
     after the rules above, so it wins on order at equal specificity; its phone
     rules sit in the EARLY 700px block, after the reveal overrides there. */
  .reveal-pair-head,
  .reveal-row.reveal-pair {
    /* The centre band: 164px (two 63px scores, the rule, their gaps and the
       cell's padding) widened by 10%. See .rv-scores for how the scores sit in it. */
    grid-template-columns: minmax(0, 1fr) 180.4px minmax(0, 1fr);
    gap: 17px;
  }
  /* The player's username over their side, BEST LINEUP over the other, at the
     player name's size (24px here, 15.3 on a phone). Letter-spacing comes down
     from 0.18em to 0.06em with the size: at 24px the old spacing ran a
     20-character username -- the most a name can be -- past its column. A name
     that still does not fit is truncated rather than wrapped, with the whole
     name on the title attribute. */
  .reveal-pair-head {
    display: grid;
    align-items: end;
    padding: 0 0 10px;
    border-bottom: 1px solid var(--rule);
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 24px;
    letter-spacing: 0.06em;
    line-height: 1.2;
    text-transform: uppercase;
    color: var(--muted);
  }
  .reveal-pair-head > span {
    padding: 0 8px;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* Each side's heading may run on into the centre band, by request (17
     September), but never past the slot's letters in the row below: the
     username stops where QB starts, BEST LINEUP where it ends. Both sides span
     their own column and the band, on one row, and syncRevealPairHead measures
     QB and sets the two limits. The centre span is unused. */
  .reveal-pair-head > span { box-sizing: border-box; grid-row: 1; }
  .reveal-pair-head > span:first-child {
    grid-column: 1 / 3;
    justify-self: start;
    max-width: var(--rv-head-left-max, 50%);
  }
  .reveal-pair-head > span:last-child {
    grid-column: 2 / 4;
    justify-self: end;
    max-width: var(--rv-head-right-max, 50%);
  }
  .reveal-pair-head > span:nth-child(2) { display: none; }
  /* The head stays in view under the page's header while the rows scroll
     beneath it, and is let go as the last row (FLEX) starts under it.

     Sticky holds an element inside its parent's CONTENT box, so the list carries
     the last row in its bottom padding instead of its content: the row's own
     negative margin gives its height back, the list is no taller, and the
     content box now ends where FLEX begins. Both lengths come from
     syncRevealPairHead(): --rv-last-row is that row's height, and --rv-head-stick
     is where the page's header ends once it has stuck, plus the margin under it,
     so the head sticks at the same distance below it that it sits at rest.

     z-index 29, under the header's 30: the paper strip painted upward (120px,
     past anything it could need to cover) goes under the header, and covers the
     gap between the two for rows passing through it. */
  .reveal-pair-head {
    position: sticky;
    top: var(--rv-head-stick, 0px);
    z-index: 29;
    background: var(--paper);
  }
  .reveal-pair-head::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;
    height: 120px;
    background: var(--paper);
    pointer-events: none;
  }
  .reveal-list { padding-bottom: var(--rv-last-row, 0px); }
  .reveal-list > .reveal-row:last-child { margin-bottom: calc(-1 * var(--rv-last-row, 0px)); }
  .reveal-row.reveal-pair {
    padding: 21px 0;
    align-items: start;
    position: relative;
  }
  /* The cream behind the centre column, the full height of the row. A band on
     the row rather than a background on the cell, because the cell no longer
     stretches -- it is baseline-aligned. The columns are symmetric, so the
     centre column is centred on the row. */
  .reveal-row.reveal-pair::before {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 180.4px;
    transform: translateX(-50%);
    background: var(--cream);
  }
  /* Positioned, so they paint over the band, which is. */
  .rv-side,
  .rv-center { position: relative; }
  /* The centre of each row: the slot, both scores under it -- yours left, the
     week's best right, a hairline between -- and a top pick's stars either side of the slot,
     over the cream band the row paints.

     The slot label is centred on the FIRST line of the players' names. Sides and
     centre all start at the row's top, and a name's first line is centred at its
     side's top padding plus half its line box; the label's box (line-height 1)
     starts half its own height above that. Desktop: 6 + 24 x 1.1 / 2 - 26 / 2 =
     6.2px. A wrapped name adds lines below its first, so the label stays level
     with the first. The phone breakpoints restate the sum at their sizes. */
  .rv-center {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    /* No side padding: the scores' halves run from the band's edge to its centre. */
    padding: 6.2px 0 0;
  }
  .reveal-pair .slot-label {
    font-size: 26px;
    text-align: center;
    line-height: 1;
  }
  /* The band in two equal halves either side of the rule, the full width of the
     column, so each score is centred in the space between the band's edge and the
     centre line -- and the rule, between two equal tracks, is that centre line. */
  .rv-scores {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 1px minmax(0, 1fr);
    align-items: center;
    width: 100%;
  }
  .rv-score-rule {
    width: 1px;
    align-self: stretch;
    background: rgba(var(--ink-rgb), 0.3);
  }
  /* Each score centred in its own column, either side of the rule. The two
     columns share one min-width (1.75em), so the rule stays on the centre line. */
  .rv-center .rv-score-yours,
  .rv-center .rv-score-best { text-align: center; }
  /* A side is the player alone now -- the score moved to the centre cell -- so
     one column: name and matchup, stats under them. The best side mirrors
     yours, right-justified. */
  .rv-side {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "info" "stats";
    align-content: start;
    min-width: 0;
    padding: 6px 8px;
    border-radius: 10px;
  }
  .rv-side .player-info { grid-area: info; min-width: 0; }
  .rv-side .stat-line { grid-area: stats; }
  .rv-best .player-info,
  .rv-best .stat-line { text-align: right; }
  .reveal-pair-head > span:last-child { text-align: right; }
  .reveal-pair .player-info .nm { font-size: 24px; overflow-wrap: anywhere; }
  .reveal-pair .player-info .opp-line { font-size: 12.75px; margin-top: 6px; min-height: 15px; }
  /* Two lines at most, and two lines always reserved (3em at a 1.5 line
     height), so a row does not grow under the player when its stats land. */
  .reveal-pair .stat-line {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 12.75px;
    color: var(--ink);
    margin-top: 4px;
    letter-spacing: 0.04em;
    line-height: 1.5;
    min-height: 3em;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    transition: opacity 0.4s ease;
  }
  .reveal-pair .stat-line .elite-stat { font-weight: 900; color: var(--rust); }
  /* A team's final score after its code in the matchup: the line's own font and
     colour, bold. */
  .reveal-pair .opp-line .mu-score { font-weight: 700; color: inherit; }
  /* VS (or @) 25% under the line on tablets and desktop, by request; phones put
     it back to the line's size in the 700px block. */
  .reveal-pair .opp-line .mu-vs { font-size: 0.75em; }
  /* Tablets (701-880): name, matchup and stats 2px up, by request. The phone
     block below does not reach this range, so nothing later overrides it. */
  @media (min-width: 701px) and (max-width: 880px) {
    .reveal-pair .player-info .nm { font-size: 26px; }
    .reveal-pair .player-info .opp-line { font-size: 14.75px; min-height: 19px; }
    .reveal-pair .stat-line { font-size: 14.75px; }
  }
  /* INT and fumbles lost, 2px under the rest of the line at every size: 1em
     here is the stat line's own size, whatever the breakpoint made it. */
  .reveal-pair .stat-line .stat-minor { font-size: calc(1em - 2px); }
  .reveal-pair .points { font-size: 36px; min-width: 1.75em; }
  /* A top pick: a star either side of the slot, each centred over the score
     below it -- yours left, the best right. 1.23em of the slot's size: the star
     is a fallback glyph whose ink is ~0.70em tall against Anton's ~0.86em
     capitals, so at 1em it read smaller than the letters. Out of flow, so the
     row is the same height as one without.

     The label stretches to the centre cell's full width, which is the scores'
     grid's width (no side padding on either), so the score columns' centres
     are the label's too: (100% - the 1px rule) / 4 in from each edge.

     Stretched, the label's text-align shows, and the phone block's
     `.reveal-row .slot-label { text-align: left }` comes later at the same
     specificity -- hence both classes on the row here. */
  .reveal-row.reveal-pair .slot-label { position: relative; align-self: stretch; text-align: center; }
  .reveal-pair .rv-star {
    position: absolute;
    top: 50%;
    font-size: 1.23em;
    line-height: 1;
    color: var(--gold);
    transform: translate(-50%, -50%);
  }
  .reveal-pair .rv-star:first-child { left: calc((100% - 1px) / 4); }
  .reveal-pair .rv-star:last-child { left: calc(100% - (100% - 1px) / 4); }
  .reveal-pair .rv-star.star-pending { visibility: hidden; }
  .reveal-pair .rv-star.star-revealed { animation: starPop 0.5s ease; }
  @keyframes starPop {
    0%   { opacity: 0; scale: 0.4; }
    60%  { opacity: 1; scale: 1.3; }
    100% { scale: 1; }
  }
  /* A correct pick lights the whole row, top to bottom, from BOTH edges -- the
     player's side fading to the right, the best side mirrored, fading to the
     left -- each spent by the time it reaches the centre band. The layout is
     symmetric, so one distance serves both: the band's edge less the column gap,
       (100% - 180.4px) / 2 - 17px = 50% - 107.2px.
     The phone breakpoints restate it with their own column sizes. */
  .reveal-row.reveal-pair.top-pick {
    --rv-fade-end: calc(50% - 107.2px);
    background:
      linear-gradient(90deg, rgba(var(--gold-rgb), 0.18), rgba(var(--gold-rgb), 0) var(--rv-fade-end)),
      linear-gradient(270deg, rgba(var(--gold-rgb), 0.18), rgba(var(--gold-rgb), 0) var(--rv-fade-end));
  }
  /* About four characters off the side facing the score, so a stat line never
     runs up to it. In ch, the stat line's own Inter, so it stays about four
     characters at every size; fitRevealStats measures the narrower box. Not on
     a phone: see the 700px block. */
  .rv-yours .stat-line { margin-right: 4ch; }
  .rv-best .stat-line { margin-left: 4ch; }
  .rv-side.empty .player-info .nm { color: var(--muted); font-style: italic; font-weight: 500; }
  .rv-side.empty .opp-line,
  .rv-side.empty .stat-line { display: none; }

  /* =================================================================
     SCOREBOARD — card layout
     Rounded cards on the paper panel instead of the old flat rows. The
     palette is unchanged: rust for the player's score, green for the
     optimal, cream cards, ink text, gold stars.
     ================================================================= */
  .scoreboard {
    margin: 50px auto 0;
    max-width: 800px;
    padding: 14px;
    background: var(--paper);
    border: 2px solid var(--ink);
    border-radius: 20px;
  }
  .sb-grid {
    display: grid;
    /* minmax(0, 1fr) rather than 1fr. A bare 1fr is minmax(AUTO, 1fr), so a
       card whose content will not shrink drags its column past its share and
       the pair stops being equal. The zero floor makes them equal at every
       width, whatever is inside them. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px;
  }
  .sb-card {
    background: var(--cream);
    border-radius: 14px;
    padding: 18px 20px;
    min-width: 0;
  }
  /* Scoped to the panel: inside .sb-grid the gap already spaces the cards, and
     a top margin there would subtract from the row stretch and desync heights. */
  .scoreboard > .sb-grid + .sb-card,
  .scoreboard > .sb-card + .sb-grid,
  .scoreboard > .sb-card + .sb-card { margin-top: 12px; }

  .sb-label {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 16px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--muted);
    display: flex;
    align-items: center;
    gap: 9px;
    line-height: 1.1;
    /* Matches the tallest thing a label can hold (the Time Bonus icon) so the
       text centres identically whether or not a card has an icon — that is what
       lines the two cards' labels up at the top. */
    min-height: 21px;
  }
  /* One size for every card's number — the score, the bonus and the optimal
     all read as the same rank of figure. */
  .sb-value {
    font-family: 'Anton', sans-serif;
    font-size: 60px;
    line-height: 1;
    margin-top: 10px;
    letter-spacing: 0.01em;
  }
  .sb-icon {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    display: block;
  }

  /* Your Score — the one solid-filled card */
  .sb-card-score { background: var(--rust); }
  .sb-card-score .sb-label { color: rgba(var(--paper-rgb), 0.82); }
  .sb-card-score .sb-value { color: var(--paper); }

  /* Your Lineup's card reads from the outside in: the time bonus and the
     percentage on the LEFT, left-justified; the title and the score on the
     RIGHT, right-justified.

     The markup keeps its order (title then bonus, score then percentage), so
     `order` brings the pair forward and an auto margin holds the title and the
     score against the card's right edge -- on a line of their own too, when a
     narrow card wraps the row. Only the right-hand run carries an auto margin: a
     second one on the left-hand run would split the free space and pull the
     title off the edge. The 12px column gap keeps the two apart where they meet.

     This replaced a midline arrangement (both left-hand runs starting on the
     card's centre line), and the breakpoints that floored or ranged those runs
     are undone below with it. */
  .sb-card-score .sb-label,
  .sb-card-score .sb-score-row {
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: 6px 12px;
  }
  .sb-card-score .sb-bonus,
  .sb-card-score .sb-pct { order: -1; text-align: left; }
  .sb-card-score .sb-label > span:first-child,
  .sb-card-score .sb-value:not(.sb-pct) {
    margin-left: auto;
    text-align: right;
  }
  /* The title can be a username now, up to 20 characters, which a phone card
     cannot hold: it shortens with an ellipsis rather than running off the card,
     and the whole name is on its title attribute. */
  .sb-card-score .sb-label > span:first-child {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
    max-width: 100%;
  }
  .sb-bonus {
    display: flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
    /* It is a <button> so the icon can open the number on a phone. Everything a
       button brings with it has to come back off; the anchor is for the popover
       below, which must not move the label line it sits on. */
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    color: inherit;
    cursor: pointer;
    position: relative;
    /* Relative to the label rather than a fixed 13px, so the ratio between the
       two runs is the same at every breakpoint -- which is what lets one nudge
       below correct all of them. */
    font-size: 0.8em;
    letter-spacing: 0.08em;
    /* align-items: center lines up the two runs GLYPH CENTRES, and for text of
       different sizes that leaves the smaller one baseline about 1px high --
       which is the bit that reads as not quite level. This drops it back onto
       the title s baseline. Half the cap-height difference: the label's caps run
       ~0.73em, the runs differ by 0.2em, so 0.73 * 0.2 / 2 = 0.073em of the
       label, which is 0.09em of this. */
    position: relative;
    top: 0.09em;
  }
  .sb-bonus .sb-icon { width: 16px; height: 16px; }

  /* The score, and the share of the week's ceiling it came to, on one line. The
     percentage lands under the bonus above it because both halves start on the
     midline, not because either is positioned against the other. */
  /* The sign at half the digits, with its top on THEIR top rather than on the
     line box. vertical-align: top only gets the line box, and in Anton that
     sits above the cap height by different amounts at different sizes, which
     left the sign 2px proud at 60px.

     The nudge is that 2px expressed in the sign's own em -- 0.067 -- so it
     stays correct at 44px and 38px without a rule per breakpoint. Derived from
     the font: actualBoundingBoxAscent is 52 of the 60px digits and 26 of the
     30px sign, against font ascents of 71 and 35. */
  .sb-pct span {
    font-size: 50%;
    line-height: 1;
    vertical-align: top;
    position: relative;
    top: 0.067em;
  }
  .sb-score-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
  }
  /* Full strength against the 0.82 the rest of the label runs at, so the
     number reads and the words stay a caption. */
  .sb-bonus b { color: var(--paper); font-weight: 500; }

  /* Best Lineup — Your Lineup's opposite number */
  .sb-card-best .sb-value { color: var(--green); }
  /* The ceiling is only a number until you know what kind of week it was. This
     is that: where the week's perfect lineup ranks against all 550 in the game.

     Centred rather than on the baseline the row otherwise uses -- the note runs
     to two or three lines against one 60px figure, and sitting its FIRST line
     on the number's baseline would hang the rest below the card. Wraps to its
     own line under the number when the card is too narrow to hold both, the
     same way the time bonus does on Your Score. */
  /* flex-start so the note sits against the figure it is about. The shared rule
     sets space-between, which parked it on the card's right edge with 105px of
     nothing between the two. 8px of gutter, not the shared 10: at 485 the figure
     is 99px of a 181px card and the two pixels decide whether the note keeps its
     place on the line or drops below and takes the card's height with it. */
  .sb-card-best .sb-score-row {
    align-items: center;
    flex-wrap: wrap;
    justify-content: flex-start;
    column-gap: 8px;
    /* The figure's 10px of top margin moves up here. align-items centres MARGIN
       boxes, so while that margin sat on the figure the note centred on a box
       10px taller than the digits and came out 4.6px high. On the row it still
       holds the figure off the label, and the two items centre on what is
       actually drawn. */
    margin-top: 10px;
  }
  .sb-card-best .sb-value { margin-top: 0; }
  /* Three stated lines rather than a sentence left to wrap, so the note has one
     width -- that of HIGHEST WEEK -- instead of a max-content the flex algorithm
     had to be argued out of. Natural size: it sits beside the figure wherever
     that width fits and takes its own row where it does not. */
  .sb-rank-note span { display: block; }
  .sb-rank-note {
    flex: 0 0 auto;
    /* Five pixels clear of the figure, on top of the row's 8px gutter. */
    margin-left: 5px;
    /* Ranged left off the figure rather than off the card's right edge: the
       sentence reads out of the score it is about. */
    text-align: left;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 12px;
    line-height: 1.35;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--muted);
  }
  /* The ordinal suffix stays lower case: 343RD reads as an abbreviation
     shouted, 343rd reads as a number.
     Two things this rule has to beat, both from `.sb-rank-note span` above:
     the uppercase it inherits, and `display: block`, which would drop "rd"
     onto its own line. Written as `.sb-rank-note .sb-rank-ord` (0,2,0) rather
     than `.sb-rank-ord` (0,1,0) so it outweighs that (0,1,1) selector. */
  .sb-rank-note .sb-rank-ord {
    display: inline;
    text-transform: none;
  }


  /* Extra Credit — only rendered when the bonused score beats the optimal */
  .sb-card-extra {
    grid-column: 1 / -1;
    background: var(--green);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    padding: 14px 20px;
  }
  .sb-card-extra .sb-label { color: rgba(var(--paper-rgb), 0.9); }
  .sb-extra-stars {
    font-size: 22px;
    line-height: 1;
    color: var(--gold);
    letter-spacing: 3px;
  }

  /* ---- ACTION CARDS ---- */
  /* NFL Matchup Results, under the board and centred on it. The button keeps
     the width it had as one of the board's two action cards; the panel it opens
     takes the board's full width. */
  .sb-below { max-width: 800px; margin: 16px auto 0; }
  .sb-below > .sb-action { max-width: calc((100% - 12px) / 2); margin: 0 auto; }
  .sb-below > .best-lineup { margin-top: 12px; }
  .sb-action {
    display: flex;
    align-items: center;
    gap: 14px;
    width: 100%;
    text-align: left;
    background: var(--cream);
    border: none;
    border-radius: 14px;
    padding: 16px;
    cursor: pointer;
    color: var(--ink);
    font-family: 'Newsreader', Georgia, serif;
    transition: background 0.12s;
  }
  .sb-action:hover { background: var(--cream-hover); }
  .sb-action-badge {
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: var(--green);
    color: var(--paper);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
  }
  .sb-action-badge .sb-icon { width: 22px; height: 22px; }
  .sb-action-text { flex: 1; min-width: 0; }
  .sb-action-title {
    display: block;
    font-family: 'Anton', sans-serif;
    font-size: 16px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    line-height: 1.15;
  }
  .sb-action-sub {
    display: block;
    margin-top: 5px;
    font-size: 13px;
    color: var(--muted);
    line-height: 1.3;
  }
  .sb-chev {
    flex-shrink: 0;
    color: var(--muted);
    transition: transform 0.2s ease;
  }
  .sb-action[aria-expanded="true"] .sb-chev { transform: rotate(90deg); }

  .matchup-result-card .matchup-row {
    display: flex; align-items: center; gap: 8px;
    line-height: 1.1;
    padding: 2px 0;
    color: #5a564f;
  }
  .matchup-result-card .matchup-row.winner {
    color: var(--ink);
  }
  .matchup-result-card .matchup-row .score {
    margin-left: auto;
    font-family: 'Anton', sans-serif;
    font-size: 18px;
    letter-spacing: 0.04em;
  }

  /* Expanded panels sit as another card under the action that opened them */
  .best-lineup {
    margin-top: 12px;
    background: var(--cream);
    border-radius: 14px;
    padding: 4px 20px;
  }
  .best-row {
    display: grid;
    grid-template-columns: auto 1fr auto;
    gap: 16px;
    align-items: center;
    padding: 12px 0;
    border-bottom: 1px solid rgba(0,0,0,0.1);
  }
  .best-row:last-child { border-bottom: none; }
  .best-row .best-slot {
    font-family: 'Anton', sans-serif;
    font-size: 27px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    text-align: center;
    min-width: 60px;
  }
  .best-row .best-info {
    display: flex;
    flex-direction: column;
    gap: 3px;
  }
  .best-row .best-name {
    font-family: 'Newsreader', Georgia, serif;
    font-weight: 700;
    font-size: 20px;
    line-height: 1.15;
  }
  .best-row .best-matchup {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 14px;
    color: var(--muted);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    line-height: 1.4;
  }
  .best-row .best-stats {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 14px;
    color: var(--ink);
    letter-spacing: 0.04em;
    line-height: 1.4;
  }
  .best-row .best-pts {
    font-family: 'Anton', sans-serif;
    font-size: 22px;
    color: var(--green);
    text-align: right;
    justify-self: end;
  }
  .best-row.is-yours {
    background: linear-gradient(90deg, rgba(var(--gold-rgb), 0.18), transparent 70%);
  }
  .yours-tag {
    display: inline-block;
    background: var(--rust);
    color: var(--paper);
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 10px;
    padding: 2px 7px;
    margin-left: 10px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    vertical-align: middle;
    border: 1px solid var(--ink);
  }
  .play-again {
    display: block;
    margin: 36px auto 0;
    background: var(--ink); color: var(--paper);
    border: none;
    /* Matches .sb-action so it reads as part of the same card family */
    border-radius: 14px;
    padding: 14px 32px;
    font-family: 'Anton', sans-serif;
    font-size: 18px;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    cursor: pointer;
  }
  .play-again:hover { background: var(--rust); }

  /* The scoreboard panel and the replay button live in the reveal markup from
     the start, but the player rows take ~3s each to appear — so for most of
     that run an empty .scoreboard would sit under them as a bare rounded
     outline, with a live Play Another Week button below it. Both stay out of
     the flow until renderScoreboard() has the final score. */
  #reveal.results-pending #scoreboard,
  #reveal.results-pending .sb-below,
  #reveal.results-pending .res-panel { display: none; }
  /* The summary now sits inside the board, in the slot the Best Lineup
     Possible card left. Full width to match the score cards above it, and no
     border of its own -- the board already draws one, and a second would read
     as a box inside a box.

     It keeps --paper rather than taking the --cream of the cards around it,
     because .res-bar uses --cream as its track: on a cream panel every bar
     would disappear into its own background. */
  .sb-results-slot { margin-top: 12px; }
  .sb-results-slot .res-panel {
    max-width: none;
    margin: 0;
    padding: 4px 0 0;
    border: none;
    border-radius: 0;
  }

  /* HEADSHOT CIRCLE */
  .head {
    width: 48px; height: 48px;
    border-radius: 50%;
    background: var(--cream);
    border: 1px solid var(--rule);
    background-size: cover; background-position: center top;
    flex-shrink: 0;
  }
  .head.large { width: 80px; height: 80px; }

  /* DEBUG BAR */
  .debug-bar {
    position: fixed; bottom: 0; left: 0; right: 0;
    background: var(--ink); color: var(--paper);
    padding: 6px 16px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 10px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    display: flex; justify-content: space-between;
    z-index: 50;
  }
  /* Ships hidden and is revealed only on localhost; see IS_LOCAL below. */
  .debug-bar button {
    background: var(--paper); color: var(--ink);
    border: none; padding: 2px 8px;
    font-family: 'Inter', system-ui, sans-serif; font-size: 10px;
    letter-spacing: 0.1em; cursor: pointer; text-transform: uppercase;
  }

  /* =================================================================
     STACKED LAYOUT (<= 880px)
     At this width .game-grid drops to a single column. The desktop sticky
     chain (masthead / matchup / roster + pool heads) hardcodes
     offsets tuned for the two-column layout, so it no longer lines up and
     would pin roughly a third of the viewport. Keep only the masthead and
     the matchup bar pinned; everything else scrolls.
     ================================================================= */
  @media (max-width: 880px) {
    /* TIME BONUS goes here rather than at 700, because this is where the line
       breaks. The panel stops growing at 768, so above about 859 the card holds
       169px of title box and 158.6 of bonus inside 338 and the two sit together.
       Below it the card shrinks with the viewport -- 293 at 789 against the 305
       they need -- and the bonus took a second line for another 180px of viewport
       before the old rule caught it.

       The number is the point; the words were a caption for it, and the stopwatch
       says the same thing in 16px. Under 470 the number goes too and the icon
       opens it. */
    .sb-bonus span { display: none; }

    .matchup { top: 76px; }
    /* Back to the margin it is masking. The 30px above is sized to reach heads
       pinned at 242, and nothing is pinned below this width -- at 30 it painted
       over the top 6px of the roster head instead. The <=700 block takes it to
       14 for the same reason. */
    .matchup::after { height: 22px; }
    .roster-head, .pool-head { position: static; }
    .reveal-head { top: 76px; }
  }

  /* =================================================================
     MOBILE  (<= 700px)
     The layout above is authored desktop-first. On a phone the two big
     problems are (a) the sticky stack — masthead + matchup banner +
     info panels + roster/pool headers — eating most of the viewport,
     and (b) the display type (Anton at 42-72px) wrapping into columns
     of single words. This block collapses the sticky chain down to
     masthead + a compact matchup bar, and rescales the type.
     ================================================================= */
  @media (max-width: 700px) {
    :root {
      /* Height of the mobile masthead (30.8px logo + 8.6/6.6 padding + 3px
         double border). Every sticky offset below is derived from this. */
      --m-head: 49px;
    }

    /* ---- MASTHEAD ---- */
    /* The logo 10% up (28 -> 30.8px, by request, 27 September), the 2.8px taken
       from the padding above and below it, so the masthead stays 49px. */
    .masthead-inner { padding: 8.6px 16px 6.6px; gap: 14px; }
    .masthead .brand-img { height: 30.8px; }
    .menu-toggle { width: 26px; height: 21px; }
    .menu-toggle span { height: 3px; }
    .header-account { margin-right: -10px; padding: 6px 10px; font-size: 13px; letter-spacing: 0.06em; }
    /* The masthead must never push wider than the phone and give the whole
       page a horizontal scrollbar. Let the wordmark be the thing that yields:
       height:auto with a max-height keeps its aspect ratio while flex shrinks
       it, and min-width:0 is what allows a flex item to go below its
       intrinsic width at all. */
    .brand-link { min-width: 0; }
    .masthead .brand-img { height: auto; max-height: 30.8px; max-width: 100%; }

    /* ---- RESULTS ---- */
    .res-panel { padding: 18px 14px 16px; border-radius: 16px; }
    .res-bar { padding: 8px 11px; gap: 7px; }
    /* No .res-btn size here. This block is declared before the RESULTS rules, so
       it never won one -- and at 17.25px neither button overflows at 320, so
       there is nothing for it to fix. */

    /* No .lb-* rules here. The leaderboard is declared after this block, so
       they all lost on source order and never applied; they live in the late
       700px block now. */

    /* ---- MENU OVERLAY ---- */
    .menu-bar { padding: 14px 16px 12px; gap: 14px; }
    .menu-brand { font-size: 19px; }
    .menu-close { font-size: 30px; }
    .menu-nav { padding: 10px 0 70px; max-width: none; }
    .menu-overlay { --menu-gutter: 20px; }
    .menu-item { font-size: 19px; padding-top: 13px; padding-bottom: 13px; }
    .menu-section-head { padding-top: 16px; padding-bottom: 6px; }
    .menu-divider { margin: 6px 20px; }
    .menu-group-pro { margin: 0 12px; }
    .menu-group-pro .menu-section-head { padding-left: 14px; }
    .menu-group-pro .menu-item { padding-left: 14px; padding-right: 14px; }

    /* ---- LAYOUT ---- */
    main { padding: 14px 16px 70px; }

    /* ---- HOME ---- */
    /* 133px = masthead (49) + main padding (14 top + 70 bottom), so the
       landing screen fills the viewport exactly with no dead scroll. */
    #home.active { padding-top: 2vh; min-height: calc(100svh - 133px); }
    .home-inner { padding: 0 4px; }
    .home-logo-img { max-width: 300px; }
    /* 77px of gap here, so 8 - 19. */
    .home-tagline { font-size: 26px; margin-top: -11px; line-height: 1.35; }
    /* Breaks after "Draft the past"; the spans stay inline on desktop */
    .home-tagline span { display: block; }
    .home-play {
      margin-top: 26px;
      padding: 0 12px;
      font-size: 18px;
    }
    .home-unlimited { margin-top: 12px; padding: 0 12px; font-size: 18px; }

    /* ---- PRELUDE ---- */
    #prelude.active { padding: 18px 4px 50px; min-height: calc(100svh - 133px); }
    .prelude-item { font-size: 30px; padding: 10px 0; }
    .prelude-list { margin-bottom: 22px; padding: 0 16px; }
    .prelude-header {
      font-size: 38px;
      margin-top: 22px;
      padding-top: 20px;
    }
    /* ---- SPLASH ---- */
    #splash { padding: 50px 16px; }
    #splash .big { font-size: 46px; }
    #splash .sub { font-size: 11px; margin-top: 14px; }

    /* ---- MATCHUP BANNER ----
       One compact row: season/week on the left, timer on the right. This is
       the only element besides the masthead that stays pinned on mobile. */
    .matchup {
      top: var(--m-head);
      grid-template-columns: 1fr auto;
      /* Bottom-aligned so the season label sits on the same baseline as the
         clock — the right column is taller (stacked TIME/REMAINING above it),
         so centring would float the label up against the clock's mid-height. */
      align-items: end;
      gap: 12px;
      padding: 9px 14px;
      margin-bottom: 14px;
      box-shadow: 0 8px 12px -9px rgba(0,0,0,0.3);
    }
    .matchup::after { height: 14px; }
    /* Season label matches the clock */
    .matchup .value { font-size: 30px; line-height: 1; letter-spacing: 0.02em; }
    .matchup .label { font-size: 9px; letter-spacing: 0.16em; line-height: 1.25; }
    /* TIME over REMAINING, both right-justified against the clock */
    .matchup .label span { display: block; }
    .matchup .timer { font-size: 30px; }
    /* 21px: 1 + 4 + 11 + 4 + 1, in the 33px a phone banner has above the caps. */
    .playoff-badge {
      left: 14px;
      padding: 4px 10px;
      font-size: 10px;
      letter-spacing: 0.14em;
      border-width: 1px;
      gap: 6px;
    }
    .playoff-badge .badge-star { width: 11px; height: 11px; }
    .playoff-badge .badge-text { padding-left: 0.14em; }

    /* ---- MATCHUP CARDS (results) ---- */
    .matchups-grid { grid-template-columns: repeat(2, 1fr); gap: 8px 10px; }
    .matchup-card { padding: 6px 8px; gap: 3px; }
    .matchup-card .team-name { font-size: 14px; }
    .matchup-card .matchup-row { gap: 6px; }
    .team-logo, .team-logo-fallback { width: 18px; height: 18px; }

    /* ---- ROSTER ---- */
    .game-grid { gap: 14px; }
    .roster-head {
      position: static;
      padding: 10px 14px;
      font-size: 17px;
    }
    .roster-head .roster-count { font-size: 17px; }
    .slot {
      padding: 10px 14px;
      gap: 10px;
      min-height: 64px;
    }
    .slot .pos { font-size: 18px; width: 44px; }
    .slot-team-logo { width: 30px; height: 30px; }
    .slot .pick { font-size: 16px; }
    .slot .pick .player-name { font-size: 16px; }
    .slot .pick .meta { font-size: 10px; letter-spacing: 0.06em; }
    .slot .clear { width: 30px; height: 30px; font-size: 15px; }

    /* ---- PLAYER POOL ----
       Sits below the roster once the grid stacks, so the list gets a
       viewport-relative height and selectSlot() scrolls it into view. */
    .pool { min-height: 0; }
    .pool-head {
      position: static;
      padding: 10px 14px;
      gap: 10px;
    }
    .pool-head h2 { font-size: 17px; }
    .pool-head .filter { font-size: 10px; letter-spacing: 0.1em; text-align: right; }
    .pool-search { padding: 8px 14px; }
    /* 16px keeps iOS Safari from zooming in when the field takes focus */
    .pool-search input { font-size: 16px; }
    .pool-list {
      height: auto;
      max-height: calc(100svh - var(--m-head) - 150px);
      min-height: 300px;
      -webkit-overflow-scrolling: touch;
    }
    .pool-row {
      padding: 10px 14px;
      min-height: 58px;
      column-gap: 10px;
    }
    .pool-row .pool-who { gap: 3px; }
    .pool-row .nm { font-size: 16px; }
    .pool-row .meta { font-size: 10px; letter-spacing: 0.06em; }
    .pool-row .pool-matchup { font-size: 12.4px; letter-spacing: 0.06em; }

    /* ---- SUBMIT ---- */
    .submit-bar {
      flex-direction: column;
      align-items: stretch;
      gap: 12px;
      margin-top: 16px;
      margin-bottom: 40px;
      padding-top: 14px;
    }
    .submit-bar .progress { text-align: center; font-size: 11px; }
    .submit-btn {
      width: 100%;
      padding: 15px 20px;
      font-size: 24px;
    }

    /* ---- REVEAL ---- */
    .reveal-head {
      top: var(--m-head);
      padding: 10px 0 8px;
      margin-bottom: 10px;
      /* The header looked taller on arrival than it did once the page moved,
         which is the same header at two heights above it: 20px below the masthead
         at rest, 3px once it sticks. Pulling it up by the difference means it
         arrives where it is going to sit. Only the flow position moves -- when it
         is stuck, top wins and this is not consulted. */
      margin-top: -17px;
    }
    .reveal-head::after { height: 5px; }
    .reveal-head h1 { font-size: 34px; }
    .reveal-head .kicker { font-size: 10px; }
    /* Equal side inset. The position label is left-justified and the score
       right-justified against the same 12px row padding, so both land the same
       distance from the page edge (the label was centred in its column before,
       which put FLEX 10px further left than TE). The rules keep the full row
       width so they still line up with the double rule under the title.
       The narrower middle column wraps the stat line sooner, which is what
       keeps it clear of the score. */
    .reveal-row {
      grid-template-columns: 46px 1fr auto;
      gap: 10px;
      padding: 16px 12px;
    }
    .reveal-row .slot-label { font-size: 21px; text-align: left; }
    .reveal-row .player-info .nm { font-size: 18px; }
    .reveal-row .player-info .opp-line { font-size: 11px; margin-top: 5px; min-height: 14px; }
    .reveal-row .player-info .stat-line { font-size: 11px; line-height: 1.45; min-height: 16px; }
    .reveal-row .points { font-size: 26px; min-width: 46px; }

    /* REVEAL SIDE BY SIDE: 15% under the phone values just above --
       name 18 -> 15.3, meta 11 -> 9.35, points 26 -> 22, label 21 -> 18. */
    /* 98px (two 38.5px scores, the rule, gaps and padding) widened by 10%. */
    .reveal-pair-head,
    .reveal-row.reveal-pair { grid-template-columns: minmax(0, 1fr) 107.8px minmax(0, 1fr); gap: 6px; }
    .reveal-row.reveal-pair::before { width: 107.8px; }
    .reveal-pair-head { font-size: 15.3px; letter-spacing: 0.06em; padding-bottom: 8px; }
    .reveal-pair-head > span { padding: 0 5px; }
    .reveal-row.reveal-pair { padding: 14px 0; }
    .rv-center { padding: 4.4px 0 0; gap: 4px; }
    .reveal-pair .slot-label { font-size: 16px; }
    .rv-side { column-gap: 6px; padding: 4px 5px; }
    .reveal-pair .player-info .nm { font-size: 15.3px; }
    .reveal-pair .player-info .opp-line { font-size: 10.3px; margin-top: 3px; min-height: 13.2px; }
    .reveal-pair .opp-line .mu-vs { font-size: 1em; }
    .reveal-pair .stat-line { font-size: 10.3px; line-height: 1.45; min-height: 2.9em; margin-top: 3px; }
    /* No four-character gap on a phone. It left the stat line 77px at 375 and 60
       at 320, where a single "412 pass yds" needs 79 -- so a stat that did not fit
       whole took every stat after it, and 6 of a lineup's 14 stat lines came out
       blank at 375, 11 at 320. The side's own padding and the column gap still
       hold the stats off the centre band. */
    .rv-yours .stat-line { margin-right: 0; }
    .rv-best .stat-line { margin-left: 0; }
    .reveal-pair .points { font-size: 22px; min-width: 1.75em; }
    /* (100% - 107.8px) / 2 - 6px gap. */
    .reveal-row.reveal-pair.top-pick { --rv-fade-end: calc(50% - 59.9px); }

    /* ---- SCOREBOARD ---- */
    .scoreboard {
      margin-top: 32px;
      padding: 10px;
      border-radius: 16px;
    }
    /* Was 0.9fr / 1.1fr, to buy the Time Bonus label a few extra px at 375.
       That card is gone -- the bonus sits on Your Score's label line now -- and
       the lopsided split it left behind was visible: two cards that should
       mirror each other, one wider than the other. */
    .sb-grid { gap: 10px; }
    .sb-card { padding: 14px; border-radius: 12px; }
    .scoreboard > .sb-grid + .sb-card,
    .scoreboard > .sb-card + .sb-grid,
    .scoreboard > .sb-card + .sb-card { margin-top: 10px; }
    .sb-label { font-size: 14px; letter-spacing: 0.05em; gap: 6px; min-height: 18px; }
    .sb-value { font-size: 44px; margin-top: 8px; }
    /* Small enough to keep its place beside the figure rather than dropping
       below it. Below is what made the card grow: at 485 the two cards stand at
       98px without the note and 152.5 with it, because the note took its own row
       and Your Score stretched to match a height it had no content for. Beside,
       three lines at this size come to 41px against the figure's 44, so the row
       is already tall enough to hold them and the card does not move.

       On an actual phone the figure alone is most of the card and nothing fits
       beside it, so the note does drop below -- but there Your Score is four rows
       deep and taller than the note makes this card, so the pair still does not
       grow. */
    /* 8px, and a 6px gutter rather than the row's 8, so the note keeps its place
       beside the figure instead of dropping under it. The card is 169px inside its
       padding at 460 and 206.3 takes 99 of that: at 9.5px the note wanted 70.7 of
       the 62 that were left and wrapped, which is what took the card from one
       block to two. At 8px it draws 59.5 and fits with four to spare. Below about
       450 nothing fits beside the figure and it wraps regardless.

       The 5px the note stands off the figure on a desktop comes off here too: at
       460 it was 99.2 of figure, 6 of gap, 5 of margin and 59.5 of note against a
       169px card -- eight tenths of a pixel over, and over is over. */
    .sb-rank-note { font-size: 8px; letter-spacing: 0.02em; margin-left: 0; }
    .sb-card-best .sb-score-row { column-gap: 6px; }
    /* No floor on the title and the score: they range right against the card's
       edge now, and the bonus and the percentage left (see .sb-card-score). */
    .sb-card-score .sb-label > span:first-child,
    .sb-card-score .sb-value:not(.sb-pct) { min-width: 0; padding-right: 0; }
    .sb-score-row { flex-wrap: wrap; }
    .sb-score-row .sb-pct { margin-top: 0; }
    .sb-icon { width: 17px; height: 17px; }
    .sb-card-extra { padding: 12px 14px; }
    .sb-extra-stars { font-size: 18px; }


    /* One column on a phone, as the action cards were. */
    .sb-below > .sb-action { max-width: none; }
    .sb-action { padding: 13px 14px; gap: 12px; }
    .sb-action-badge { width: 38px; height: 38px; }
    .sb-action-badge .sb-icon { width: 19px; height: 19px; }
    .sb-action-title { font-size: 14px; }
    .sb-action-sub { font-size: 12px; margin-top: 4px; }

    /* ---- RESULT ACTIONS / BEST LINEUP ---- */
    .best-row {
      grid-template-columns: 42px 1fr auto;
      gap: 10px;
      padding: 10px 0;
    }
    .best-row .best-slot { font-size: 20px; min-width: 0; }
    .best-row .best-name { font-size: 16px; }
    .best-row .best-matchup, .best-row .best-stats { font-size: 11px; letter-spacing: 0.04em; }
    .best-row .best-pts { font-size: 18px; }
    .yours-tag { font-size: 8px; padding: 2px 5px; margin-left: 6px; }
    .matchup-result-card .matchup-row .score { font-size: 15px; }
    /* radius deliberately not overridden — .sb-action keeps 14px at every
       width, and this button tracks it */
    .play-again { margin-top: 28px; padding: 13px 26px; font-size: 16px; }

    /* ---- DEBUG BAR ---- */
    .debug-bar { padding: 5px 10px; font-size: 9px; gap: 8px; }
    .debug-bar #dbg-info {
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .debug-bar button { font-size: 9px; }
  }

  /* Very narrow phones (Galaxy Fold, older 360px Androids) */
  /* =================================================================
     THE MIDLINE RUNS OUT  (<= 570px)
     The bonus and the percentage start on the card's midline, which is a
     floor of the card less 87px. Below about 567 that floor is narrower
     than the 135px the title draws, so it stops governing and the two runs
     simply follow whatever they sit behind -- one at 135, the other at 97,
     which reads as neither aligned nor deliberate.

     So they range right instead, on the card's own edge. It is an edge the
     card can always give them and it is the same edge for both.
     ================================================================= */
  /* =================================================================
     DRAFT BOARD, MOBILE (880px and down) -- 14 September, from the user's
     reference screens. Two views of one board, switched by #game.is-picking
     (openPicker / closePicker):
       roster  -- the seven slots; an empty one reads "Select quarterback" with a
                  chevron, a filled one POS | name | matchup | clear.
       picker  -- the pool in the roster's place, POS | player | age | matchup,
                  opened by a slot tap and closed by a pick or ‹ Roster.
     Type and columns are the desktop board's: position 22px Anton, name 18px
     Newsreader, age and matchup 13.5px Inter, 56px rows. The tracks are
     narrower -- 92px holds "PHI VS DAL" at 13.5px -- and a long name ellipses.

     AFTER the early (max-width: 700px) block on purpose: that block restyles
     .slot and .pool-row, and media queries carry no specificity, so this one has
     to come later to win.
     ================================================================= */
  @media (max-width: 880px) {
    #game .pool { display: none; }
    #game.is-picking .pool { display: flex; }
    #game.is-picking .roster { display: none; }

    /* Sized to the screen (fitMobileBoard sets --m-row): the progress line and
       Kickoff stay on screen under either view, so the bar's 40px tail goes. */
    .submit-bar { margin-bottom: 12px; }

    /* The two heads are one height, so the lineup and the picker in its place
       are one height too, and switching between them moves nothing. */
    .roster-head, .pool-head {
      box-sizing: border-box;
      height: 48px;
      padding-top: 0;
      padding-bottom: 0;
      align-items: center;
    }

    .roster-cols, .pool-cols {
      display: grid;
      align-items: center;
      column-gap: 10px;
      padding: 7px 12px;
      background: var(--cream);
      border-bottom: 1px solid var(--rule);
      font-family: 'Inter', system-ui, sans-serif;
      font-size: 11px;
      letter-spacing: 0.14em;
      text-transform: uppercase;
      color: var(--muted);
    }

    .slot, .pool-row {
      box-sizing: border-box;
      height: var(--m-row, 56px);
      min-height: 0;
      padding: 0 12px;
      column-gap: 10px;
      align-items: center;
    }

    /* ---- roster ---- */
    .roster-cols,
    .slot { grid-template-columns: 48px minmax(0, 1fr) 92px 30px; }
    .slot { display: grid; row-gap: 0; }
    /* The row is a door to the picker, not a selection, so nothing stays gold.
       #roster for the specificity: the (hover: none) block further down puts
       .slot.active:hover back to gold, and a phone's tap leaves :hover stuck on
       the row just tapped. */
    #roster .slot.active, #roster .slot.active:hover { background: none; }
    .slot.active .pick .meta, .slot.active .mu-opp { color: inherit; }
    /* FLEX drafted with slots still open (renderRoster's needs-fill): the open
       slots take the results page's score-bar gold (.res-bar-mid) until each is
       filled. After the .active rule above, at its specificity, so it wins. */
    #roster .slot.needs-fill, #roster .slot.needs-fill:hover { background: rgba(var(--gold-rgb), 0.40); }
    .slot .pos { width: auto; font-size: 22px; line-height: 1.2; }
    .slot .pick .meta { display: none; }
    .slot .pick .player-name { font-size: 18px; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .slot .slot-matchup {
      display: block;
      font-family: 'Inter', system-ui, sans-serif;
      font-size: 13.5px;
      line-height: 1.2;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      text-align: left;
      white-space: nowrap;
    }
    .slot .mu-opp { color: var(--muted); }
    /* Desktop's "— empty —": the name's size, italic, muted. */
    .slot .pick.empty {
      grid-column: 2 / 4;
      font-family: 'Newsreader', Georgia, serif;
      font-style: italic;
      font-size: 18px;
      line-height: 1.2;
      color: var(--muted);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .slot .slot-chev {
      display: block;
      justify-self: center;
      font-size: 26px;
      line-height: 1;
      color: var(--muted);
    }
    .slot .clear { width: 30px; height: 30px; font-size: 15px; }

    /* ---- picker ---- */
    .pool { min-height: 0; }
    /* The picker's head is the lineup's: ink, paper type, the same size and
       spacing (.roster-head's 20px, 17px from the <=700 block). */
    .pool-head {
      justify-content: flex-start;
      gap: 12px;
      padding-left: 0;
      padding-right: 12px;
      background: var(--ink);
      color: var(--paper);
    }
    /* Right-justified to the left edge of the list's scrollbar, by request: the
       head's right padding IS the scrollbar's measured width (--pool-scrollbar,
       syncPoolScrollbar), 15-17px on a desktop browser. A phone's overlay
       scrollbar measures 0, and then it is the 14px floor instead, so the title
       never runs into the border. */
    .pool-head { padding-right: max(var(--pool-scrollbar, 0px), 14px); }
    .pool-head h2 { color: var(--paper); letter-spacing: 0.1em; margin-left: auto; text-align: right; }

    /* Both heads 10% up, by request -- 20px to 22 here, 17 to 19 below 700 --
       with ‹ LINEUP from 15 to 17. The count goes with its title, so the lineup
       head stays one size. 19 and 17 were 18.7 and 16.5 until 17 September,
       rounded to whole pixels so the type is not soft on a 1x screen. */
    .roster-head, .roster-head .roster-count, .pool-head h2 { font-size: 22px; }
    @media (max-width: 700px) {
      .roster-head, .roster-head .roster-count, .pool-head h2 { font-size: 19px; }
    }
    /* YOUR LINEUP starts where POS and the rows' positions do: the head's left
       padding is the columns' and rows' 12px. (Tablets only until 17 September;
       a phone's was 14, 2px off.) */
    .roster-head { padding-left: 12px; }
    .pool-head .filter { display: none; }
    /* Styled as the desktop board's active tab, which the user pointed at: a
       rust block in the ink bar. It fills the head from the panel's top border
       to the head's bottom line, flush to the panel's left border, so the ink
       frames it on three sides; no border of its own, even padding either side. */
    .pool-back {
      display: inline-flex;
      align-items: center;
      align-self: stretch;
      margin: 0;
      padding: 0 14px;
      background: var(--rust);
      border: none;
      color: var(--paper);
      font-family: 'Anton', sans-serif;
      font-size: 17px;
      line-height: 1;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      cursor: pointer;
    }
    .pool-cols,
    .pool-row { grid-template-columns: 36px minmax(0, 1fr) 28px 92px; }
    /* Over the rows' columns even when the list shows a scrollbar
       (--pool-scrollbar, syncPoolScrollbar). */
    .pool-cols { padding-right: calc(12px + var(--pool-scrollbar, 0px)); }
    /* Nudged 3px left, heading and cells together, by request. */
    .pool-row .pool-age,
    .pool-cols span:nth-child(3) { position: relative; left: -3px; }
    /* Exactly the lineup's seven rows tall, scrolling inside itself a row at a
       time, so the picker is the lineup's height and its lines are the lineup's. */
    .pool-list {
      height: calc(var(--m-row, 56px) * 7);
      max-height: none;
      min-height: 0;
      overflow-y: auto;
      scroll-snap-type: y mandatory;
    }
    .pool-row { scroll-snap-align: start; scroll-snap-stop: always; }
    .pool-row .pool-pos,
    .pool-row .pool-age { display: block; line-height: 1.2; }
    .pool-row .pool-pos { font-family: 'Anton', sans-serif; font-size: 22px; letter-spacing: 0.06em; }
    .pool-row .pool-who { justify-content: center; }
    .pool-row .nm { font-size: 18px; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .pool-row .meta { display: none; }
    .pool-row .pool-age { font-family: 'Inter', system-ui, sans-serif; font-variant-numeric: tabular-nums; font-size: 13.5px; letter-spacing: 0.08em; color: var(--muted); }
    .pool-row .pool-matchup { font-size: 13.5px; line-height: 1.2; letter-spacing: 0.08em; text-align: left; }

    /* ---- TABLETS (701-880px) ----
       LAST in this block, and it has to be: it overrides the type and tracks
       above at their own specificity, so source order decides. Placed higher, the
       pool's rows kept the phone sizes while the lineup's grew (found 15
       September).

       The phone board scaled up where a tablet has the room, by request: the
       row type 25% larger (22 -> 27.5, 18 -> 22.5, 13.5 -> 16.9) with tracks
       widened to hold it, the column heads 50% taller with type to match, and
       ‹ LINEUP at the title's 22px.

       The progress line is 50% larger (12 -> 18px) and so takes a row of its
       own above Kickoff. At 701px the content is 637px wide and the short
       wording alone is ~420px at 18px: beside a 208px Kickoff it would wrap,
       and the line beside Kickoff never wraps. Stacked, it cannot. */
    @media (min-width: 701px) {
      .pool-back { font-size: 22px; }

      .slot .pos, .pool-row .pool-pos { font-size: 27.5px; }
      .slot .pick .player-name, .slot .pick.empty, .pool-row .nm { font-size: 22.5px; }
      .slot .slot-matchup, .pool-row .pool-matchup, .pool-row .pool-age { font-size: 16.875px; }
      .roster-cols, .slot { grid-template-columns: 60px minmax(0, 1fr) 116px 36px; }
      .pool-cols, .pool-row { grid-template-columns: 46px minmax(0, 1fr) 36px 116px; }
      .slot .clear { width: 36px; height: 36px; font-size: 18px; }

      .submit-bar {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        column-gap: 12px;
        row-gap: 12px;
        align-items: stretch;
      }
      .submit-bar .progress { grid-column: 1 / -1; font-size: 18px; text-align: center; }
      .submit-btn { width: 100%; }

      /* Column heads 50% taller (~29 -> 44px) and their type 50% up (11 ->
         16.5px), by request. Letter-spacing eased from 0.14em so AGE still fits
         its 36px track. fitMobileBoard measures the heads, so the rows give up
         the height and Kickoff stays on screen. */
      .roster-cols, .pool-cols {
        box-sizing: border-box;
        height: 44px;
        padding-top: 0;
        padding-bottom: 0;
        font-size: 16.5px;
        letter-spacing: 0.1em;
      }

      /* AVAILABLE ... starts where the Age column's text does, by request. Age
         text sits 177px in from a row's right edge -- padding 12 + matchup 116 +
         gap 10 + age 36, plus the 3px nudge left -- and a row stops short of the
         head by the list's scrollbar. So the title is that wide, pushed right. */
      .pool-head { padding-right: 0; }
      .pool-head h2 {
        flex: none;
        width: calc(var(--pool-scrollbar, 0px) + 177px);
        text-align: left;
        white-space: nowrap;
      }
    }

    /* ---- PHONES (700px and down) ----
       Position and name 1px down, by request (22 -> 21, 18 -> 17), in the lineup
       and the picker alike; the empty slot's placeholder goes with the name it
       stands in for. LAST in this block, like the tablet one above, so it comes
       after the 22 / 18 it overrides. */
    @media (max-width: 700px) {
      .slot .pos, .pool-row .pool-pos { font-size: 21px; }
      .slot .pick .player-name, .slot .pick.empty, .pool-row .nm { font-size: 17px; }

      /* More room for the name, by request (17 September): the columns right of
         it move over. At 320 the lineup's name goes 68 -> 78px and the picker's
         82 -> 92, and fitPlayerNames shortens whatever still does not fit.

         The matchup itself cannot move right on either view: the widest (WAS VS
         HOU, 101px) already ends 3px inside the picker's row and 1px short of the
         lineup's ×. So it stays, and the room comes from the left of it.

         Picker: position 36 -> 32 (WR, the widest, is 26px), gaps 10 -> 8, right
         padding 12 -> 4 with the matchup track 92 -> 100 to keep its text where
         it was; and age is right-aligned in its track (the 3px nudge left goes),
         which moves it ~14px right, 8px short of the matchup.

         Lineup: position 48 -> 42 (FLEX is 40.6px) and gaps 10 -> 8, the matchup
         track taking 2 of it so its text stays put. */
      .roster-cols,
      .slot { grid-template-columns: 42px minmax(0, 1fr) 94px 30px; column-gap: 8px; }
      .pool-cols,
      .pool-row { grid-template-columns: 32px minmax(0, 1fr) 28px 100px; column-gap: 8px; }
      .pool-row { padding-right: 4px; }
      .pool-cols { padding-right: calc(4px + var(--pool-scrollbar, 0px)); }
      .pool-row .pool-age,
      .pool-cols span:nth-child(3) { left: 0; text-align: right; }
    }
  }

  /* Every line on the draft board that starts on the left starts where the
     banner's year does (by request, 26 September): the pick row's name, YOUR
     LINEUP, the POS head and the rows' positions. --board-inset is the banner's
     own left padding (24px, 14 at 700 and down); the lineup's panel has the
     banner's 2px border, and the pick row has none, so it takes 2px more. After
     the desktop and mobile board blocks, and #game for the weight, so it wins
     over their 16 and 12. */
  #game { --board-inset: 24px; }
  @media (max-width: 700px) { #game { --board-inset: 14px; } }
  #game .roster-head, #game .roster-cols, #game .slot { padding-left: var(--board-inset); }
  #game .board-turn { padding-left: calc(var(--board-inset) + 2px); }

  /* Each player's age centred under AGE, at every width (by request, 26
     September) -- it was left of it on tablets and right-aligned on phones.
     Both are centred in the same track, and each gets its own letter-spacing
     back on the left, because the spacing trails the last letter and would pull
     the letters left of centre by half of it. */
  #game .pool-cols span:nth-child(3),
  #game .pool-row .pool-age { text-align: center; }
  /* The box itself too: on tablets AGE (38px) is wider than its 36px track,
     and text that overflows its box is set from the left, not centred. */
  #game .pool-cols span:nth-child(3) { justify-self: center; }
  #game .pool-cols span:nth-child(3) { padding-left: 0.14em; }
  #game .pool-row .pool-age { padding-left: 0.08em; }
  @media (min-width: 701px) and (max-width: 880px) {
    #game .pool-cols span:nth-child(3) { padding-left: 0.1em; }
  }

  /* A taken player's note (by request, 26 September) is the matchup cell, so it
     keeps the age and matchup's size on every device -- 13.5px, 16.875 on
     tablets -- rather than the name's. No letter-spacing, which bought ROUND back
     for short names. fitPoolNotes goes to RD (phones only), then to two lines,
     as room runs out. */
  #game .pool-row.is-unavailable .pool-note { line-height: 1.2; letter-spacing: 0; }

  @media (max-width: 570px) {
    /* No floor, and the bonus and the percentage stay on the left as they do at
       every width (see .sb-card-score): no auto margin pushing them right. */
    .sb-card-score .sb-label > span:first-child,
    .sb-card-score .sb-value:not(.sb-pct) { min-width: 0; }

    /* The number goes and the stopwatch opens it. 100px of title plus 5 of gutter
       plus a 16px icon is 121 of the 134 available; the number would have wanted
       another 44 and took the whole line for itself. Padding for the finger,
       negative margin to give the space straight back, so the label line does not
       move for a tap target. */
    /* -8 on all four sides gives back the padding that makes the icon a
       finger-sized target, so the tap target moves nothing -- and no auto margin,
       since the bonus sits on the card's left now.

       top: 0 undoes the nudge the bonus carries elsewhere. That one drops a run
       of 0.8em text onto the title's baseline, and there is no text here -- the
       number is behind the icon at this width -- so it was pushing the stopwatch
       0.09em below the middle of a line it is the only other thing on. */
    .sb-bonus {
      padding: 8px;
      margin: -8px;
      top: 0;
    }
    .sb-bonus b { display: none; }
    .sb-bonus.is-open b {
      display: block;
      position: absolute;
      top: calc(100% + 6px);
      /* From the bonus's left edge, not centred on it: the bonus sits at the
         card's left, and centred, the popover hung off the card. */
      left: 0;
      z-index: 5;
      background: var(--paper);
      color: var(--ink);
      font-family: 'Inter', system-ui, sans-serif;
      font-size: 13px;
      letter-spacing: 0.06em;
      text-transform: uppercase;
      white-space: nowrap;
      padding: 7px 11px;
      border-radius: 9px;
      box-shadow: 0 3px 12px rgba(0, 0, 0, 0.28);
    }
    /* The popover says what the number is. The label it replaces is display:none
       at this width, so there is nothing on screen to read it off. */
    .sb-bonus.is-open b::before { content: 'Time bonus '; }
  }

  /* =================================================================
     NARROW PHONE  (<= 470px)
     Below this the score card stops holding its own contents: the bonus
     drops off the title's line and the percentage off the figure's. A 390px
     phone gives the card 134px inside its padding, and 137.0 alone drew 92 of it.
     ================================================================= */
  @media (max-width: 470px) {
    /* As large as the card can hold rather than a size per breakpoint. The two
       numbers and their gutter come to about 3.9 times the type, so the card can
       carry roughly an eighth of the viewport: 32px at 390, 36 by 470 where the
       cap takes over, 26 at the floor. Measured on the widest each can be --
       137.0 against a round that hits the ceiling and prints 100%. */
    .sb-value { font-size: clamp(26px, 8.2vw, 36px); }

    /* No floor here, and the bonus and the percentage stay on the left, as they
       do at every width (see .sb-card-score). */
    .sb-card-score .sb-label > span:first-child,
    .sb-card-score .sb-value:not(.sb-pct) { min-width: 0; }

    /* The reveal's matchup with both scores, "WAS 48 VS NYG 45", drew ~112px
       at 0.08em against 102 of room at 375. Without the letter-spacing it is
       ~99, and it stays on one line. */
    .reveal-pair .player-info .opp-line { letter-spacing: 0; }
  }

  /* Below 375 the scores go, by decision, and the matchup reads DAL VS TEN as it
     did before them. Not at the 360 step: the widest pair, "WAS 44 VS HOU 44",
     is 99.7px against 95 of room at 361, and first fits at about 370. 375 has
     102. Measured over every pair of team codes. */
  @media (max-width: 374px) {
    .reveal-pair .opp-line .mu-score { display: none; }
  }

  @media (max-width: 360px) {
    /* YOUR LINEUP draws 100px of the 103 this card has inside its padding, which
       leaves nothing for the stopwatch and drops it onto a line of its own -- and
       an icon on its own line cannot be centred against a title it is no longer
       beside. The budget is 82 once the 16px icon and its 5px gutter are out:
       11.5px came to 82.2 and lost the line by two tenths, 11px comes to 78.6. */
    .sb-label { font-size: 11px; }
    /* The tap padding goes vertical-only here. It counts toward the line even
       though the negative margin gives the space back -- flex breaks the line on
       the outer size, and 8px each side made the button 24 wide against the 19
       left after the title, which put the icon on a line of its own again. The
       target keeps its 32px height and takes the icon's 16 in width. */
    .sb-bonus { padding: 8px 0; margin: -8px 0; }
    main { padding-left: 12px; padding-right: 12px; }
    .matchup .value { font-size: 26px; }
    .matchup .timer { font-size: 26px; }
    .matchups-grid { grid-template-columns: 1fr; }
    .reveal-row { grid-template-columns: 40px 1fr auto; gap: 8px; padding-left: 10px; padding-right: 10px; }
    .reveal-row .slot-label { font-size: 19px; }
    .reveal-row .points { font-size: 23px; min-width: 40px; }
    /* REVEAL SIDE BY SIDE */
    /* 88px widened by 10%. */
    .reveal-pair-head,
    .reveal-row.reveal-pair { grid-template-columns: minmax(0, 1fr) 96.8px minmax(0, 1fr); gap: 5px; }
    .reveal-row.reveal-pair::before { width: 96.8px; }
    .reveal-pair .slot-label { font-size: 14px; }
    .rv-center { padding-top: 5.4px; }
    .reveal-pair .points { font-size: 19px; min-width: 1.75em; }
    /* (100% - 96.8px) / 2 - 5px gap. */
    .reveal-row.reveal-pair.top-pick { --rv-fade-end: calc(50% - 53.4px); }
    .home-logo-img { max-width: 250px; }
  }

  /* Touch devices: :hover styles latch on after a tap and read as a stuck
     selection, so neutralize the ones that change background or colour. */
  @media (hover: none) {
    .slot:hover { background: none; }
    .slot.active:hover { background: var(--gold-light); }
    .pool-row:hover { background: none; }
    .menu-item:hover { color: var(--paper); background: none; }
    .menu-cta:hover { background: none !important; }
    .brand-link:hover { opacity: 1; }
    .menu-toggle:hover span { background: var(--ink); }
    .home-play:hover:not(:disabled) { background: var(--ink); }
    /* :not(:active) so a latched hover is neutralised without swallowing the
       press feedback, which is the only feedback a touch device gets */
    .submit-btn:hover:not(:active):not(:disabled) { background: var(--rust); }
    .sb-action:hover { background: var(--cream); }
    .play-again:hover { background: var(--ink); }
    .slot .clear:hover { background: none; color: var(--ink); }
    .matchup-action.is-light:hover:not(:disabled) { background: var(--paper); }
    .matchup-action.is-dark:hover:not(:disabled) { background: var(--gray-dark); }
  }

  /* Kill the blue box iOS and Android (Samsung Internet most visibly) paint
     over whatever is tapped -- on the whole page, since 26 September: it was a
     list of classes, and every button left off it (Versus's, Unlimited's, the
     grey and rust action buttons) still flashed blue on a Samsung phone. The
     property inherits, so html covers everything. Buttons keep their own
     :active and :hover feedback. */
  html { -webkit-tap-highlight-color: transparent; }
  /* And a tap is not a keyboard: a button a finger focused shows no ring, a
     button reached with Tab still does (:focus-visible). */
  button:focus:not(:focus-visible), a:focus:not(:focus-visible) { outline: none; }

  /* =================================================================
     RESULTS
     The record of a finished round, which is what you come back to
     once the reveal has had its moment. Same panel language as the
     scoreboard and the leaderboard: paper card, cream inset, ink text,
     gold for a perfect pick.
     ================================================================= */
  .res-panel {
    max-width: 560px;
    margin: 0 auto;
    padding: 22px 20px 20px;
    background: var(--paper);
    border: 2px solid var(--ink);
    border-radius: 20px;
    text-align: center;
  }
  .res-streak {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 16.5px;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--muted);
  }

  /* One marker per slot: a trophy where you found the best player at that
     slot, otherwise a dot coloured by how close you came. */
  .res-marks {
    display: flex;
    align-items: center;
    justify-content: center;
    /* Half again on everything here, markers and the space between them, so the
       row scales rather than just gaining fatter dots. */
    gap: 15px;
    min-height: 30px;
  }
  .res-dot { width: 21px; height: 21px; border-radius: 50%; display: block; }
  .res-mark-trophy { width: 28px; height: 28px; display: block; color: var(--gold-deep); }
  .res-good  { background: var(--green); }
  .res-mid   { background: var(--gold); }
  .res-poor  { background: var(--rust); }

  /* Slot bars */
  /* First child of the panel now, so it takes no top margin of its own -- the
     slot above supplies the gap to the score cards. */
  .res-bars { margin-top: 0; text-align: left; }
  /* The same two columns as .res-actions below it, so each half sits over its
     own button -- 1fr and 1fr either side of the same 10px gap put the two
     grids on identical tracks. Centred against each other rather than
     top-aligned: the markers are one 30px row against the date's two lines.

     auto-fit rather than a plain 1fr 1fr, because seven markers at this size
     measure 244px and cannot be squeezed into half of a phone: below the 498px
     of panel that two 244px tracks need, the columns become rows and both
     halves centre on the panel instead. That is a measurement, not a
     breakpoint, so it moves when the marker sizes do. */
  .res-meta {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(244px, 1fr));
    gap: 14px 10px;
    align-items: center;
    /* Collapses against .res-actions' own 20px below, so the row sits in 24px of
       air on both sides rather than 16 above and 20 below. */
    margin: 24px 0;
  }
  .res-bar {
    position: relative;
    display: flex;
    /* Baseline, not center. The row runs Newsreader against Anton, and centring
       lines up their BOXES -- which left the name and the position sitting 2.5
       and 4px below the rank and the score, because Anton's cap height fills far
       more of its em than Newsreader's does. Sitting them on one baseline is
       what makes a line of mixed faces read as level.

       Baseline gets the row to within a pixel of centred on its own: measured as
       cap-band centres against the row's own centre, the rank, the score and the
       trophy all land on 0, the position 0.5 low and the name 1 low. Anton's
       normal line box happens to be symmetric about its caps, which is why the
       Anton items need nothing. .res-bar-name takes the last pixel below. */
    align-items: baseline;
    gap: 9px;
    padding: 9px 13px;
    border-radius: 10px;
    background: var(--cream);
    overflow: hidden;
  }
  .res-bar + .res-bar { margin-top: 7px; }
  /* The fill is a tint rather than a solid, so one ink text colour stays
     legible whether it lands on the fill or on the bare track. */
  .res-bar-fill {
    position: absolute;
    inset: 0 auto 0 0;
    border-radius: 10px 0 0 10px;
  }
  .res-bar-good .res-bar-fill { background: rgba(var(--green-rgb), 0.34); }
  .res-bar-mid  .res-bar-fill { background: rgba(var(--gold-rgb), 0.40); }
  .res-bar-poor .res-bar-fill { background: rgba(var(--rust-rgb), 0.26); }
  .res-bar > *:not(.res-bar-fill) { position: relative; }
  /* Slot, name and score all run at one size in one colour. The slot was
     11px and muted, the name 13px, the score 17px -- three ranks where the
     row only has one thing to say. Widened to 52px with it: FLEX at 18px
     mono needs about 50, and 40 would have clipped it. */
  .res-bar-slot {
    flex-shrink: 0;
    /* Measured at each size rather than scaled from one: Anton is condensed, so
       FLEX renders 36px here against the 50 Newsreader wanted, and the width it
       gives back goes to the name. Centred, so QB and FLEX sit over the same
       axis instead of ranging left. */
    width: 36px;
    text-align: center;
    font-family: 'Anton', sans-serif;
    font-size: 16px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink);
  }
  .res-bar-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: 'Newsreader', Georgia, serif;
    font-size: 18px;
    color: var(--ink);
  }
  /* An icon has no baseline, so it centres on the row instead and is nudged to
     sit with the text rather than with the box. */
  .res-bar-crown-icon {
    width: 20px; height: 20px;
    flex-shrink: 0;
    /* 10px further left than the row's gap alone would put it. Everything to
       its right is pinned to the row's right edge, so the margin moves the
       trophy, not the columns after it -- the name gives up the space. */
    margin-right: 10px;
    align-self: center;
    color: var(--gold-deep);
    display: block;
  }
  .res-bar-val {
    flex-shrink: 0;
    /* Fixed and right-aligned, like the rank. Without it the score's own width
       -- 8.1 against 30.1 -- shifted the rank column about from row to row, so
       a right-aligned rank still did not line up.

       36px, not the 52 it started at. The highest single-week score in the
       whole dataset is 62.1, so four characters is the ceiling and the rest
       was width taken off the name for nothing. */
    /* 33px: past the 32.55 the widest two-digit score draws. At 31 it was under
       it, so a row like 25.7 grew its own box while 13.0 did not -- Anton sets 1
       at 5.95px against 8.89 for every other digit -- and the rank column in
       front of it moved with the difference. That is why the ranks ranged right
       on a phone, where 28px clears every score, and not on a desktop. */
    min-width: 33px;
    text-align: right;
    font-family: 'Anton', sans-serif;
    font-size: 18px;
    letter-spacing: 0.02em;
    color: var(--ink);
    /* Laid out as two parts rather than one number. font-variant-numeric:
       tabular-nums was the obvious answer and it does nothing here -- Anton
       as served carries no tnum feature, and 1111 measures the same width
       with it as without. So the fraction gets a fixed box and the integer
       is pushed up against it, which puts the point at the same offset from
       the right edge on every row whatever the digits happen to be. */
    display: flex;
    justify-content: flex-end;
    /* Stated, not left to the default stretch. A flex container hands the row its
       first item's baseline only when that item has one to give; stretched, it
       synthesises the baseline from the box instead. On a full row the rank sat at
       the same height and hid it, but on a No pick row the rank is empty and the
       score dropped 2px below everything beside it. */
    align-items: baseline;
  }
  /* Where the pick finished at its position that week. Same size and colour as
     everything else on the row -- it is part of the reading, not a footnote to
     it. */
  .res-bar-rank {
    flex-shrink: 0;
    /* Right-aligned, so 1st, 17th and 105th all end on the same edge -- with
       tabular figures the digits keep one width, so only the suffix moves the
       left edge.

       35px is 48th at this size, the two-digit case, not 105th. It is a FLOOR,
       and the item does not shrink, so a three-digit rank grows its own box and
       only that row pays for it. Floored at the three-digit width instead, every
       row paid: truncation of real player names on a 375px screen went from 1.8%
       to 7.1%, against 3.2% this way. */
    min-width: 35px;
    text-align: right;
    /* Inter, which is what this design uses for labels and meta (DM Mono until
       16 September), with tabular figures so the digits hold one width. */
    font-family: 'Inter', system-ui, sans-serif;
    font-variant-numeric: tabular-nums;
    font-weight: 400;
    /* On top of the row's 9px, so the rank stands off the score rather than
       running into it -- and the trophy and the rank move left together, since
       the name absorbs whatever the right-hand columns give back. */
    margin-right: 8px;
    font-size: 14px;
    letter-spacing: 0.02em;
    color: var(--ink);
    /* Measured with DM Mono, which sat its caps lower against a shared baseline than the Anton and
       Newsreader either side of it, which put the rank 2px below the middle of
       the bar while every other column measured 0. In em, so the correction
       follows the type down through the breakpoints instead of needing one
       number per size. */
    top: -0.125em;
  }
  /* .res-bar-fill is absolutely positioned, and a positioned element paints
     ABOVE static in-flow content -- so the tint was going over the text, not
     under it. It only looked survivable because the fill is translucent; the
     labels were reading through a wash of gold or green. Positioning the text
     puts it back on top, where it was meant to be. */
  .res-bar > :not(.res-bar-fill) { position: relative; }
  .res-bar-val i {
    font-style: normal;
  }
  .res-bar-val b {
    font-weight: inherit;
    /* Every fraction but .1 measures 13.73px at 18px and .1 only 10.8, so the
       box has to clear the wider one or the point still drifts -- 13px left it
       0.7px out, which is the same bug one order smaller. In em so it holds at
       16px and 12px without a rule apiece. */
    min-width: 0.78em;
    text-align: left;
  }
  /* Newsreader's line box is not symmetric about its caps the way Anton's is, so
     on a shared baseline the name sits a pixel below everything else in the row.
     Measured on the cap band -- ink top down to the baseline, descenders left
     out, because centring a name on its ink would move 'Rodgers' and leave
     'Foster' where it was and the column would jitter down the card. Holds at
     18px and at 16px; at 13px the offset is 0.5 either way and there is nothing
     left to correct. */
  .res-bar-name { top: -1px; }
  /* Anton at the slot size sits half a pixel below its own caps for the same
     reason, on every width measured. */
  .res-bar-slot { top: -0.5px; }
  .res-bar-empty .res-bar-name { color: var(--muted); font-style: italic; }

  /* Actions */
  .res-actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin-top: 20px;
  }
  .res-btn {
    padding: 13px 16px;
    border-radius: 14px;
    border: 2px solid var(--ink);
    font-family: 'Anton', sans-serif;
    /* 15px + 15%. */
    font-size: 17.25px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 0.12s, color 0.12s;
  }
  .res-btn-share { background: var(--cream); color: var(--ink); }
  .res-btn-share:hover { background: var(--cream-hover); }
  .res-btn-board { background: var(--ink); color: var(--paper); }
  .res-btn-board:hover { background: var(--rust); }
  /* Unlimited rounds, where Share Score and Leaderboard are hidden: Play Again
     takes Leaderboard's column as well as its look. Pinned to the second
     track, because auto-placement would slide it into the first the moment
     Share Score stops occupying it. */
  .res-btn-again { grid-column: 2; }
  /* Quiet, under the two buttons that matter. Anton would shout; this is an
     alternative route, not a third action. */
  .res-social {
    display: flex;
    justify-content: center;
    align-items: baseline;
    gap: 8px;
    margin-top: 14px;
    /* Centred on Share Score rather than on the panel. Shifted rather than
       narrowed to the button's width: .res-actions is two 1fr columns with a
       10px gap, so the left column's centre sits a quarter of the panel plus
       half the gap to the left of the panel's own centre, and translateX takes
       its percentage from this element -- which is full width, so the two are
       the same number. Narrowing the row to calc(50% - 5px) lands in the same
       place and wraps it onto two lines at 375px, and four while the copy note
       is up.

       The second term is a floor. This row is 250px of text against a button
       131px wide at 320px, so the full shift puts its first letter 8px outside
       the card. 85px is half the row, making -50% + 85px the furthest it can go
       with its left edge still on the panel's -- max() takes whichever of the
       two is the smaller move. Below about 390px that floor is what applies,
       and the row still sits far nearer Share Score than the panel's centre. */
    transform: translateX(max(calc(-25% - 2.5px), calc(-50% + 125px)));
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 11px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--muted);
  }
  .res-social button {
    background: none;
    border: none;
    /* Padding for the finger, negative margin to give the space back to the
       layout, so the row reads exactly as before while X stops being an
       eight-pixel target. */
    padding: 8px 6px;
    margin: -8px -6px;
    font: inherit;
    letter-spacing: inherit;
    /* font: inherit does not carry text-transform, and the user agent stops
       a button inheriting it, so Facebook came out in title case beside an
       uppercase row. Stated rather than inherited. */
    text-transform: uppercase;
    color: var(--muted);
    cursor: pointer;
    border-bottom: 1px solid transparent;
  }
  .res-social button:hover {
    color: var(--ink);
    border-bottom-color: var(--rust);
  }

  .res-empty {
    padding: 30px 10px;
    font-family: 'Newsreader', Georgia, serif;
    font-style: italic;
    color: var(--muted);
  }

  /* =================================================================
     LEADERBOARD
     The scoreboard's card language reused: paper panel, cream inset,
     ink rules, gold for the winner and rust for you. The date is the
     masthead, with an arrow either side to walk the archive.
     ================================================================= */
  .lb-panel {
    max-width: 560px;
    margin: 0 auto;
    padding: 14px;
    background: var(--paper);
    border: 2px solid var(--ink);
    border-radius: 20px;
  }
  .lb-head {
    display: grid;
    grid-template-columns: 40px 1fr 40px;
    align-items: center;
    gap: 8px;
    padding: 6px 0 16px;
  }
  .lb-head-mid { text-align: center; min-width: 0; }
  .lb-date {
    font-family: 'Anton', sans-serif;
    font-size: 26px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    line-height: 1.05;
  }
  /* The day's week, under the date (16 September). The server leaves it off
     today's board until you have played (20260916000001); the line stays open
     so the rows do not hop between a named day and an unnamed one. */
  .lb-week {
    margin-top: 6px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 13.75px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink);
    /* One line of it, measured: 18px. */
    min-height: 18px;
  }
  .lb-panel.is-streaks .lb-week { display: none; }
  .lb-meta {
    margin-top: 6px;
    font-family: 'Inter', system-ui, sans-serif;
    /* 11px + 25%, on both boards (13 September). */
    font-size: 13.75px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--muted);
    /* Held open so the date does not hop when a day has no scores to count */
    min-height: 15px;
  }
  .lb-nav {
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    border-radius: 50%;
    color: var(--ink);
    cursor: pointer;
    transition: background 0.12s, opacity 0.12s;
  }
  .lb-nav:hover:not(:disabled) { background: var(--cream); }
  /* Kept in the layout rather than hidden, so the date stays put as you reach
     either end of the archive. */
  .lb-nav:disabled { opacity: 0.22; cursor: default; }
  .lb-nav svg { width: 20px; height: 20px; display: block; }

  .lb-list {
    background: var(--cream);
    border-radius: 14px;
    overflow: hidden;
  }
  .lb-row {
    display: grid;
    grid-template-columns: 44px 1fr auto;
    align-items: center;
    gap: 10px;
    padding: 11px 16px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 14px;
  }
  .lb-row + .lb-row { border-top: 1px solid rgba(var(--ink-rgb), 0.09); }
  .lb-rank {
    font-variant-numeric: tabular-nums;
    color: var(--muted);
    font-size: 12px;
    letter-spacing: 0.03em;
  }
  .lb-who {
    display: flex;
    align-items: center;
    gap: 7px;
    min-width: 0;
  }
  .lb-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .lb-score {
    font-family: 'Anton', sans-serif;
    font-size: 19px;
    letter-spacing: 0.02em;
  }
  .lb-trophy { width: 15px; height: 15px; flex-shrink: 0; display: block; }

  /* Your own line, wherever it lands */
  .lb-row-you { background: rgba(var(--rust-rgb), 0.13); }
  .lb-row-you .lb-rank,
  .lb-row-you .lb-name,
  .lb-row-you .lb-score { color: var(--rust); }

  /* The winner (16 September): the results bars' 40% gold behind the row, every
     word in ink, and the trophy in a column of its own just left of the score.
     After .lb-row-you and doubled up, so a first-placed You is still gold -- the
     You tag says whose it is. The mobile block restates the four columns. */
  .lb-row.lb-row-first {
    grid-template-columns: 44px 1fr auto auto;
    background: rgba(var(--gold-rgb), 0.40);
  }
  .lb-row.lb-row-first .lb-rank,
  .lb-row.lb-row-first .lb-name,
  .lb-row.lb-row-first .lb-score,
  .lb-row.lb-row-first .lb-trophy { color: var(--ink); }
  /* Trophy 25% up, rank and name 10% up. The mobile block restates the two text sizes. */
  .lb-row.lb-row-first .lb-trophy { width: 18.75px; height: 18.75px; }
  .lb-row.lb-row-first .lb-rank { font-size: 13.2px; }
  .lb-row.lb-row-first .lb-name { font-size: 15.4px; }
  /* Played after the day's restart. Quieter than You: a note, not a badge.
     Two classes, so it beats .lb-tag below whatever the source order. */
  .lb-tag.lb-tag-restart {
    background: transparent;
    color: var(--muted);
    box-shadow: inset 0 0 0 1px rgba(var(--muted-rgb), 0.6);
  }
  .lb-tag-restart .rs-abbr { display: none; }
  @media (max-width: 470px) {
    /* Beside YOU on a phone the word left the name 48px at 375; the glyph
       gives most of that back. The full word stays on title and aria-label. */
    .lb-tag-restart .rs-full { display: none; }
    .lb-tag-restart .rs-abbr { display: inline; }
  }
  .lb-tag {
    flex-shrink: 0;
    font-size: 9px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    padding: 2px 6px;
    border-radius: 6px;
    background: var(--rust);
    color: var(--paper);
  }

  /* Shown when your rank falls outside the page on screen, so the jump from
     10th to 4,182nd reads as a jump and not as a gap in the ranking. */
  .lb-break {
    text-align: center;
    padding: 6px 0 5px;
    color: var(--muted);
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 13px;
    letter-spacing: 0.3em;
    border-top: 1px solid rgba(var(--ink-rgb), 0.09);
  }

  .lb-empty {
    padding: 34px 16px;
    text-align: center;
    font-family: 'Newsreader', Georgia, serif;
    font-style: italic;
    font-size: 15px;
    color: var(--muted);
  }

  .lb-more {
    display: block;
    margin: 14px auto 2px;
    background: var(--cream);
    border: 2px solid var(--ink);
    border-radius: 12px;
    padding: 9px 22px;
    color: var(--ink);
    font-family: 'Anton', sans-serif;
    font-size: 14px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 0.12s;
  }
  .lb-more:hover:not(:disabled) { background: var(--cream-hover); }
  .lb-more:disabled { opacity: 0.5; cursor: wait; }

  /* Dim the list while a different day is loading, rather than blanking it --
     the panel keeps its height and the arrows stay where your cursor is. */
  .lb-panel.is-loading .lb-list { opacity: 0.4; }
  .lb-panel.is-loading .lb-more { visibility: hidden; }

  /* The score column is a percentage, two decimals. Anton has no tabular
     figures, so the whole part and a fixed-width fraction are separate boxes:
     the column is right-aligned, and a fraction of one width puts every
     decimal point on the same axis. In em so it scales with the score's size.
     One decimal since 13 September: the widest fraction, ".0%" (every digit
     but 1 sets as wide), measures 34.95px at 19px (1.84em), so 1.9em holds
     every one. Re-measure if the decimals change -- at two it was 2.4em. */
  .lb-score i { font-style: normal; }
  .lb-score b { font-weight: normal; display: inline-block; min-width: 1.9em; text-align: left; }
  /* The streak board's number and its unit. */
  .lb-unit {
    margin-left: 5px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 10px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--muted);
  }
  /* The streak board is always current, so it has no day to walk to. Hidden
     by visibility rather than display so the title keeps the middle track. */
  .lb-panel.is-streaks .lb-nav { visibility: hidden; }

  /* Share Score and Active Streaks, above Back: the results page's two
     buttons, reused whole. Active Streaks is pinned to the second track so it
     does not slide left whenever Share Score is hidden. */
  .lb-actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    max-width: 560px;
    margin: 16px auto 0;
  }
  .lb-actions .res-btn-board { grid-column: 2; }
  .lb-actions + .play-again { margin-top: 22px; }

  /* =================================================================
     HOW TO PLAY
     Guests only, and deliberately upstream of the prelude -- the clock
     starts in startPrelude(), so this screen costs a player nothing.
     Same card language as everywhere else: paper panel, cream inset
     per rule, Anton numbers over a Newsreader body.
     ================================================================= */
  /* This screen holds the viewport rather than sitting at the top of it. The
     container is deliberately narrow -- narrower than the 1000px that first
     made everything fit -- because narrow is what makes the blocks run tall
     enough to occupy the height. Whatever is still left over is absorbed by
     centring, so there is no dead band under the button.
     73px masthead + 54px of main's padding is the chrome above and below. */
  #howto.active {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    min-height: calc(100vh - 127px);
  }
  .how-panel {
    max-width: 620px;
    margin: 0 auto;
    padding: 14px 30px 18px;
    background: var(--paper);
    border-radius: 20px;
    text-align: center;
  }
  /* The hook borrows the prelude's voice rather than the app's button voice:
     same Newsreader serif and the same clamp-scaled sizing as .prelude-item,
     so arriving at the nostalgia page feels like a continuation. */
  .how-hook {
    font-family: 'Newsreader', Georgia, serif;
    font-size: clamp(26px, 2.8vw, 36px);
    line-height: 1.3;
    font-weight: 700;
  }
  /* One line each at every width, never wrapped (by request, 27 September):
     the heading, the rule titles and the note. fitHowLines steps a line's type
     down until it fits, from the size set here. */
  .how-hook, .how-rule-title, .how-note { white-space: nowrap; }

  .how-rules {
    margin-top: 16px;
    text-align: left;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
  }
  .how-rule {
    display: grid;
    grid-template-columns: 30px 1fr;
    gap: 10px;
    align-content: start;
    /* Vertical padding is 10% over the 14px the blocks started at. */
    padding: 15.4px 18px;
    background: var(--cream);
    border-radius: 14px;
  }
  .how-num {
    font-family: 'Anton', sans-serif;
    font-size: 28px;
    line-height: 1;
    color: var(--rust);
  }
  .how-rule-title {
    font-family: 'Anton', sans-serif;
    font-size: 18px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    line-height: 1.15;
  }
  .how-rule-body {
    margin-top: 4px;
    font-family: 'Newsreader', Georgia, serif;
    font-size: 17px;
    line-height: 1.45;
  }

  /* (2) The one place a guest is told what an account buys them, so the offer
     is made where it is read rather than left to the menu. */
  .how-signin {
    background: none;
    border: none;
    padding: 0;
    font-family: 'Newsreader', Georgia, serif;
    font-size: inherit;
    color: var(--rust);
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
  }
  .how-signin:hover { color: var(--ink); }
  /* The account rule is an invitation, not an instruction, so it steps back
     rather than competing with the three that are actually the game. */
  .how-copy-narrow { display: none; }

  .how-rule-optional .how-num { color: var(--muted); }
  .how-rule-optional .how-rule-body { color: var(--muted); }

  /* Says the quiet part: reading this page is free. The clock is the whole
     tension of the game, so it is worth being explicit about when it starts. */
  main:has(#howto.active) { padding-bottom: 26px; }
  #howto .play-again { margin-top: 18px; }

  /* ---- UNLIMITED ROUNDS ----
     Same side of the line as .ace-* below: declared after the early 700px
     block, so every .unl-* mobile override belongs in the LATE one. Put one
     in the early block and it silently does nothing. */
  .unl-panel {
    max-width: 700px;
    margin: 0 auto;
    padding: 14px 30px 24px;
    background: var(--paper);
    border-radius: 20px;
    text-align: center;
  }
  .unl-hook {
    font-family: 'Newsreader', Georgia, serif;
    font-size: clamp(26px, 2.8vw, 36px);
    line-height: 1.3;
    font-weight: 700;
  }
  .unl-err {
    margin-top: 16px;
    padding: 12px 16px;
    border-radius: 14px;
    background: rgba(var(--rust-rgb), 0.12);
    color: var(--rust);
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 13px;
    line-height: 1.5;
  }
  /* One row of doors: Random first and filled, then the decades. Every tile
     starts a round on the tap -- there is no selected state and no second
     button to press, so the row is actions, not a form. */
  /* Seven since the '70s joined (28 September): All and six decades. */
  .unl-tiles {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 10px;
    margin-top: 22px;
  }
  .unl-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-height: 96px;
    padding: 12px 6px;
    background: var(--cream);
    color: var(--ink);
    border: 2px solid transparent;
    border-radius: 14px;
    cursor: pointer;
    transition: background 0.12s, border-color 0.12s;
  }
  .unl-tile:hover:not(:disabled) { border-color: var(--ink); }
  .unl-tile:disabled { opacity: 0.5; cursor: wait; }
  /* No text-transform: the decades read '80s, and Anton sets 80S as an
     abbreviation shouted -- the same call the ordinal suffix made. */
  .unl-tile-title {
    font-family: 'Anton', sans-serif;
    font-size: 26.4px;
    line-height: 1;
    letter-spacing: 0.04em;
  }
  .unl-tile-sub {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 12.1px;
    letter-spacing: 0.06em;
    color: var(--muted);
  }
  .unl-tile-random { background: var(--rust); border-color: var(--rust); color: var(--paper); }
  /* ALL, at the decades' size and spacing (by request, 27 September; it was
     smaller and wider-set as RANDOM WEEK). */
  .unl-tile-random .unl-tile-title { text-transform: uppercase; }
  .unl-tile-random:hover:not(:disabled) { background: var(--ink); border-color: var(--ink); }
  /* A decade with no seasons in seasons.json yet. Not a loading state, so it
     overrides :disabled's wait cursor and fade rather than wearing them --
     declared after it, at the same specificity, so source order decides. */
  .unl-tile[data-empty] {
    background: transparent;
    border: 2px dashed rgba(var(--ink-rgb), 0.18);
    cursor: default;
    opacity: 1;
  }
  .unl-tile[data-empty] .unl-tile-title,
  .unl-tile[data-empty] .unl-tile-sub { color: var(--muted); }
  /* OR PICK A SPECIFIC WEEK, by request, 15 September: type doubled (12 -> 24)
     and then 25% back down (18), with the space above and below 15% and then
     25% up (24/14 -> 27.6/16.1 -> 34.5/20.1). */
  .unl-rule {
    display: flex;
    align-items: center;
    gap: 14px;
    margin: 34.5px 0 20.125px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 18px;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--muted);
  }
  .unl-rule::before,
  .unl-rule::after { content: ''; flex: 1; border-top: 1px solid rgba(var(--ink-rgb), 0.15); }
  /* RANDOM ERA WEEK over the tiles (by request, 27 September): the rule's own space
     below, as under OR PICK A SPECIFIC WEEK, not the tiles' margin. */
  .unl-rule + .unl-tiles { margin-top: 0; }
  .unl-pick { display: flex; gap: 10px; align-items: flex-end; justify-content: center; }
  .unl-field { display: flex; flex-direction: column; gap: 6px; text-align: left; flex: 1 1 0; min-width: 0; }
  .unl-field span {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 11px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--muted);
  }
  .unl-field select {
    width: 100%;
    background: var(--cream);
    border: 2px solid transparent;
    border-radius: 12px;
    padding: 0 12px;
    font-family: 'Anton', sans-serif;
    font-size: 15px;
    letter-spacing: 0.06em;
    color: var(--ink);
    cursor: pointer;
  }
  .unl-field select:focus { outline: none; border-color: var(--ink); }
  .unl-pick-go {
    flex: 1 1 0;
    background: var(--ink);
    color: var(--paper);
    border: 2px solid var(--ink);
    border-radius: 12px;
    padding: 0 20px;
    font-family: 'Anton', sans-serif;
    font-size: 18px;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 0.12s;
  }
  .unl-pick-go:hover:not(:disabled) { background: var(--rust); }
  .unl-pick-go:disabled { background: var(--muted); border-color: var(--muted); cursor: wait; }

  /* UNLIMITED, DESKTOP (881px and up) -- 15 September, by request. The panel
     widens to the results screen's 800 (.scoreboard, .reveal-list) and the doors
     grow 25%: tiles 96 -> 120px tall with their type to match, and the Season /
     Week / Play type and labels 25% too. Their HEIGHT stays the shared 56px
     action-button rule's, the Back button's, by request -- a 70px row was tried
     and taken back the same day. #unlimited so these beat the plain selectors. */
  @media (min-width: 881px) {
    #unlimited .unl-panel { max-width: 800px; }
    #unlimited .unl-tile { min-height: 120px; padding: 15px 8px; }
    #unlimited .unl-tile-title { font-size: 33px; }
    #unlimited .unl-tile-sub { font-size: 15.1px; }
    #unlimited .unl-field span { font-size: 13.75px; }
    #unlimited .unl-field select { font-size: 18.75px; }
    #unlimited .unl-pick-go { font-size: 22.5px; }
  }

  /* UNLIMITED, TABLET (701-880px) -- 15 September, by request. The headline 50%
     up (its clamp gives 26px here; 39px), the doors three across and two down
     like a phone's, sized up to fill the panel, and the Season / Week / Play type
     25% up as on desktop, at the Back button's 56px height. */
  @media (min-width: 701px) and (max-width: 880px) {
    #unlimited .unl-hook { font-size: 39px; }
    #unlimited .unl-tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
    #unlimited .unl-tile { min-height: 132px; padding: 16px 10px; }
    #unlimited .unl-tile-title { font-size: 40px; }
    #unlimited .unl-tile-sub { font-size: 15px; }
    #unlimited .unl-field span { font-size: 13.75px; }
    #unlimited .unl-field select { font-size: 18.75px; }
    #unlimited .unl-pick-go { font-size: 22.5px; }
  }

  /* ---- ACE / UPGRADE ----
     Declared HERE, after the early 700px block, so its mobile overrides belong
     in the LATE one (~3100). Put an .ace-* rule in the early block and it
     silently does nothing -- see CLAUDE.md on which side of the line each
     component sits. */
  .ace-panel {
    max-width: 700px;
    margin: 0 auto;
    padding: 14px 30px 24px;
    background: var(--paper);
    border-radius: 20px;
    text-align: center;
  }
  .ace-hook {
    font-family: 'Newsreader', Georgia, serif;
    font-size: clamp(26px, 2.8vw, 36px);
    line-height: 1.3;
    font-weight: 700;
  }
  .ace-sub {
    margin-top: 8px;
    font-family: 'Newsreader', Georgia, serif;
    font-size: 17px;
    line-height: 1.45;
    color: var(--muted);
  }
  /* The daily is free and stays free. Said on the page that asks for money,
     because that is the only place the question actually occurs to anybody. */
  .ace-free-note {
    margin-top: 14px;
    padding: 10px 14px;
    background: var(--cream);
    border-radius: 12px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 12px;
    letter-spacing: 0.04em;
    line-height: 1.5;
    color: var(--muted);
    /* Left, against the panel's centre. Centred mono at this measure strands a
       word or two on the last line, and the note is a statement of policy
       rather than a flourish. */
    text-align: left;
  }

  .ace-plans {
    margin-top: 18px;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 10px;
  }
  .ace-plan {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 18px 16px;
    background: var(--cream);
    border-radius: 14px;
    border: 2px solid transparent;
    text-align: left;
  }
  .ace-plan-featured { border-color: var(--gold); }
  .ace-plan-name {
    font-family: 'Anton', sans-serif;
    font-size: 20px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    line-height: 1.15;
  }
  .ace-plan-badge {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 10px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--gold-deep);
  }
  .ace-plan-blurb {
    font-family: 'Newsreader', Georgia, serif;
    font-size: 15px;
    line-height: 1.4;
    color: var(--muted);
    flex: 1;
  }
  /* Deliberately no price. Stripe Checkout renders the real amount from the
     Price object, so there is no second copy of the money here to drift out of
     step with the dashboard. */
  .ace-plan-btn {
    margin-top: 4px;
    background: var(--ink); color: var(--paper);
    border: none;
    border-radius: 14px;
    padding: 12px 18px;
    font-family: 'Anton', sans-serif;
    font-size: 15px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    cursor: pointer;
  }
  .ace-plan-btn:hover:not(:disabled) { background: var(--rust); }
  .ace-plan-btn:disabled { background: var(--muted); cursor: wait; }

  .ace-status {
    margin-top: 16px;
    padding: 14px 18px;
    background: var(--cream);
    border-radius: 14px;
    text-align: left;
  }
  .ace-status-title {
    font-family: 'Anton', sans-serif;
    font-size: 17px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
  }
  .ace-status-body {
    margin-top: 4px;
    font-family: 'Newsreader', Georgia, serif;
    font-size: 16px;
    line-height: 1.45;
    color: var(--muted);
  }
  .ace-err {
    margin-top: 14px;
    padding: 10px 14px;
    background: rgba(var(--rust-rgb), 0.10);
    border-radius: 12px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 12px;
    line-height: 1.5;
    color: var(--rust);
    text-align: left;
  }
  .ace-empty {
    margin-top: 18px;
    font-family: 'Newsreader', Georgia, serif;
    font-style: italic;
    color: var(--muted);
  }

  .how-note {
    margin-top: 12px;
    font-family: 'Inter', system-ui, sans-serif;
    /* 22px less 35%. */
    font-size: 14px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--muted);
  }
  /* Where the numbers come from: a quiet line, in the note's colour. */
  .how-sources {
    margin-top: 10px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 12px;
    line-height: 1.5;
    color: var(--muted);
  }
  .how-sources a { color: inherit; }

  /* =================================================================
     STATS
     Your own record, in the leaderboard's card language: paper panel,
     cream insets, Anton numbers over Inter labels. The chart reuses
     the results page's good/mid/poor scale, so a green bar means the
     same thing here that a green dot means on a finished round.
     ================================================================= */
  .st-panel {
    max-width: 560px;
    margin: 0 auto;
    padding: 14px;
    background: var(--paper);
    border: 2px solid var(--ink);
    border-radius: 20px;
  }
  .st-head { text-align: center; padding: 6px 0 16px; }
  .st-title {
    font-family: 'Anton', sans-serif;
    font-size: 26px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    line-height: 1.05;
  }
  .st-meta {
    margin-top: 6px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 11px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--muted);
    /* Held open so the title does not hop between the empty state and
       the loaded one, same reason as .lb-meta */
    min-height: 15px;
  }

  /* Three across at every width. Rounds / average / best is the headline,
     and stacking it would push the chart under the fold on a phone. */
  .st-tiles {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 8px;
  }
  .st-tile {
    background: var(--cream);
    border-radius: 14px;
    padding: 14px 8px 12px;
    text-align: center;
  }
  .st-tile-value {
    display: block;
    font-family: 'Anton', sans-serif;
    font-size: 30px;
    line-height: 1;
    letter-spacing: 0.01em;
  }
  .st-tile-unit { font-size: 15px; color: var(--muted); }
  .st-tile-label {
    display: block;
    margin-top: 7px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 10px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--muted);
  }

  /* Streaks read as a sentence rather than a number, so they take a row
     under the tiles instead of becoming a fourth one. */
  .st-streaks {
    display: flex;
    justify-content: space-between;
    gap: 10px;
    margin-top: 8px;
    padding: 12px 16px;
    background: var(--cream);
    border-radius: 14px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 11px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--muted);
  }
  .st-streaks b {
    font-family: 'Anton', sans-serif;
    font-size: 15px;
    font-weight: 400;
    letter-spacing: 0.04em;
    color: var(--ink);
  }
  /* A streak still running is the one thing here worth colouring. */
  .st-streak-live b { color: var(--rust); }

  /* The referral counts under the chart: the chart's heading spacing, and
     the streak row's look (.st-streaks, which carries its phone sizes too). */
  .st-refs-head { margin: 18px 0 8px; }

  .st-chart-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin: 18px 0 8px;
  }
  .st-chart-title {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 11px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--muted);
  }
  .st-range { display: flex; gap: 6px; flex-shrink: 0; }
  .st-range-btn {
    background: none;
    border: 1px solid var(--ink);
    border-radius: 8px;
    padding: 4px 10px;
    color: var(--ink);
    font-family: 'Anton', sans-serif;
    font-size: 12px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 0.12s, color 0.12s;
  }
  .st-range-btn[aria-pressed="true"] { background: var(--ink); color: var(--paper); }

  /* One column per calendar day in the window, played or not, so a break
     in a run shows up as a hole instead of being closed up. */
  .st-chart {
    display: flex;
    align-items: flex-end;
    gap: 3px;
    height: 132px;
    padding: 10px 12px;
    background: var(--cream);
    border-radius: 14px;
  }
  .st-col {
    flex: 1 1 0;
    min-width: 0;
    height: 100%;
    display: flex;
    align-items: flex-end;
    border-radius: 3px;
    /* The track for a day with no round. Faint, so the played days carry
       the shape of the chart. */
    background: rgba(var(--ink-rgb), 0.07);
  }
  .st-col-fill {
    width: 100%;
    border-radius: 3px;
  }
  /* A day with no round holds its slot as a baseline nub. It kept the full
     track at first, which at a glance read as a bar at 100% -- the opposite
     of what it means. */
  .st-col-none { background: none; }
  .st-col-none::after {
    content: '';
    width: 100%;
    height: 3px;
    border-radius: 2px;
    background: rgba(var(--ink-rgb), 0.14);
  }
  .st-good .st-col-fill { background: var(--green); }
  .st-mid  .st-col-fill { background: var(--gold); }
  .st-poor .st-col-fill { background: var(--rust); }

  .st-axis {
    display: flex;
    justify-content: space-between;
    margin-top: 7px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 10px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--muted);
  }

  .st-empty {
    padding: 34px 16px;
    text-align: center;
    font-family: 'Newsreader', Georgia, serif;
    font-style: italic;
    font-size: 15px;
    color: var(--muted);
  }

  /* Dimmed rather than blanked while reloading, so the panel keeps its
     height -- same treatment as the leaderboard. */
  .st-panel.is-loading .st-body { opacity: 0.4; }

  /* =================================================================
     ACCOUNT MODALS
     Same card language as the scoreboard: rounded, cream on paper, ink
     borders, Anton headings.
     ================================================================= */
  .modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(var(--ink-rgb), 0.72);
    z-index: 200;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    overflow-y: auto;
  }
  .modal {
    position: relative;
    width: 100%;
    max-width: 420px;
    background: var(--paper);
    border: 2px solid var(--ink);
    border-radius: 20px;
    padding: 28px 26px 24px;
    max-height: calc(100svh - 40px);
    overflow-y: auto;
  }
  .modal-close {
    position: absolute;
    top: 10px;
    right: 14px;
    background: none;
    border: none;
    font-size: 30px;
    line-height: 1;
    color: var(--muted);
    cursor: pointer;
    padding: 0 4px;
    font-family: inherit;
  }
  .modal-close:hover { color: var(--rust); }
  .modal-title {
    font-family: 'Anton', sans-serif;
    font-size: 30px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    line-height: 1.05;
  }
  .modal-sub {
    margin-top: 10px;
    font-size: 15px;
    line-height: 1.45;
    color: var(--muted);
  }
  .modal-err {
    margin-top: 14px;
    padding: 10px 12px;
    border-radius: 10px;
    background: rgba(var(--red-rgb), 0.12);
    border: 1px solid var(--red);
    color: var(--red);
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 12px;
    line-height: 1.4;
  }

  .modal-note {
    margin-top: 14px;
    padding: 12px;
    border-radius: 10px;
    background: rgba(var(--green-rgb), 0.12);
    border: 1px solid var(--green);
    color: var(--green);
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 12px;
    line-height: 1.5;
  }
  .modal-note strong { font-weight: 500; color: var(--ink); }

  /* Provider buttons */
  .oauth-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 100%;
    margin-top: 14px;
    padding: 13px 16px;
    background: var(--cream);
    color: var(--ink);
    border: 2px solid var(--ink);
    border-radius: 14px;
    font-family: 'Anton', sans-serif;
    font-size: 15px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 0.12s;
  }
  .oauth-btn:hover { background: var(--cream-hover); }
  .oauth-btn:disabled { opacity: 0.55; cursor: wait; }
  .oauth-icon { width: 18px; height: 18px; flex-shrink: 0; }
  /* Scaled per mark rather than together: Google 1.25x, Apple 1.68x (1.4 then 1.2). */
  #btn-google .oauth-icon { width: 22.5px; height: 22.5px; }
  #btn-apple .oauth-icon { width: 30.2px; height: 30.2px; }

  .modal-divider {
    display: flex;
    align-items: center;
    gap: 12px;
    /* The Email label's 12px top margin collapses out through the form and
       meets this one, so the visible gap is max(the two). It was 12px; this
       takes it 20% wider. */
    margin: 20px 0 14.4px;
    color: var(--muted);
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 11px;
    letter-spacing: 0.2em;
    text-transform: uppercase;
  }
  .modal-divider::before,
  .modal-divider::after {
    content: "";
    flex: 1;
    height: 1px;
    background: rgba(var(--ink-rgb), 0.18);
  }

  /* Email form */
  .auth-form { display: block; }
  .auth-label {
    display: block;
    margin-top: 12px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 11px;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--muted);
  }
  .auth-input {
    width: 100%;
    margin-top: 5px;
    padding: 11px 12px;
    border: 2px solid var(--ink);
    border-radius: 10px;
    background: var(--paper);
    color: var(--ink);
    /* 16px keeps iOS Safari from zooming the page when the field takes focus */
    font-size: 16px;
    font-family: 'Newsreader', Georgia, serif;
  }
  /* Light enough to read as the name that is already there rather than as
     something typed. --muted on --paper is 4.0:1, which is legible and still
     clearly not ink. */
  .auth-input::placeholder {
    color: var(--muted);
    opacity: 1;
  }
  .auth-input:focus { outline: 2px solid var(--rust); outline-offset: 1px; }
  .auth-submit {
    width: 100%;
    margin-top: 19px;
    padding: 14px 16px;
    background: var(--ink);
    color: var(--paper);
    border: 2px solid var(--ink);
    border-radius: 14px;
    font-family: 'Anton', sans-serif;
    font-size: 21px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 0.12s;
  }
  .auth-submit:hover:not(:disabled) { background: var(--rust); }
  /* The second action under a form's button, grey (by request, 27 September):
     Suggest a random name, under Save and Save Name. */
  .auth-alt {
    width: 100%;
    margin-top: 10px;
    padding: 12px 16px;
    background: var(--cream);
    color: var(--ink);
    border: 2px solid var(--cream);
    border-radius: 14px;
    font-family: 'Anton', sans-serif;
    font-size: 16px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 0.12s, border-color 0.12s;
  }
  .auth-alt:hover:not(:disabled) { border-color: var(--ink); }
  .auth-alt:disabled { opacity: 0.6; cursor: wait; }
  .auth-submit:disabled { background: var(--muted); cursor: wait; }
  .auth-toggle {
    display: block;
    width: 100%;
    margin-top: 14px;
    background: none;
    border: none;
    color: var(--muted);
    font-family: 'Newsreader', Georgia, serif;
    font-size: 14px;
    font-style: italic;
    cursor: pointer;
    text-decoration: underline;
  }
  .auth-toggle:hover { color: var(--rust); }
  /* Two of these stack under the sign-in form -- forgot password, then the
     create-account toggle. 14px each reads as two unrelated links. */
  .auth-toggle + .auth-toggle { margin-top: 8px; }
  /* The sign-in box's two links run 25% over the shared size -- the type, and
     the gaps with it; line-height is normal, so each line follows the type.
     By id, so the reset box's "Back to sign in" keeps 14px. */
  #btn-forgot, #btn-toggle-mode { font-size: 17.5px; margin-top: 17.5px; }
  #btn-forgot + #btn-toggle-mode { margin-top: 10px; }

  /* Account row in the hamburger menu */
  .menu-account {
    padding: 14px 28px;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    margin-top: 8px;
  }
  .menu-account-name {
    font-family: 'Anton', sans-serif;
    font-size: 18px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--paper);
  }
  .menu-account.is-clickable { cursor: pointer; }
  .menu-account.is-clickable:hover .menu-account-name { color: var(--gold); }
  .menu-account.is-clickable:focus-visible {
    outline: 2px solid var(--gold);
    outline-offset: -2px;
  }

  .menu-account-state {
    margin-top: 3px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 11px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(var(--paper-rgb), 0.5);
  }

  @media (max-width: 700px) {
  /* These sit in a SECOND max-width:700px block, after the component CSS
     rather than before it. The main mobile block near the top of this sheet
     is declared ahead of the RESULTS / LEADERBOARD / HOW TO PLAY / STATS
     rules, so at equal specificity the desktop values win on source order and
     its overrides for those screens never apply. */
    /* ---- STATS ---- */
    .st-panel { padding: 10px; border-radius: 16px; }
    .st-head { padding: 4px 0 12px; }
    .st-title { font-size: 22px; }
    .st-tile { padding: 12px 4px 10px; }
    .st-tile-value { font-size: 24px; }
    .st-tile-unit { font-size: 13px; }
    .st-tile-label { font-size: 9px; letter-spacing: 0.1em; }
    .st-streaks { padding: 11px 12px; font-size: 10px; letter-spacing: 0.06em; }
    .st-streaks b { font-size: 14px; }
    .st-chart { height: 108px; padding: 8px; gap: 2px; }
    .st-chart-title { font-size: 10px; letter-spacing: 0.1em; }
    .st-range-btn { padding: 4px 8px; font-size: 11px; }
    .st-axis { font-size: 9px; letter-spacing: 0.06em; }

    /* ---- LEADERBOARD ----
       Moved here from the early 700px block on 14 September, where every one
       had lost to the leaderboard's own rules since they were written. The
       score's fraction box (.lb-score b) is in em, so it follows 17px itself. */
    .lb-panel { padding: 10px; border-radius: 16px; }
    .lb-head { grid-template-columns: 34px 1fr 34px; padding: 4px 0 12px; }
    .lb-date { font-size: 22px; }
    .lb-nav { width: 34px; height: 34px; }
    .lb-nav svg { width: 18px; height: 18px; }
    .lb-row { grid-template-columns: 38px 1fr auto; padding: 10px 12px; font-size: 13px; }
    .lb-row.lb-row-first { grid-template-columns: 38px 1fr auto auto; }
    .lb-row.lb-row-first .lb-rank { font-size: 12.1px; }
    .lb-row.lb-row-first .lb-name { font-size: 14.3px; }
    .lb-rank { font-size: 11px; }
    .lb-score { font-size: 17px; }

    /* ---- UNLIMITED ROUNDS ----
       In this block for the same reason .ace-* is: declared after the early
       700px block, so an override there would lose on source order. */
    .unl-panel { padding: 18px 14px 18px; border-radius: 16px; }
    .unl-hook { font-size: 27.6px; }
    /* Seven across stops fitting before this: 4 + 3 keeps All top-left and
       every tile a thumb's width. */
    .unl-tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-top: 18px; }
    .unl-tile { min-height: 84px; }
    .unl-tile-title { font-size: 24.2px; }
    /* The desktop line's steps at phone scale: 11 -> 22 -> 16.5px, and the room
       above and below 20/12 -> 23/13.8 -> 28.75/17.25. Spacing eased so the 23
       characters keep to one line; centred if a very narrow phone wraps them. */
    .unl-rule { margin: 28.75px 0 17.25px; font-size: 16.5px; letter-spacing: 0.04em; text-align: center; }
    /* The row is a season select, a week select and a button. At 375px the
       week labels run to CONFERENCE CHAMPIONSHIPS and the three cannot share
       a line without the button losing its own words, so the button takes its
       own row underneath and the two selects keep the top one. */
    .unl-pick { flex-wrap: wrap; }
    .unl-field { flex: 1 1 40%; }
    .unl-pick-go { flex: 1 0 100%; padding: 0 20px; }

    /* ---- ACE / UPGRADE ----
       In this block and not the early one: .ace-* is declared after the how-to
       rules, so an override in the early block would lose on source order. */
    .ace-panel { padding: 18px 14px 18px; border-radius: 16px; }
    .ace-hook { font-size: 27.6px; }
    .ace-sub { font-size: 15px; }
    /* Three plan cards side by side on a phone is three words to a line, and
       the lifetime card carries the longest name. One column. */
    .ace-plans { margin-top: 14px; grid-template-columns: 1fr; }
    .ace-plan { padding: 14px 13px; }
    .ace-plan-name { font-size: 17px; letter-spacing: 0.06em; }
    .ace-plan-blurb { font-size: 14px; }
    .ace-plan-btn { font-size: 14px; padding: 11px 16px; }
    .ace-status { padding: 12px 13px; }
    .ace-status-title { font-size: 15px; letter-spacing: 0.06em; }
    .ace-status-body { font-size: 14px; }
    .ace-free-note { font-size: 11px; }

    /* ---- HOW TO PLAY ---- */
    /* The masthead shrinks to 52px here and main's top padding to 14px, so
       the chrome to subtract is 92px rather than the desktop 127px. */
    #howto.active { min-height: calc(100vh - 92px); }
    .how-panel { padding: 18px 14px 16px; border-radius: 16px; }
    /* 24px plus 15%. */
    .how-hook { font-size: 27.6px; }
    /* Single column on a phone -- two of these side by side would be four
       words to a line. */
    .how-rules { margin-top: 14px; grid-template-columns: 1fr; }
    .how-rule { grid-template-columns: 26px 1fr; gap: 10px; padding: 13.2px 13px; }
    .how-num { font-size: 22px; }
    .how-rule-title { font-size: 14px; letter-spacing: 0.06em; }
    .how-rule-body { font-size: 14px; }
    .how-copy-wide { display: none; }
    .how-copy-narrow { display: block; }
    /* At 14px this line overshoots a 315px column by about 18px, which is
       letter-spacing alone -- tightening it to 0.04em holds one line without
       touching the size. */
    .how-note { letter-spacing: 0.04em; }
  }

  @media (max-width: 700px) {
    .modal { padding: 24px 18px 20px; border-radius: 16px; }
    .modal-title { font-size: 26px; }
    .modal-sub { font-size: 14px; }
    .menu-account { padding: 14px 20px; }

    /* Down here, not in the 700px block near the top: that one is declared
       BEFORE the RESULTS rules, so at equal specificity the desktop values win
       on source order and anything put there for these screens does nothing.

       18px is the desktop size and it truncates on a phone. Measured against
       the longest name a real lineup throws up: at 375 the ceiling is 17px,
       and 18 clips 'Christian McCaffrey'. The slot column follows the size --
       FLEX is four characters of tracked mono, so it needs about 0.72em each. */
    /* Measured on a row carrying the TROPHY, which is the narrow case: the icon
       and its gap take 29px out of the name, so a row with one has far less room
       than the row beside it -- and sizing against a row without one, which is
       what happened first, reads far too optimistic.

       At 375 the trophy row truncates 32.4% of real player names at 18px, 14.5%
       at 17px, and 4.7% at 16 with the icon down to 16 as well. The columns
       follow the size: FLEX, 105th and 62.1 are what each has to hold. */
    .res-bar-slot { width: 33px; font-size: 15px; }
    .res-bar-name { font-size: 16px; }
    .res-bar-rank { min-width: 30px; font-size: 12px; }
    /* The trophy's 10px comes out of the name on every row, trophy or not, and
       a phone has not got 10px to give: at 320 it took truncation of real player
       names from 5.9% to 16.7%. Halved here, gone at the small end. */
    .res-bar-crown-icon { margin-right: 5px; }

    /* Held to the Share Score button's own width -- .res-actions is two 1fr
       columns with a 10px gap, so its first column is exactly this, and a block
       of that width starts at the same left edge. The row wraps inside it and
       each line centres, which is the two-line block sitting on the button. The
       desktop shift comes off: a box already in the right place does not need
       moving.

       Down here rather than in the 700px block above, which is declared BEFORE
       .res-social and lost the transform to it on source order -- the width
       applied, nothing overrode that, and the row came out button-sized and
       still shifted 48px off the button. */
    .res-social {
      width: calc(50% - 5px);
      transform: none;
    }
    /* TEXT . X . FACEBOOK, and nothing else. Share with was the half of the row
       that would not fit on one line over a button this wide, and the three names
       say what the row is without it. */
    .res-social-label { display: none; }
    /* 30, not 28: the widest two-digit score draws 28.9 at this size, so 28 let
       a row like 25.7 grow its own box while 13.0 did not, and the rank column in
       front moved with it. */
    .res-bar-val  { min-width: 30px; font-size: 16px; }
    .res-bar-crown-icon { width: 16px; height: 16px; }
  }

  @media (max-width: 360px) {
    /* Re-measured for Newsreader, which is wider than the Anton it replaced on
       the name -- and measured on a row carrying the TROPHY, which is the narrow
       case: the icon and its gap take 29px out of the name column, and the first
       sweep missed that by landing on a row without one. On a trophy row at 320,
       14px truncates 22.5% of real player names against 4.5% at 13px. */
    .res-bar-slot { width: 27px; font-size: 12px; }
    .res-bar-name { font-size: 13px; }
    .res-bar-rank { min-width: 23px; font-size: 9px; }
    .res-bar-crown-icon { margin-right: 0; }
    .res-bar-val  { min-width: 24px; font-size: 13px; }
    .res-bar-crown-icon { width: 16px; height: 16px; }

    /* Measured at 320, where the home column holds 250px. Play Today's
       Challenge is 230px of label at 18px, and with 12px a side and the 2px
       border it needs 258 -- so a flex item's min-content width pushed it 4px
       wider than the button under it, the one thing the pair must not do.
       Both buttons share one size at every width, so both drop here. */
    .home-play, .home-unlimited { font-size: 17px; }

    /* Measured at 320, where Leaderboard's column is 121px. PLAY AGAIN is 90px
       of label, and 16px a side plus the 2px border left it 85 -- two lines.
       12px a side leaves 93. It keeps the column rather than taking the row,
       because the column is the place it was asked to go. */
    .res-btn-again { padding-left: 12px; padding-right: 12px; }
  }

  /* ---- ONE BUTTON HEIGHT ----
     Every action button on the site stands 56px, the height Share Score and
     Leaderboard on the results screen already had.

     LAST IN THE STYLESHEET ON PURPOSE. Media queries carry no specificity, so
     only source order lets one rule outrank every breakpoint's padding -- four
     of these set vertical padding of their own at 700px. It zeroes that padding
     and lets min-height do the sizing, which is also why this is min-height and
     not height: a label that wraps on a narrow phone grows its button instead
     of being clipped by it. Horizontal padding, fonts and widths stay with each
     component.

     A new action button joins this list. A vertical padding written for one of
     these anywhere above here does nothing -- the same trap as the two 700px
     blocks, in the other direction.

     Deliberately not here: the scoreboard's .sb-action cards (an icon badge and
     two lines of text, not a button), the masthead Sign In, menu rows, icon
     buttons, the leaderboard day arrows, stats range chips, the share-with
     links and the unlimited decade tiles. */
  .res-btn, .play-again, .home-play, .home-unlimited, .submit-btn,
  .unl-pick-go, .unl-field select,
  .auth-submit, .oauth-btn, .ace-plan-btn, .lb-more {
    min-height: 56px;
    padding-top: 0;
    padding-bottom: 0;
  }
