*,*::before,*::after{box-sizing:border-box;}

  :root{
    /* THE DAYLIGHT PALETTE, from the design handoff's design-tokens.json.
       The game used to be raced at night under sodium lamps, and the names said
       so - ink, asphalt, verge, sodium, chalk. It is now a bright, warm, kid-
       facing circuit, and every one of those names would have been a lie about
       its own value. They are renamed rather than merely revalued: a variable
       called --accent holding a blue is the kind of thing that is read wrong
       once and then copied everywhere. */
    /* THE PAGE IS WHITE, AND THE TOKEN FILE SAYS IT IS NOT. design-tokens.json
       proposes #FFFCF5, but the mockups the tokens were written from are near-
       white: sampled over the four desktop references, the dominant background
       is #FEFEFE on all four, with #FFFCF5's warmth appearing only as a faint
       tint under the illustrations. Taken literally the warm value reads as
       beige the moment a white card sits on it - which is exactly what it did.
       So the screens win over the token file, and the warmth is kept where the
       mockups actually put it: in the art. */
    --paper:#FEFEFE;   /* the page */
    --card:#FFFFFF;    /* a panel raised off it - separated by shadow, not fill */
    --sunken:#F4F6FA;  /* an inset: inputs, wells, the inner face of a control */
    --edge:#DDE3EE;    /* a filled border, or a control's resting face */
    --line:#E3E7F0;    /* a hairline */

    --navy:#081D49;    /* every piece of text that is read; 16.2:1 on paper */
    /* THE PACKET'S #6D7894 MEASURED 4.30:1 ON PAPER AND FAILED AA. Darkened by
       four per cent, which is invisible beside it and clears the bar at 4.62:1.
       The same trap the old --muted carried a note about: a muted grey chosen by
       eye is the one token that is always a shade too light. */
    --muted:#68738E;   /* 4.70:1 on paper */

    --accent:#1468FF;      /* the primary action, and every interactive accent */
    --accent-soft:#EEF3FF; /* its wash: the current letter, a pressed key */
    --player:#FDE321;      /* YOU - sampled from the car sprite. 1.3:1: never text */
    --player-ink:#8A6206;  /* the same identity where it has to be READ; 5.4:1 */
    --teal:#009F98;        /* the letters already typed, at 34px and up */
    --teal-ink:#007F79;    /* the same green where it has to carry small text */
    --mint:#E4F5E8;
    --lavender:#EEE9FF;
    /* Was #E23D4E in the packet and measured 4.09:1; darkened to clear AA. */
    --brake:#CF3847;

    --hero-h:128px;   /* the mobile hero's cap; see the portrait rule */
    --gold:#F4B72D;
    --silver:#AAB4C3;
    --bronze:#BA743F;

    /* Nunito is the handoff's proposed face and is not bundled with it, so it is
       fetched with the ones the game already fetches and falls back to the same
       system stack the packet's own SVGs do. */
    --display:"Nunito","Trebuchet MS",sans-serif;
    --ui:"Nunito","Helvetica Neue",Arial,sans-serif;
    --mono:"JetBrains Mono",ui-monospace,"SF Mono",Menlo,monospace;
  }
  /* The drill panel opens in the standings column part-way through a slow race,
     the document grows past the window, and a scrollbar arrives — which narrows
     the viewport and slides the centred shell four pixels left, mid-word. That
     is the screen jumping while somebody is typing, the oldest bug in this
     project, in its horizontal form; it went unseen for as long as the check
     sampled only the word row's TOP. Reserve the gutter always so the page
     cannot change width while a race is running. */
  /* The body is centred with margin:0 auto inside a max-width, so ANY change in
     the page's usable width moves it sideways — a scrollbar arriving mid-race
     slid the whole shell 4px left while the player was typing. scrollbar-gutter
     reserves the space where it is supported; overflow-y:scroll forces the track
     to exist at all times and is the version no engine can decline. Both, so the
     width the shell is centred in is a constant from first paint. */
  html{scrollbar-gutter:stable;overflow-y:scroll;}

  /* THE ATTRIBUTE THAT WAS NOT WINNING, FIXED AS A CLASS.
     `hidden` is honoured by a UA rule of the very lowest priority, so ANY author
     rule that sets `display` on the same element beats it and the element stays
     on screen with the attribute set. Sixteen `.thing[hidden]{display:none}`
     rules had been written into this file one at a time, each one after somebody
     noticed. `.readout` never got one, so `accRow.hidden = solo` in hud.js has
     been a no-op since iteration 36 and a solo lap has shown its accuracy twice
     in the same row of readouts ever since — which is how it reached a user.
     Rule 29: fix the class, not the seventeenth instance. `!important` because
     equal-specificity ordering is what made this fragile in the first place; a
     rule added below with a higher specificity would take it straight back.
     Nothing in this file wants to lay out a hidden element — the one rule that
     mentions the attribute positively, .tap-hint, asks for :not([hidden]). */
  [hidden]{display:none !important;}

  body{
    background:var(--paper);
    color:var(--navy);
    font-family:var(--ui);
    font-size:15px;
    line-height:1.45;
    -webkit-font-smoothing:antialiased;
  }

  /* THE WIDTH THE GAME IS ALLOWED TO USE, AND WHY IT IS NOT A CONSTANT.
     1180px was measured on a 2112x1177 window as 44% of the screen unused, with
     the circuit — the main event, the thing a player watches — drawn 852px wide
     inside a 2112px window. The matrix topped out at 1440x900, so every monitor
     sold in the last decade was untested (rule 4: the property every row shared
     was "at most 1440 wide").

     WHAT DESERVES THE WIDTH IS THE PICTURE, NOT THE PROSE (rules 245/246). The
     circuit is a figure and scales; the word being typed is the focal glyph of a
     typing game and scales with the box it sits in (below); everything that is
     READ — the help text, the setup copy, the results verdicts and tables, the
     rival list — already carries its own ch or px measure and keeps it, because
     running text at 1800px is unreadable. The standings column stays 268px: it
     holds four names, a trait and a number, and there is nothing there for extra
     width to do.

     THE LIMIT IS HEIGHT, NOT WIDTH. fitCanvas() draws the circuit at a fixed 0.6
     aspect, so every pixel of extra width costs 0.6px of height, and a shell
     sized on width alone pushes the bottom of the track off a 16:9 screen — the
     race half-visible is worse than the race small. Measured on this layout the
     page is 432px of non-canvas above and below the circuit (measured: a racing
     screen at 1920x1080 is 943px tall with a 511px canvas), plus the 16px the
     word band grows when the wide-screen word size below turns on. So the whole
     game fits one screenful while
         0.6 x (shell - 326px) <= 100vh - 448px
     where 326px is this shell's padding plus the standings column plus the gap.
     That solves to 166.66vh - 421px, and 436 is used: fifteen pixels of slack,
     because the 432 is a measurement of a real layout and not an identity, and
     the direction the error must not go is "the track runs off the screen".
     The lower bound is the old constant, so NO viewport that fits inside 1180px
     moves by a pixel and every row of the matrix at or below 1440x900 is
     byte-identical; the 96vw upper bound keeps a gutter on a portrait window,
     where width runs out first.

     The consequence worth stating: on a large desktop the document now ends at
     the fold rather than short of it, which is what check-layout's large-screen
     fill gate asserts in both directions — big enough to use the screen, small
     enough not to run off it. */
  .shell{
    max-width:clamp(1180px, calc(166.66vh - 436px), 96vw);
    margin:0 auto;
    padding:22px 20px 40px;
    display:flex;
    flex-direction:column;
    gap:18px;
  }

  /* ---------- header ---------- */
  .masthead{
    display:flex;
    align-items:flex-end;
    justify-content:space-between;
    gap:20px;
    flex-wrap:wrap;
    border-bottom:1px solid var(--line);
    padding-bottom:14px;
  }
  .brand{display:flex;flex-direction:column;gap:2px;}
  .brand h1{
    font-family:var(--display);
    font-weight:900;
    font-size:29px;
    letter-spacing:-.01em;
    margin:0;
    text-wrap:balance;
  }
  .brand h1 em{font-style:normal;color:var(--player-ink);}
  /* SENTENCE CASE, NOT SMALL CAPS. Uppercase at 0.14em of tracking is a
     timing-screen label - it reads as a spec line, and it is the first thing
     under the game's name. The player this is for reads sentences. */
  .brand p{
    margin:0;
    color:var(--muted);
    font-size:14px;
    letter-spacing:0;
    font-weight:600;
  }

  .readouts{display:flex;gap:26px;}
  .readout{display:flex;flex-direction:column;gap:1px;min-width:66px;}
  .readout .k{
    font-size:10px;letter-spacing:.16em;text-transform:uppercase;
    color:var(--muted);font-weight:600;
  }
  .readout .v{
    font-family:var(--display);font-weight:600;font-size:25px;
    font-variant-numeric:tabular-nums;line-height:1.1;
  }
  .readout .v small{font-size:14px;color:var(--muted);font-weight:500;}

  /* ---------- main grid ---------- */
  .arena{
    display:grid;
    grid-template-columns:1fr 268px;
    /* Two rows, so the pace card can sit under the running order instead of
       under the circuit. In the left column it made a 1440x900 desktop 999px
       tall and the page scrolled mid-race; the right column had 284px of unused
       height beside a 366px standings panel.
       The slack goes in row 2, not split between the rows: the left column is
       taller than the standings, and shared slack opened a gap between the two
       right-hand cards. */
    grid-template-rows:auto 1fr;
    gap:18px;
    align-items:start;
  }
  .arena > .panel-col{grid-column:1;grid-row:1 / 3;}
  .arena > .standings{grid-column:2;grid-row:1;}
  .arena > .strip{grid-column:2;grid-row:2;margin-top:0;}
  /* grid items are min-width:auto by default, so a single wide child drags the
     whole column past the viewport — this is what broke the phone layout */
  .arena > *{min-width:0;}
  .panel-col,.strip,.standings,.track-wrap{min-width:0;}

  /* ---------- track ---------- */
  .track-wrap{
    position:relative;
    background:var(--card);
    border:1px solid var(--line);
    border-radius:20px;
    box-shadow:0 1px 2px rgba(8,29,73,.05), 0 6px 18px rgba(8,29,73,.05);
    overflow:hidden;
  }
  canvas{display:block;width:100%;height:auto;}
  /* The start screen floats inside this panel, and the panel's height comes from
     the canvas aspect ratio — so a tall, narrowish desktop window gave the
     overlay under 400px and buried the setup controls inside its own scroll.
     Reserve room where the window has room to give, and nowhere else: the min()
     collapses to nothing on a short laptop, which must not grow a scrollbar. */
  .track-wrap{min-height:min(560px, 62vh);}

  /* Race events live inside the word strip, in a row reserved at a fixed height
     so an arriving event can never reflow the words. They have to be here and
     not on the track panel: with a soft keyboard up, the phone shows only the
     header and the strip — the circuit is behind the keyboard. */
  .events{
    height:22px;flex:none;
    display:flex;justify-content:flex-end;align-items:center;gap:6px;
    overflow:hidden;pointer-events:none;
  }
  .ev{
    font-family:var(--display);font-weight:600;font-size:12.5px;
    flex:none;
    letter-spacing:.07em;text-transform:uppercase;
    padding:5px 11px 5px 9px;border-radius:8px;
    background:rgba(255,255,255,.92);
    border-left:3px solid var(--muted);
    color:var(--navy);
    white-space:nowrap;
    animation:evin .18s ease-out;
  }
  .ev.good{border-left-color:var(--teal);color:var(--teal-ink);}
  .ev.bad{border-left-color:var(--brake);color:var(--brake);}
  .ev.flag{border-left-color:var(--navy);}
  /* The kind, as a shape. Inline in the chip's own line box, so the strip's
     reserved 22px does not move and neither do the words above it. */
  .ev-mark{
    font-style:normal;font-size:11px;
    margin-right:5px;letter-spacing:0;
  }
  @keyframes evin{
    from{opacity:0;transform:translateX(10px);}
    to{opacity:1;transform:none;}
  }
  .strip{position:relative;}

  /* The overlays and the canvas share ONE grid cell rather than the overlay
     floating at inset:0 with its own scrollbar. Absolute positioning fixes the
     overlay to the canvas's height, so anything taller than the canvas — the
     results, and now the records with the backup controls under them — put its
     own buttons below an internal fold where nobody finds them. That defect has
     been paid for three times already: once on phones (the overlay was made to
     flow underneath the circuit), once when the course and championship rows
     were squashed into single wrapping lines, and once when the results grew a
     sticky footer. Stacking instead lets the taller of the two set the panel
     height, so opening a disclosure pushes the panel down rather than hiding
     what it just revealed. */
  /* Two rows now: the word band, then the circuit. The band is a ROW and not a
     layer floating over the canvas, because every circuit is normalised to fill
     the canvas box — the largest car-free centred region any of the three leaves
     is about 36px of a 600px-tall canvas on Chicane, so an overlay large enough
     to read would sit on top of the cars. A reserved row cannot: the word's box
     and the canvas's box do not intersect, which is what the checks measure.
     The overlays span both rows so a setup or results screen covers the word
     band too. The canvas row is 1fr and not auto so that the panel's min-height
     on a tall window is absorbed BELOW the circuit: two auto rows both stretch,
     which grew the word band on a tall narrow window and pushed the circuit
     down with it. */
  .track-wrap{display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:auto 1fr;}
  .track-wrap > .word-stage{grid-area:1 / 1;}
  /* The overlay deliberately spans BOTH rows so it can cover the circuit, which
     means the two genuinely overlap and paint order decides the winner. DOM
     order alone is not enough: iOS Safari promotes a canvas to its own
     compositing layer, and a composited layer paints above in-flow content that
     has no stacking context of its own. On a real iPhone that split the start
     card in half — the heading drawn over the word band, the championship line
     sliced by the canvas — while Chrome, Playwright's WebKit and this project's
     whole responsive matrix all showed it correct, because every one of those
     checks measures geometry and geometry cannot see compositing. Both layers
     are now named explicitly rather than left to the engine. */
  .track-wrap > .track-box{grid-area:2 / 1;align-self:start;z-index:0;min-width:0;}
  .track-box > canvas{display:block;width:100%;height:auto;}
  .track-wrap > .overlay{grid-area:1 / 1 / 3 / 2;z-index:1;}
  .overlay{
    position:relative;
    display:flex;flex-direction:column;
    align-items:center;justify-content:safe center;
    gap:16px;
    background:rgba(254,254,254,.92);
    text-align:center;
    padding:24px;
    min-height:100%;
  }
  .overlay[hidden]{display:none;}
  /* THE RESULTS ARE READ; EVERYTHING ELSE ON A CIRCUIT IS GLANCED AT.
     Five starting lights, a pause and a share card over 14% of a moving track is
     atmosphere. The results screen is a headline, a tally, two or three
     paragraphs and two charts, and the track drawn through all of it is the
     immediate cause of a user calling this screen noisy and hard to read. Same
     colour, no transparency — which also makes every contrast ratio this project
     measures honest, since the checker walks up to the first background with a
     non-zero alpha and reads it as if it were opaque. */
  /* THE START SCREEN IS A HERO WITH A CARD BESIDE IT, which is how the handoff
     draws its ready state: the illustration takes the left of the panel and
     everything you can DO sits on a white card to the right of it.

     The hero is a background rather than an <img> because it says nothing a
     screen reader should read between the heading and the button, and because a
     background crops to the panel's shape without a wrapper to letterbox it.
     `cover` is safe here in a way the handoff warns it is not everywhere: the
     scenes are square, and the car sits low and central in this one, so the
     crop takes sky rather than bodywork.

     THE URL IS RELATIVE TO THE STYLESHEET, NOT THE PAGE. style.css ships from
     build/, so ./assets resolves to build/assets and silently loads nothing -
     which looks exactly like a hero that has not arrived yet. ../assets is the
     repo root, where they are. */
  .overlay-hero{
    flex-direction:row;align-items:stretch;justify-content:flex-end;
    gap:0;padding:0;background:var(--paper);
    text-align:center;
  }
  .hero-art{
    flex:1 1 44%;min-width:0;
    background:var(--mint);
    border-radius:20px 0 0 20px;
  }
  /* THE IMAGE IS DECLARED ONLY WHERE THE HERO IS SHOWN. Under 720px the element
     is display:none, and a background named on a rule that also applies there is
     a 66KB fetch for something no phone ever paints. Naming it inside the same
     query that shows the hero keeps the declaration and the display in one
     place, so neither can drift from the other. */
  @media (min-width:721px){
    .hero-art{
      background:var(--mint) url("../assets/heroes/car-welcome-scene.webp?v=871d5695f6e0") center 60% / cover no-repeat;
    }
  }
  .ov-card{
    flex:0 1 clamp(340px, 56%, 560px);min-width:0;
    display:flex;flex-direction:column;align-items:center;
    justify-content:safe center;gap:16px;
    padding:24px;overflow:auto;
    background:var(--card);
    border-radius:0 20px 20px 0;
    box-shadow:-1px 0 0 var(--line);
  }
  /* A PHONE GETS NO HERO, AND THAT IS A MEASUREMENT RATHER THAN A PREFERENCE.
     The handoff's mobile ready screen does show the illustration, and a band of
     it across the top costs about a third of the panel - which put "Free race"
     59px below the fold on an iPhone 14, "Start lesson" 169px and
     "Championship" 292px. Every one of those is a way INTO the game that the
     player has to scroll to find, which is the one thing the fold budget exists
     to prevent, and the reason iterations 50 and 51 were spent undoing exactly
     this regression once already.

     So under 720px the start card goes back to being what it was: one column,
     no picture, no scroll container of its own. The two-pane hero is a desktop
     layout, where the width it needs actually exists. */
  @media (max-width:720px){
    .overlay-hero{flex-direction:column;padding:24px;background:var(--paper);}
    /* THE HERO COMES BACK ON A TALL PHONE, CAPPED. It was removed outright
       because a band at 34% of the panel put Free race 59px, Start lesson 169px
       and Championship 292px below the fold. A FIXED height is a different
       proposition from a percentage: it cannot grow with the card, and the
       height below is the largest the fold budget was measured to allow with
       the telemetry and the pace strip still on screen. Portrait only, and only
       above 780px of viewport - a shorter phone has no room and the landscape
       tier hides it outright. */
    .hero-art{display:none;}
    @media (orientation:portrait) and (min-height:780px){
      .hero-art{
        display:block;flex:none;height:var(--hero-h);
        border-radius:20px 20px 0 0;
        background:var(--mint) url("../assets/heroes/car-welcome-scene.webp?v=871d5695f6e0")
                   center 62% / cover no-repeat;
      }
    }
    /* AND IT MUST NOT SCROLL. `overflow:auto` makes the card a clipping box, and
       the setup panel that opens inside it was cut off 113px past its edge - a
       control that is not merely below the fold but unreachable. The page's own
       scroll is what carries a long start card on a phone, exactly as before. */
    .ov-card{flex:1 1 auto;width:100%;padding:0;overflow:visible;
             background:none;border-radius:0;box-shadow:none;gap:16px;}
  }

  #ov-finish{background:var(--paper);}
  .overlay h2:focus-visible{outline:2px solid var(--accent);outline-offset:4px;}

  /* a heading that names the dialog for assistive tech without repeating the
     card's own title on screen */
  .sr-title{
    position:absolute;width:1px;height:1px;
    margin:-1px;padding:0;overflow:hidden;
    clip-path:inset(50%);white-space:nowrap;
  }

  .lights{display:flex;gap:9px;}
  .lights span{
    width:26px;height:26px;border-radius:50%;
    background:var(--edge);
    border:2px solid var(--edge);
    transition:background .1s linear,box-shadow .1s linear;
  }
  .lights span.on{
    background:var(--brake);
    border-color:var(--brake);
    box-shadow:0 0 16px rgba(207,56,71,.45);
  }
  .go-word{
    font-family:var(--display);font-weight:900;font-size:56px;
    letter-spacing:.02em;color:var(--accent);
    text-shadow:none;
  }
  .overlay h2{
    font-family:var(--display);font-weight:900;font-size:34px;margin:0;
    letter-spacing:-.01em;line-height:1.15;
  }

  /* The start card's heading and its Help button on one line. A row rather than
     a stacked button because the three ways into the game sit immediately below
     with 23px of spare screenful on the tightest screen in the matrix, and a
     button on its own row would spend all of it. The button is smaller than the
     heading's line box, so this row is exactly as tall as the h2 was. */
  .ov-head{
    display:flex;align-items:center;justify-content:center;
    gap:12px;width:100%;flex-wrap:nowrap;
  }
  /* THE HEADING GIVES UP NO WIDTH. A nowrap flex row shrinks its items, and a
     shrunk h2 does not clip - it WRAPS, three lines of it on a 375px phone, and
     the row that is supposed to cost nothing grew 37px and pushed the ways in
     with it. The heading is sized by its own text; the picker beside it is the
     item that gives way, because a select can be ellipsised and a heading
     cannot. */
  .ov-head h2{flex:none;}
  .help-btn{
    flex:none;
    font-family:var(--display);font-weight:600;font-size:11px;
    letter-spacing:.09em;text-transform:uppercase;
    color:var(--navy);background:var(--sunken);
    border:1px solid var(--edge);border-radius:10px;
    padding:5px 11px;cursor:pointer;
  }
  .help-btn:hover{border-color:var(--accent);color:var(--accent);}

  /* THE LESSON PICKER, on the heading line beside Help and for the same reason:
     inside the h2's line box it costs the card no height at all, and the three
     ways into the game below it have as little as 27px of spare screenful.
     It SHRINKS rather than wraps - .ov-head is nowrap, so an item that refuses
     to give width back would push the row wider than a 375px phone. A native
     select is otherwise as wide as its longest option ("Lesson 6 - Caps and
     punctuation, 3 laps"), which is 250px on its own; the box is capped and the
     label ellipsised inside it, while the OPTION LIST the browser draws is its
     own popup and is not clipped by the box. */
  .pick{
    flex:0 1 auto;min-width:0;max-width:11em;
    font-family:var(--display);font-weight:600;font-size:10.5px;
    letter-spacing:.06em;
    color:var(--navy);background:var(--sunken);
    border:1px solid var(--edge);border-radius:10px;
    padding:4px 7px;cursor:pointer;
    text-overflow:ellipsis;
  }
  .pick:hover{border-color:var(--accent);color:var(--accent);}
  .pick:focus-visible{outline:2px solid var(--navy);outline-offset:2px;}
  /* A rung not yet reached is SHOWN and refused, not hidden: the ladder is the
     method, and a player who cannot see the rungs above them cannot see that
     there are any. The browser greys a disabled option by itself; the label says
     so in words as well, because grey is not a message. */
  .pick option:disabled{color:var(--muted);}
  .pick[hidden]{display:none;}

  /* The instructions. Left-aligned in a reading column inside a centred overlay:
     nine short sections a player can scan for the one answer they came for,
     which is the difference between moving a wall of text and editing it. */
  .help-body{
    display:flex;flex-direction:column;gap:14px;
    text-align:left;width:100%;max-width:min(62ch,100%);
  }
  .help-sec h3{
    font-family:var(--display);font-weight:600;font-size:12px;
    letter-spacing:.11em;text-transform:uppercase;
    color:var(--accent);margin:0 0 4px;
  }
  .help-sec p{
    margin:0;font-size:13px;line-height:1.55;color:var(--muted);
  }
  .help-sec b{color:var(--navy);font-weight:600;}
  .help-sec kbd{
    font-family:var(--mono);font-size:11px;
    background:var(--sunken);border:1px solid var(--edge);
    border-radius:8px;padding:1px 5px;color:var(--navy);
  }
  .overlay .sub{color:var(--muted);max-width:min(44ch,100%);margin:0;font-size:14px;}

  /* THE WAYS IN. Free play, the course and the championship, stacked directly
     under the heading and above the settings. None of them may be below the
     fold - which is what check-layout.mjs now asserts for every screen in the
     matrix rather than merely printing.
     Exactly ONE of them is the loud one at a time. While the ladder is
     unfinished the lesson wears the filled .btn and free play goes quiet;
     once every lesson is cleared the course button is gone and free play is
     loud again, because from there the settings really are the point. A player
     working through a seven-lesson course read the lesson line, pressed the
     brightest control above it, and got free play with their own text step.
     paintCourse() owns the swap; the CSS below gives .ways its own pair of
     sizes so that quiet means "smaller" as well as "dimmer", and the two
     buttons trade both. Because they trade, the block gains no height either
     way - it comes out a few pixels shorter.
     The dividers come from .course/.champ themselves, so this is only the
     column that holds them. */
  .ways{
    display:flex;flex-direction:column;align-items:center;
    gap:10px;width:100%;max-width:100%;
  }

  /* The course: one row above the settings, because being told what to practise
     is the thing a beginner needs and choosing it is not. */
  /* One wrapping row, not three stacked lines. With two of these entries above
     the settings — the course and the championship — the stacked form pushed the
     grid buttons past the bottom of the overlay on a laptop screen, which is a
     control the player cannot reach. */
  .course{
    display:flex;flex-direction:row;flex-wrap:wrap;
    align-items:baseline;justify-content:center;gap:3px 10px;
    width:100%;max-width:100%;
    padding-bottom:7px;border-bottom:1px solid var(--line);
  }
  .course-line{
    margin:0;font-family:var(--display);font-size:14px;font-weight:600;
    color:var(--accent);letter-spacing:.02em;text-align:center;
  }
  .course-bar{
    margin:0;font-size:12px;color:var(--muted);text-align:center;
    max-width:min(46ch,100%);
  }
  /* Keyed on .btn-quiet, NOT on the two ids. An id out-specifies every class,
     so an id-keyed font-size would have pinned the lesson button small however
     loud paintCourse() made it, and the size half of the swap would have
     silently done nothing - which is why the matrix now asserts the two font
     sizes against each other and not only the class.
     The promoted button is sized HERE rather than inheriting the full .btn,
     and that is a measurement, not a preference. .course lays its line, its bar
     and its button out as ONE wrapping row, and on a 667x375 iPhone SE in
     landscape all three fit with about 43px to spare. A full-size .btn is
     253px wider than the quiet one, so promoting the lesson wrapped the button
     onto a second flex line and cost 21px of a 23px fold budget - the exact
     row-gaining this change was forbidden. At this size the promoted button is
     about 27px wider than the quiet one, it stays on the line, and the block
     comes out SHORTER than before because free play gives up more than the
     lesson takes. Quiet comes second so it wins the tie on equal specificity. */
  .ways .btn{font-size:14px;padding:10px 22px;}
  .ways .btn-quiet{font-size:12px;padding:8px 18px;}
  #btn-course[hidden],#btn-champ[hidden]{display:none;}

  /* The championship sits directly under the course: the same kind of offer,
     here is what to race next. It is laid out as one wrapping row rather than
     three stacked lines, because two full-width entries above the settings is
     what pushed the grid buttons out of the reach of a laptop screen. */
  .champ{
    flex-direction:row;flex-wrap:wrap;align-items:baseline;
    justify-content:center;gap:2px 10px;padding-bottom:7px;
  }
  .champ .course-line{font-size:13px;}
  .champ .course-bar{font-size:11.5px;line-height:1.35;max-width:min(52ch,100%);}
  .champ-result{
    display:flex;flex-direction:column;align-items:flex-start;gap:8px;
    width:100%;max-width:100%;
  }
  .champ-result[hidden]{display:none;}
  .champ-table{width:100%;max-width:min(38ch,100%);}
  .champ-table th{
    font-family:var(--display);font-size:11px;letter-spacing:.09em;
    text-transform:uppercase;color:var(--muted);text-align:left;padding-bottom:4px;
    border-bottom:1px solid var(--line);
  }

  /* ---------- the rounds, as steps ---------- */
  .steps{
    list-style:none;margin:0 0 4px;padding:0;
    display:flex;align-items:flex-start;justify-content:center;gap:0;
    width:100%;max-width:min(420px,100%);
  }
  .steps .step{
    display:flex;flex-direction:column;align-items:center;gap:4px;
    flex:1 1 0;min-width:0;position:relative;
  }
  /* The rail between two steps, drawn by the step on its right so the first one
     has none - a leading rail would point at nothing. */
  .steps .step + .step::before{
    content:"";position:absolute;top:15px;right:50%;left:-50%;height:3px;
    background:var(--line);border-radius:2px;
  }
  .steps .step[data-state="done"]::before,
  .steps .step[data-state="now"]::before{background:var(--teal);}
  .steps .step-dot{
    position:relative;z-index:1;
    width:32px;height:32px;border-radius:50%;
    display:flex;align-items:center;justify-content:center;
    font-family:var(--display);font-weight:800;font-size:15px;
    background:var(--edge);color:var(--navy);
    border:3px solid var(--card);
  }
  .steps .step[data-state="done"] .step-dot{background:var(--teal);color:var(--navy);}
  .steps .step[data-state="now"] .step-dot{
    background:var(--accent);color:#fff;
    box-shadow:0 0 0 4px rgba(20,104,255,.2);
  }
  .steps .step-name{font-size:12.5px;color:var(--muted);font-weight:600;}
  .steps .step[data-state="now"] .step-name{color:var(--accent);font-weight:800;}
  /* The car on a standings row. The sprites are PORTRAIT - 384x512, nose up -
     so a landscape box fits one to its width and leaves it about eighteen
     pixels of car. Sized to the sprite's own shape instead, and kept short
     enough that a table of five does not become a table of five pictures. */
  .row-car{
    width:26px;height:34px;object-fit:contain;
    vertical-align:middle;margin-right:4px;
  }

  /* ---------- the championship overview ---------- */
  /* OPAQUE, like the results screen and for its reason: this is a screen that
     is READ - a heading, three cards and a points table - and the circuit
     drawn through it is the thing that made the results screen noisy. It also
     keeps every contrast ratio here honest, since the checker walks up to the
     first background with a non-zero alpha and reads it as opaque. */
  .overlay-champ{gap:12px;background:var(--paper);}
  .round-list{
    list-style:none;margin:4px 0 0;padding:0;
    display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;
    width:100%;max-width:min(920px,100%);
  }
  .round-card{
    position:relative;display:flex;flex-direction:column;gap:2px;
    padding:10px 10px 12px;border-radius:20px;
    background:var(--card);box-shadow:inset 0 0 0 1.5px var(--line);
    text-align:left;
  }
  /* The round about to be raced is the only loud one. A round already won is
     dimmed rather than hidden: the map of the championship is the point, and a
     card that vanishes when you win it makes the journey shorter as you go. */
  .round-card[data-state="next"]{box-shadow:inset 0 0 0 2.5px var(--accent);}
  /* NOT opacity. At .55 over paper the card's own name fell to 3.83:1 and its
     lap line to 2.13:1, and "raced" was carried by nothing else — a colour-only
     state, which is the one thing this project's colour gate exists to refuse.
     The card keeps full contrast and says so in words instead. */
  /* Card white, not the well grey: --muted on --sunken is 4.38:1, which is the
     exact pair fixed twice already in this stylesheet. The tick and the word on
     the badge are what say "raced"; the card does not need to be dimmer too. */
  .round-card[data-state="done"]{background:var(--card);}
  .round-card[data-state="done"] .round-laps{color:var(--muted);}
  .round-thumb{
    width:100%;height:auto;display:block;border-radius:12px;
  }
  .round-badge{
    position:absolute;top:18px;left:18px;
    padding:4px 12px;border-radius:999px;
    background:var(--accent);color:#fff;
    font-family:var(--display);font-weight:800;font-size:12px;
  }
  /* A raced round wears the same pill in the completed colour, with a tick, so
     "done" is a word and a glyph before it is a shade. */
  .round-badge[data-state="done"]{background:var(--teal);color:var(--navy);}
  .round-name{
    margin-top:8px;font-family:var(--display);font-weight:800;font-size:16px;color:var(--navy);
  }
  .round-laps{font-size:13px;color:var(--muted);}
  @media (max-width:720px){
    .round-list{grid-template-columns:minmax(0,1fr);gap:10px;}
    .round-card{
      display:grid;grid-template-columns:88px minmax(0,1fr) auto;
         The slack goes in row 2, not split between the rows: the left column is
       taller than the standings, and shared slack opened a gap between the two
       right-hand cards. */
    grid-template-rows:auto 1fr;gap:2px 10px;
      align-items:center;padding:8px 10px;
    }
    .round-thumb{grid-column:1;grid-row:1 / 3;width:88px;height:auto;}
    .round-name{grid-column:2;grid-row:1;margin-top:0;}
    .round-laps{grid-column:2;grid-row:2;}
    .round-badge{
      position:static;grid-column:3;grid-row:1 / 3;
      white-space:nowrap;padding:4px 8px;
    }
  }

  @media (max-width:720px) and (orientation:portrait){
    .track-wrap{min-height:0;}
  }

  /* THE NEXT WORD, ON A PHONE. The packet's own acceptance check asks that the
     current word AND the next one stay visible with the keyboard open, and the
     next one was simply switched off below 880px - the queue is absolutely
     positioned to the right of the word on a desktop and there is no room for
     that here. So on a phone it comes out of that position, sits under the word
     as one line, and shows ONE upcoming word rather than three.
     `.word-stage .queue` rather than `.queue`, because the rule that hides it is
     later in the file and this has to outrank it on specificity, not order. */
  @media (max-width:880px){
    .word-stage .word-head{
      display:block;text-align:center;margin:0;
    }
    .word-stage .word-eyebrow{display:none;}
    .word-stage .word-title{font-size:13px;line-height:17px;}
    /* Its own row. .word-line is a flex row holding the word and the queue, so a
       merely static queue sits BESIDE the word and inherits the 16% cap and the
       overflow:hidden that made it a strip - which clipped "Up next:" to
       "p next:". Wrapping the line and giving the queue the full basis puts it
       under the word, where the design has it. */
    .word-stage .word-line{flex-wrap:wrap;height:auto;row-gap:2px;}
    .word-stage .queue{
      flex:1 0 100%;overflow:visible;
      display:flex;position:static;transform:none;
      max-width:100%;justify-content:center;align-items:baseline;gap:6px;
      font-family:var(--display);font-weight:800;font-size:15px;line-height:20px;
      color:var(--navy);
    }
    .word-stage .queue::before{
      content:"Up next:";
      font-family:var(--display);font-weight:700;font-size:13px;color:var(--muted);
    }
    /* One word, not three: the design shows a single upcoming word and the
       others are what made this an absolutely-positioned strip in the first
       place. */
    .word-stage .queue > span:nth-child(n+2){display:none;}
  }

  /* ---------- the road to racing ---------- */
  .journey-map{
    flex:1 1 52%;min-width:0;position:relative;
    border-radius:20px 0 0 20px;overflow:hidden;
    /* THE LETTERBOX, PAINTED AS THE PICTURE. `contain` is not optional here -
       the badges sit at fixed fractions of the plate - so the panel is taller
       than the square and shows a band above and below it. These two colours
       are sampled from the plate's own top and bottom rows, so the bands read
       as more sky and more grass rather than as a margin. */
    background:linear-gradient(#93D8ED 0%, #A0D6DD 22%, #94D28A 34%, #B4ED81 100%);
  }
  /* CONTAIN, NEVER COVER. The badges are placed at fixed fractions of the
     plate, and a cover crop slides the road under them - a rung that was on the
     tarmac lands in the lake. The panel letterboxes instead, and journey.ts
     measures the displayed square rather than the element. */
  .journey-plate{
    position:absolute;inset:0;
    background:url("../assets/backgrounds/lesson-journey-clean.webp?v=871d5695f6e0") center / contain no-repeat;
  }
  .journey-nodes{position:absolute;inset:0;}
  /* A RUNG. Positioned by its centre, which is what the layout's coordinates
     are, so translate(-50%,-50%) rather than an offset nobody can check. */
  .node{
    position:absolute;transform:translate(-50%,-50%);
    width:44px;height:44px;border-radius:50%;
    display:flex;align-items:center;justify-content:center;
    border:3px solid var(--card);
    /* NAVY ON TEAL, NOT WHITE. White on #009F98 is 3.27:1 and these numerals
       are 17px/800 — under the 18.66px-bold large-text threshold, so the bar is
       4.5:1. Navy on the same teal is 5.0:1. --teal's own note in :root says it
       is for "the letters already typed, at 34px and up". */
    background:var(--teal);color:var(--navy);
    font-family:var(--display);font-weight:800;font-size:17px;
    cursor:pointer;padding:0;
    box-shadow:0 2px 6px rgba(8,29,73,.28);
    transition:transform .12s ease;
  }
  .node:hover:not(:disabled){transform:translate(-50%,-50%) scale(1.08);}
  .node:focus-visible{outline:3px solid var(--navy);outline-offset:3px;}
  /* The rung you are on: the loud one, and the only badge that is blue. */
  /* WHITE HERE, NOT THE NAVY THE OTHER STATES USE. Navy reads on teal (5.0:1)
     and fails on accent (3.47:1) — the same number .kbk[data-f="th"] carries a
     note about. A blanket colour on .node made this badge worse than it was. */
  .node[data-state="current"]{
    color:#fff;
    background:var(--accent);
    box-shadow:0 0 0 6px rgba(20,104,255,.22), 0 2px 6px rgba(8,29,73,.28);
  }
  /* Not reached. Grey, padlocked, and genuinely disabled - the ladder is
     enforced by lessonByNumber, and this is that rule made visible. */
  .node[data-state="locked"]{
    background:var(--muted);cursor:not-allowed;box-shadow:none;
  }
  .node-cup{
    position:absolute;top:-18px;right:-16px;
    width:32px;height:32px;object-fit:contain;
    filter:drop-shadow(0 2px 3px rgba(8,29,73,.3));
  }
  /* A padlock in two rounded rectangles: the shackle is a bordered half-circle
     above a solid body. Not in the art package, and not worth a request. */
  .node-lock{
    position:absolute;bottom:-6px;right:-6px;
    width:16px;height:13px;border-radius:3px;
    background:var(--card);box-shadow:0 1px 2px rgba(8,29,73,.3);
  }
  .node-lock::before{
    content:"";position:absolute;left:4px;top:-5px;
    width:8px;height:7px;border-radius:4px 4px 0 0;
    border:2px solid var(--card);border-bottom:0;
  }
  /* NOT .chip: that name is already a livery swatch 800 lines below, and the
     later rule won - the lesson chip rendered as an empty 28px pill. */
  .lesson-chip{
    display:inline-block;padding:5px 14px;border-radius:999px;
    /* Navy, not accent: #1468FF on lavender is 3.98:1, and 13px/800 is not
       large text, so the bar is 4.5:1. Navy is 13.8:1 and the lavender still
       carries the "this is a chip" signal. */
    background:var(--lavender);color:var(--navy);
    font-family:var(--display);font-weight:800;font-size:13px;
  }
  @media (max-width:720px){
    .overlay-hero{justify-content:flex-start;}
    .journey-map{
      flex:none;width:100%;aspect-ratio:1;min-height:0;
      border-radius:20px 20px 0 0;
    }
    #ov-journey .ov-card{flex:none;}
  }

  /* ---------- the trophy ---------- */
  .trophy{
    display:flex;flex-direction:column;align-items:center;gap:6px;
    width:100%;max-width:min(46ch,100%);
  }
  .trophy[hidden]{display:none !important;}
  .trophy-cup{
    width:112px;height:112px;object-fit:contain;
    filter:drop-shadow(0 6px 10px rgba(8,29,73,.16));
  }
  .trophy-cup[hidden]{display:none;}
  .trophy-sub{
    margin:0;font-family:var(--display);font-weight:700;
    font-size:17px;color:var(--muted);
  }
  /* THE THREE TIERS, ALWAYS ALL THREE. A lap that earned nothing still shows
     them with none lit, because this is the screen that has to say what to aim
     at and an empty legend is that sentence as a picture. */
  .trophy-tiers{
    list-style:none;margin:10px 0 0;padding:0;
    display:flex;gap:10px;flex-wrap:wrap;justify-content:center;width:100%;
  }
  .trophy-tiers .tier{
    display:flex;align-items:center;gap:7px;
    padding:7px 13px 7px 9px;border-radius:999px;
    /* CARD WHITE, NOT THE WELL GREY, for the reason .kbk carries below:
       --muted on --sunken is 4.38:1 and fails AA at this size; on white it is
       4.70:1. Same pair, same fix, found by a review rather than by the gate. */
    background:var(--card);box-shadow:inset 0 0 0 1.5px var(--line);
    font-size:13px;color:var(--muted);
  }
  .trophy-tiers .tier b{color:var(--navy);font-weight:700;}
  .trophy-tiers img{width:28px;height:28px;object-fit:contain;opacity:.45;}
  /* The tier that was earned. Three cues, not one: the ring, the full-strength
     cup, and aria-current on the element itself - "the gold one is brighter" is
     not available to every player. */
  .trophy-tiers .tier[aria-current="true"]{
    background:var(--card);
    box-shadow:inset 0 0 0 2px var(--accent);
  }
  .trophy-tiers .tier[aria-current="true"] img{opacity:1;}
  .trophy-tiers .tier[aria-current="true"] b{color:var(--accent);}

  .course-verdict{
    margin:0;max-width:min(60ch,100%);
    font-size:13px;line-height:1.5;color:var(--navy);
    border-left:3px solid var(--muted);
    padding:2px 0 2px 11px;text-align:left;
  }
  .course-verdict[data-pass="true"],
  .course-verdict[data-done="true"]{border-left-color:var(--teal);}
  .course-verdict[hidden]{display:none;}

  /* THE ONE THING ON THIS CARD THAT IS ABOUT THE BROWSER RATHER THAN THE RACE.
     It borrows the course verdict's shape — a rule down the left, chalk text —
     because it is the same kind of object: a sentence about what just happened
     to the player's progress. The rule is amber rather than red: nothing is
     broken and the race is about to run exactly as it always does; what is lost
     is only the remembering, and a --brake bar over a working game is the
     second failure this was written to avoid. */
  .save-note{
    margin:0;max-width:min(60ch,100%);
    font-size:12.5px;line-height:1.5;color:var(--navy);
    border-left:3px solid var(--accent);
    padding:2px 0 2px 11px;text-align:left;
  }
  .save-note[hidden]{display:none;}

  .setup{
    display:flex;flex-direction:column;align-items:center;gap:11px;
    width:100%;max-width:100%;
  }
  .setup-summary{
    margin:0;font-size:12.5px;color:var(--navy);
    letter-spacing:.02em;text-align:center;
  }
  .setup-summary b{color:var(--accent);font-weight:600;}
  .disclose{
    font-family:var(--ui);font-size:10px;font-weight:600;
    letter-spacing:.16em;text-transform:uppercase;
    background:none;border:0;padding:2px 0;cursor:pointer;
    color:var(--muted);
    text-decoration:underline;text-underline-offset:3px;
  }
  .disclose:hover{color:var(--navy);}
  .disclose:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}
  .setup-panel{
    display:flex;flex-direction:column;align-items:center;gap:9px;
    width:100%;
  }
  .setup-panel[hidden]{display:none;}

  /* Nine record slots exist — three circuits by three distances — and until now
     you could only ever see the one you were sitting on. Each cell selects that
     combination, so a record is something you can go and attack. */
  .records{
    display:grid;grid-template-columns:auto repeat(3, 1fr);
    gap:4px;align-items:center;
    width:100%;max-width:420px;
    font-variant-numeric:tabular-nums;
  }
  .records[hidden]{display:none;}
  .records .rh{
    font-size:9px;letter-spacing:.14em;text-transform:uppercase;
    color:var(--muted);font-weight:600;text-align:center;padding-bottom:2px;
  }
  .records .rc{
    font-size:11px;color:var(--muted);font-weight:600;
    letter-spacing:.06em;text-align:right;padding-right:4px;
  }
  .records button{
    font-family:var(--mono);font-size:11.5px;
    background:var(--sunken);color:var(--navy);
    border:1px solid var(--line);border-radius:8px;
    padding:6px 4px;cursor:pointer;
  }
  .records button:hover{border-color:var(--muted);}
  .records button[aria-pressed="true"]{
    border-color:var(--accent);color:var(--accent);
  }
  .records button.empty{color:var(--edge);}
  .records button:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}

  /* All of the above lives in one browser and nowhere else. This is the way out
     of that: a code to copy, and a box to paste one back into. It sits under the
     records because that is where the progress it carries already is. */
  .backup{
    display:flex;flex-direction:column;align-items:center;gap:7px;
    width:100%;max-width:420px;
    border-top:1px solid var(--line);padding-top:11px;
  }
  .backup[hidden]{display:none;}
  .backup-h{
    margin:0;font-family:var(--display);font-weight:600;font-size:11px;
    letter-spacing:.16em;text-transform:uppercase;color:var(--muted);
  }
  .backup .setup-why{text-align:center;}
  .backup .share-link{margin-bottom:0;}
  .backup-acts{
    display:flex;gap:8px;flex-wrap:wrap;justify-content:center;width:100%;
  }
  .backup-acts .btn{font-size:12px;padding:8px 14px;letter-spacing:.08em;}
  .backup-restore{
    display:flex;flex-direction:column;align-items:center;gap:7px;width:100%;
    border-top:1px dashed var(--line);padding-top:9px;
  }
  .backup-restore[hidden]{display:none;}
  .backup-lab{
    font-size:10px;letter-spacing:.14em;text-transform:uppercase;
    color:var(--muted);font-weight:600;
  }
  .backup-warn b{color:var(--brake);}
  .backup .verdict{text-align:center;max-width:52ch;line-height:1.45;}

  /* The player's own passage. Same column-of-centred-things shape as .backup,
     because it is the same kind of thing: a box you put text into, two actions
     under it, and a sentence saying what happened. */
  .passage{
    display:flex;flex-direction:column;align-items:center;gap:7px;
    width:100%;max-width:420px;
  }
  .passage[hidden]{display:none;}
  .passage-box{
    width:100%;box-sizing:border-box;resize:vertical;
    font-family:var(--mono);font-size:11px;line-height:1.5;
    color:var(--navy);background:var(--sunken);
    border:1px solid var(--line);border-radius:8px;
    padding:7px 9px;
  }
  .passage-box:focus{outline:2px solid var(--accent);outline-offset:1px;}
  .passage .setup-why{text-align:center;}
  .passage .verdict{text-align:center;max-width:52ch;line-height:1.45;}

  .seg-row{
    display:flex;align-items:center;gap:11px;
    max-width:100%;flex-wrap:wrap;justify-content:center;
  }
  .seg-label{
    font-size:10px;letter-spacing:.16em;text-transform:uppercase;
    color:var(--muted);font-weight:600;
    width:58px;text-align:right;flex:none;
  }
  .seg{
    display:inline-flex;flex-wrap:wrap;border:1px solid var(--edge);
    border-radius:8px;overflow:hidden;max-width:100%;
  }
  .seg button{
    font-family:var(--ui);font-size:12px;font-weight:600;
    letter-spacing:.12em;text-transform:uppercase;
    background:none;border:0;color:var(--muted);
    padding:7px 16px;cursor:pointer;
    transition:background .12s ease,color .12s ease;
  }
  .seg button + button{border-left:1px solid var(--edge);}
  .seg button:hover:not(:disabled){color:var(--navy);}
  .seg button[aria-pressed="true"]{background:var(--accent);color:var(--paper);}
  .seg button:disabled{opacity:.4;cursor:not-allowed;}
  .seg button:focus-visible{outline:2px solid var(--navy);outline-offset:-2px;}

  .grid-preview{
    display:flex;gap:14px;flex-wrap:wrap;justify-content:center;
    font-variant-numeric:tabular-nums;
  }
  .grid-preview div{display:flex;align-items:center;gap:6px;font-size:13px;}
  /* Same swatch-plus-mark as .chip, at the setup screen's own scale. */
  .grid-preview i{
    width:12px;height:12px;border-radius:8px;flex:none;font-style:normal;
    font-family:var(--mono);font-size:8px;font-weight:700;line-height:12px;
    text-align:center;color:var(--paper);
  }
  .grid-preview b{
    font-family:var(--display);font-weight:600;font-size:14px;color:var(--navy);
  }
  .grid-preview span{color:var(--muted);}
  /* How the car races, beside what it is rated at. Same ink as the name so it
     reads as one label rather than as a second column. */
  .grid-preview em{font-style:normal;color:var(--muted);font-size:12px;}
  .setup-why{
    margin:0;font-size:11px;color:var(--muted);max-width:min(52ch,100%);line-height:1.45;
  }

  /* THE PILL. The old button was a 2px-radius slab in uppercase with 0.1em of
     tracking - a motorsport timing-screen button, which is what the game used to
     be. The handoff draws a fully rounded button in sentence case at a weight
     you can read across a room, and for the player this is now for that is the
     whole difference between a control and an instruction. */
  .btn{
    font-family:var(--display);font-weight:800;font-size:17px;
    letter-spacing:0;
    color:#fff;background:var(--accent);
    border:0;border-radius:999px;
    padding:14px 30px;cursor:pointer;
    box-shadow:0 2px 0 rgba(8,29,73,.13);
    transition:transform .12s ease,background .12s ease,box-shadow .12s ease;
  }
  /* The results are taller than the track panel on a short window and the overlay
     scrolls, which put Race again below the fold on a 1024x620 laptop. Pin the
     action in a bar at the bottom of the scroll area so it is always reachable,
     with the content fading under it rather than colliding with it. */
  .overlay-foot{
    position:sticky;bottom:0;gap:10px;flex-wrap:wrap;
    width:100%;margin-top:2px;
    display:flex;justify-content:center;
    padding:14px 0 2px;
    background:linear-gradient(to top,
      rgba(254,254,254,.97) 0%, rgba(254,254,254,.97) 62%, rgba(254,254,254,0) 100%);
  }
  /* The results screen's actions are no longer a FOOT — they sit directly under
     the result, above the classification table and the key chart, because that
     is where the player is looking when the race ends. Sticking them to the
     bottom of a scroll area is the right answer for a screen whose action is
     genuinely last (the start card); it is the wrong answer for one whose
     action is the first thing wanted. */
  .fin-act{
    position:static;background:none;
    margin:2px 0 4px;padding:0;
  }

  /* The secondary action is the same pill in card white with a hairline, the way
     Free race and Championship sit under Let's race in the handoff. */
  .btn-quiet{
    background:var(--card);color:var(--navy);font-weight:700;
    box-shadow:inset 0 0 0 1.5px var(--line);
  }
  .btn-quiet:hover{background:var(--sunken);color:var(--navy);
    box-shadow:inset 0 0 0 1.5px var(--accent);transform:translateY(-1px);}

  .btn:hover{background:#0B57E3;transform:translateY(-1px);box-shadow:0 4px 0 rgba(8,29,73,.13);}
  .btn:focus-visible{outline:3px solid var(--navy);outline-offset:3px;}

  /* results table */
  .results{
    width:100%;border-collapse:collapse;
    font-variant-numeric:tabular-nums;
  }
  .results th{
    font-size:10px;letter-spacing:.15em;text-transform:uppercase;
    color:var(--muted);text-align:left;font-weight:600;
    padding:0 0 6px;border-bottom:1px solid var(--line);
  }
  .results td{
    padding:7px 0;border-bottom:1px solid var(--line);
    font-size:14px;text-align:left;
  }
  .results td.num{text-align:right;font-family:var(--mono);font-size:13px;}
  .results tr.you td{color:var(--player-ink);font-weight:700;}
  .results .pos{
    font-family:var(--display);font-weight:700;width:26px;color:var(--muted);
  }
  .results tr.you .pos{color:var(--player-ink);}

  .tally{
    display:flex;gap:28px;justify-content:center;
    font-variant-numeric:tabular-nums;
  }
  .tally div{display:flex;flex-direction:column;gap:1px;}
  .tally .k{font-size:10px;letter-spacing:.15em;text-transform:uppercase;color:var(--muted);font-weight:600;}
  .tally .v{font-family:var(--display);font-weight:700;font-size:24px;}

  .fin-grid{
    display:grid;grid-template-columns:1fr 1fr;gap:30px;
    width:100%;max-width:800px;text-align:left;
  }
  .fin-col{display:flex;flex-direction:column;gap:10px;min-width:0;}
  .fin-col .panel-title{padding-bottom:7px;}

  .keychart{display:flex;flex-direction:column;gap:5px;}
  .krow{
    display:grid;grid-template-columns:16px 1fr 50px 42px 24px;
    gap:9px;align-items:center;
  }
  .kch{
    font-family:var(--mono);font-weight:700;font-size:14px;
    color:var(--navy);text-align:center;
  }
  .ktrack{
    display:block;position:relative;height:13px;
    background:var(--sunken);border-radius:8px;
  }
  .kbar{
    display:block;height:100%;background:var(--accent);
    border-radius:0 3px 3px 0;
  }
  .kbase{
    position:absolute;top:-3px;bottom:-3px;width:1px;
    background:var(--muted);
  }
  .kval{
    font-family:var(--mono);font-size:11px;color:var(--muted);
    text-align:right;font-variant-numeric:tabular-nums;
  }
  .ktrend{
    font-family:var(--mono);font-size:10.5px;font-weight:700;
    color:var(--muted);text-align:right;
    font-variant-numeric:tabular-nums;
  }
  .ktrend.better{color:var(--teal-ink);}
  .ktrend.worse{color:var(--brake);}

  .kmiss{
    font-family:var(--mono);font-size:11px;font-weight:700;
    color:var(--brake);text-align:right;
  }
  .chart-note{
    margin:2px 0 0;font-size:11px;color:var(--muted);line-height:1.4;
  }

  /* The per-finger breakdown. One full-width block under the two columns, in
     hand order, so it reads as a picture of your hands rather than a league
     table. It shares .kbase, .kval and .kmiss with the key chart above it on
     purpose: the two panels answer the same question at two grains, and a
     second set of near-identical marks would say they were unrelated. */
  .fin-fingers{
    display:flex;flex-direction:column;gap:8px;
    width:100%;max-width:800px;text-align:left;
  }
  .finger-verdict{
    margin:0;font-size:13px;line-height:1.45;color:var(--navy);
  }
  .fingerchart{display:flex;flex-direction:column;gap:5px;}
  .frow{
    display:grid;grid-template-columns:84px 1fr 62px 24px;
    gap:9px;align-items:center;
  }
  .fname{
    font-size:11px;color:var(--muted);font-weight:600;
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  }
  .ftrack{
    display:block;position:relative;height:13px;
    background:var(--sunken);border-radius:8px;
  }
  .fbar{
    display:block;height:100%;background:var(--accent);
    border-radius:0 3px 3px 0;
  }
  /* A finger that has not been pressed enough times to have a mean shows its
     press count instead, and nothing that looks like a measurement. */
  .frow[data-ms=""] .kval{font-style:italic;}
  .frow[data-f="th"] .fbar{background:var(--muted);}
  .chart-empty{
    margin:0;font-size:13px;color:var(--muted);
  }

  /* Result card — sized and contrasted to survive being screenshotted and
     posted somewhere else, where none of this page's context comes with it. */
  .card{
    width:100%;max-width:420px;
    background:var(--card);
    border:1px solid var(--edge);
    border-radius:20px;
    box-shadow:0 1px 2px rgba(8,29,73,.05), 0 6px 18px rgba(8,29,73,.05);
    padding:16px 18px 18px;
    display:flex;flex-direction:column;gap:12px;
    text-align:left;
  }
  .card-top{
    display:flex;justify-content:space-between;align-items:baseline;gap:10px;
    font-size:10px;letter-spacing:.16em;text-transform:uppercase;font-weight:600;
  }
  .card-mark{color:var(--accent);}
  .card-date{color:var(--muted);}
  .card-place{
    font-family:var(--display);font-weight:700;font-size:30px;
    line-height:1.05;letter-spacing:.01em;
  }
  .card-stats{
    display:flex;gap:22px;flex-wrap:wrap;
    border-top:1px solid var(--line);border-bottom:1px solid var(--line);
    padding:11px 0;
    font-variant-numeric:tabular-nums;
  }
  .card-stats div{display:flex;flex-direction:column;gap:1px;}
  .card-stats .k{
    font-size:9px;letter-spacing:.15em;text-transform:uppercase;
    color:var(--muted);font-weight:600;
  }
  .card-stats b{font-family:var(--display);font-weight:700;font-size:24px;}
  .card-stats .u{font-size:10px;color:var(--muted);margin-top:-2px;}
  .card-meta{font-size:12px;color:var(--muted);}
  .card-grid{
    display:flex;gap:10px;flex-wrap:wrap;
    font-size:11.5px;color:var(--muted);
    font-variant-numeric:tabular-nums;
  }
  .card-grid span{display:flex;align-items:center;gap:5px;}
  .card-grid i{width:7px;height:7px;border-radius:1px;font-style:normal;flex:none;}
  .card-grid b{color:var(--navy);font-weight:600;}
  .card-grid .beaten{color:var(--accent);}

  .share-link{
    width:100%;max-width:420px;margin-bottom:6px;
    font-family:var(--mono);font-size:11px;
    color:var(--muted);background:var(--sunken);
    border:1px solid var(--line);border-radius:8px;
    padding:7px 9px;
  }
  .share-link:focus{outline:2px solid var(--accent);outline-offset:1px;color:var(--navy);}

  .coach{
    margin:0;max-width:min(60ch,100%);
    font-size:13px;line-height:1.5;color:var(--navy);
    border-left:3px solid var(--accent);
    padding:2px 0 2px 11px;text-align:left;
  }
  .coach:empty{display:none;}
  .coach-do{
    font-size:12px;padding:8px 16px;
    align-self:flex-start;margin:-4px 0 0 14px;
  }
  .coach-do[hidden]{display:none;}
  .coach-do.done{
    color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent-soft);
    cursor:default;
  }

  .pb-note{
    margin:0;font-size:12px;font-weight:600;
    letter-spacing:.14em;text-transform:uppercase;
    color:var(--accent);
  }
  .pb-note.off{color:var(--muted);}
  .pb-note:empty{display:none;}

  /* ---------- typing strip ---------- */
  /* No background, no border, no shadow. This held the pace card; it now holds
     only the overtake feed, and a panel that is empty for most of a race is
     louder than the line inside it. */
  .strip{
    display:flex;flex-direction:column;
    margin-top:10px;
  }
  /* The band reserved for the word at the top of the play panel. Its height is
     the word row plus the guide, and neither depends on the word: the row is a
     fixed height and the word is a full-width block that centres its glyphs, so
     the element's box is identical for "at" and for "extraordinary" and
     identical on every keystroke within a word. */
  /* A CONTAINER, so the word can be sized by the box it is in rather than by the
     window. The shell's width is now bounded by the window's HEIGHT (see .shell),
     so vw stopped being a proxy for how much room the word has: a 1600x600 window
     is wide and gives the word the same 816px band a 1440x900 one does. The word
     scales on cqi below, which is that band, measured. */
  .word-stage{
    padding:14px 18px 12px;
    display:flex;flex-direction:column;gap:9px;
    border-bottom:1px solid var(--line);
    container-type:inline-size;
  }
  /* THE PANEL LABEL. Hidden below the two-column breakpoint, where the word
     band is competing with the circuit and the keyboard for a phone's screen. */
  .word-head{display:none;}
  /* 620px, NOT the 900 the word-size rule uses. That number is a measurement of
     when the word can afford to grow; this one is only asking whether two short
     lines of label fit above it, which they do far earlier. The panel is about
     855px wide on a 1440 desktop, so a 900px query never fired here at all. */
  @container (min-width:620px){
    .word-head{
      display:flex;flex-direction:column;align-items:center;gap:1px;
      margin-bottom:-2px;
    }
    .word-eyebrow{
      font-family:var(--display);font-weight:800;font-size:11px;
      letter-spacing:.14em;text-transform:uppercase;color:var(--accent);
    }
    .word-title{
      font-family:var(--display);font-weight:900;font-size:21px;color:var(--navy);
    }
  }
  .word-line{
    position:relative;
    display:flex;align-items:center;justify-content:center;
    height:56px;overflow:hidden;
  }
  .word{
    /* THE HANDOFF'S ROUNDED FACE, not the mono this used to be. The gate that
       guards this box MEASURES the widest word the pools can serve rather than
       computing it from a per-character width, so the change is safe in the
       direction that matters: a proportional face is narrower than a monospace
       one at the same size, so every word that fitted still fits with room. */
    font-family:var(--display);font-weight:900;letter-spacing:-.01em;
    /* The two-column layout starts at 881px, and at 881px this box is about
       331px wide — thirteen monospace characters at 40px, against a pool that
       reaches "straightforward" plus a capital, a mark and the trailing space:
       seventeen. The word is nowrap and overflow:hidden and centred, so it was
       clipped at BOTH ends and the cursor could sit outside the box. Every
       narrower breakpoint already clamps; this one never did, and 881-1024px is
       a band the matrix had no row in. The upper bound is the old fixed size, so
       nothing changes on a normal desktop. */
    font-size:clamp(22px,3.3vw,40px);letter-spacing:.01em;
    white-space:nowrap;overflow:hidden;
    line-height:1.1;
    /* Centred across the whole band, with the sides kept clear of the queue that
       is parked at the right end of it. The padding is a percentage of a box
       whose width does not depend on the word, so the reserved area is the same
       for every word — it keeps the two apart at a narrow desktop window without
       either one being sized by its contents. */
    width:100%;text-align:center;padding:0 18%;
  }
  /* THE WORD IS THE FOCAL POINT AND IT IS A FIGURE, NOT PROSE (rule 246).
     A measure exists so the eye can find the start of the next line; a single
     word on one line has no next line, and the thing that matters instead is
     that it can be read at a glance from where the player is sitting. So it is
     the one piece of TEXT here that scales with the panel, and it scales on the
     band's own width rather than the window's.

     WHY IT STARTS AT 900cqi. That is the band a 1264px shell gives, which is the
     narrowest shell the height-bounded .shell rule can produce above a 1020px
     viewport — so this turns on exactly where the extra room appears and never
     on a screen the old matrix covers. The floor is 40px, the size every desktop
     drew before, so nothing shrinks (rule 20's other half: a new tier must be
     checked for what it takes away as well as what it adds).

     WHY 5.2cqi AND WHY IT STOPS AT 56px. The band gives the word 64% of its
     width (padding: 0 18% each side), and the widest string the pools can serve
     — check-layout derives it, "Straightforward's?" plus its space — measures
     10.38px per px of font size. 5.2cqi therefore uses 5.2 x 10.38 / 0.64 = 84%
     of the room at every width, leaving the same proportional margin the 40px
     size had at 1440. The 56px ceiling is a legibility judgement rather than a
     fitting one: past it a word stops being one glance and starts being a
     sentence to scan, and the line box below is sized to hold it. */
  @container (min-width:900px){
    .word{font-size:clamp(40px, 5.2cqi, 56px);}
    /* 56px at line-height 1.1 is 61.6px and the band's row is 56px with
       overflow:hidden, which would slice the descenders off the largest size the
       rule above can ask for. 72px clears it with room for the cue's underline.
       The 16px this adds to the band is paid for in .shell's height budget. */
    .word-line{height:72px;}
  }
  .word .done{color:var(--teal);}
  /* The handoff tiles the current letter in a pale blue rather than reversing it
     out on a solid one: at 40-56px a full-strength fill behind a single glyph is
     a block of colour with a letter lost inside it. Navy on --accent-soft is
     14.7:1; the ring keeps the tile findable when the wash alone is too quiet. */
  .word .cur{
    color:var(--navy);background:var(--accent-soft);
    border-radius:6px;
    box-shadow:0 0 0 2px var(--accent);
  }
  .word .rest{color:var(--navy);opacity:.38;}
  /* a space you have to type has to be something you can see */
  .word .sp{
    border-bottom:2px solid var(--accent);
    border-radius:0 0 2px 2px;
  }
  .word .rest.sp{border-bottom-color:var(--edge);}
  /* The error cue may not MOVE the word. This animation used to shake it
     sideways by up to 6px, and getBoundingClientRect reports a transform, so
     #word's left read 47 / 45 / 43 / 41 / 51 depending on how far into the
     .18s the sample landed. That is the "screen jumps while I start typing"
     bug this project was founded on, in the one place feedback was allowed to
     move a box. It hid for many iterations because the assertion measured only
     the word's top, and it only ever showed up in a full suite because the
     animation runs on real time: standalone, the next keystroke arrives within
     a millisecond of the mistake and the sample lands at translateX(0).
     So the cue is now colour only — background and shadow take part in no
     layout and are invisible to getBoundingClientRect. Same rule the key
     diagram already states: a highlight is a colour and a shadow, never a
     border, a padding or a nudge. */
  .word.err{animation:jolt .18s ease;}
  /* And the mistake is not only red. Red-on-white is a hue, and the one thing
     the cursor may never do is MOVE, which rules out a border, a padding or a
     nudge — so the redundant cue is a wavy underline, which text-decoration
     paints inside the existing line box and which getBoundingClientRect cannot
     see. Greyscale, deuteranopia, protanopia, tritanopia: the wave is there in
     all four. */
  .word.err .cur{
    background:var(--brake);color:#fff;
    text-decoration:underline wavy #fff;
    text-underline-offset:2px;
  }
  @keyframes jolt{
    0%,100%{background-color:transparent;box-shadow:none;}
    35%{background-color:rgba(255,79,90,.26);
        box-shadow:0 0 0 2px rgba(255,79,90,.55) inset;}
  }
  /* Out of flow, at the right end of the fixed-height row: the queue's width
     changes every word, and in flow that width would have decided where the
     centred word sat. Its parent has an explicit height, so this is not the
     inset:0 overlay that has inherited a sibling's height four times here. */
  .queue{
    position:absolute;right:0;top:50%;transform:translateY(-50%);
    max-width:16%;
    display:flex;gap:14px;align-items:center;
    font-family:var(--mono);font-size:17px;color:var(--muted);
    white-space:nowrap;overflow:hidden;
  }
  .queue span:nth-child(2){opacity:.6;}
  .queue span:nth-child(3){opacity:.35;}
  /* "Up next: last", as the handoff writes it. The label is generated rather
     than shipped in the markup because the queue is rebuilt every word, and a
     label inside that box would have to be re-added every time. Only where
     there is room: on a phone the queue is display:none already. */
  @container (min-width:620px){
    .queue::before{
      content:"Up next:";
      font-family:var(--display);font-weight:700;font-size:14px;
      color:var(--muted);letter-spacing:0;
    }
    .queue{font-family:var(--display);font-weight:800;font-size:18px;color:var(--navy);}
    .queue span:nth-child(n+2){display:none;}
  }

  /* ---------- home-position keyboard guide ----------
     Every key is the same size whether or not it is lit: the highlight is a
     colour and a shadow, never a border or a padding, because the one bug this
     project has shipped most is the screen moving while somebody is typing. */
  .kb{
    display:flex;flex-direction:column;gap:4px;align-items:center;
    padding:2px 0 0;
  }
  .kb[hidden]{display:none;}
  body.touch .kb{display:none;}
  body.touch .kg-row{display:none;}
  .kb-row{display:flex;gap:4px;justify-content:center;}
  /* display:flex would override the hidden attribute, and the digits row is
     hidden on every step but one — so the row has to say so itself. */
  .kb-row[hidden]{display:none;}
  .kbk{
    position:relative;
    width:26px;height:26px;line-height:24px;
    font-family:var(--mono);font-size:12px;font-weight:400;
    text-align:center;
    border-radius:10px;
    /* WHITE, NOT THE WELL GREY. --muted on --sunken measures 4.38:1 at 12px and
       fails AA; on card white it is 4.74:1. The handoff draws these keys white
       anyway - it is the OS keyboard they are standing in for. */
    background:var(--card);
    border:1px solid var(--line);
    color:var(--muted);
  }
  .kbk.home{color:var(--navy);border-color:var(--accent);background:var(--accent-soft);}
  /* the two keys your index fingers find without looking */
  .kbk.anchor::after{
    content:"";position:absolute;left:7px;right:7px;bottom:3px;height:2px;
    background:var(--accent);border-radius:1px;
  }
  .kb-space{width:132px;}
  .kb-shift{width:52px;font-size:9px;letter-spacing:.05em;}
  /* A LIT KEY IS NAVY ON ITS FINGER'S COLOUR, and it used to be --paper on it.
     That was right while --paper was near-black; on a light page it became
     white-on-pastel and measured 1.8:1. Navy clears 4.5:1 on all nine. */
  .kbk.on{
    background:var(--kbc);border-color:var(--kbc);
    color:var(--navy);font-weight:700;
    box-shadow:0 0 0 2px var(--kbc), 0 0 12px rgba(8,29,73,.12);
    animation:kbpulse 1.1s ease-in-out infinite;
  }
  .kbk.on.anchor::after{background:var(--navy);}
  /* WHICH FINGER, IN WRITING, ON THE LIT KEY ONLY.
     The colour of a lit key is the diagram's entire teaching content, and eight
     hues cannot be separated under any of the three dichromacies — so the lit
     key names its finger in the tutor's own notation (L1-L4, R1-R4, T) in the
     corner. Absolutely positioned and drawn only while .on, so an unlit diagram
     is exactly the diagram it was, and a key is the same size lit or not. */
  .kbk.on::before{
    content:attr(data-fx);
    position:absolute;top:0;left:0;right:0;
    font-family:var(--mono);font-size:7px;font-weight:700;line-height:8px;
    /* No opacity: compositing navy toward the key's own colour dropped this to
       4.03:1 on the left-little key, and this label is the cue that makes a
       nine-colour diagram readable without colour at all. */
    letter-spacing:0;color:var(--navy);
    pointer-events:none;
  }
  .kbk.on.kb-space::before{text-align:center;}
  .kbk[data-f="lp"]{--kbc:#e0605c;}
  .kbk[data-f="lr"]{--kbc:#e08a4c;}
  .kbk[data-f="lm"]{--kbc:#d7c052;}
  .kbk[data-f="li"]{--kbc:#8ac96a;}
  .kbk[data-f="ri"]{--kbc:#4bbf95;}
  .kbk[data-f="rm"]{--kbc:#4b96d6;}
  .kbk[data-f="rr"]{--kbc:#9a92ea;}
  .kbk[data-f="rp"]{--kbc:#d68ad0;}
  .kbk[data-f="th"]{--kbc:#86BDF7;}   /* not --accent: navy on that is 3.47:1 */
  @keyframes kbpulse{
    0%,100%{box-shadow:0 0 0 2px var(--kbc), 0 0 12px rgba(255,255,255,.18);}
    50%{box-shadow:0 0 0 2px var(--kbc), 0 0 18px rgba(255,255,255,.34);}
  }

  /* speed meter */
  /* The race commentary, for a screen reader only. It must take no space and
     add no scroll: the word above it moves for nothing. */
  .sr-live{
    position:absolute;width:1px;height:1px;
    margin:-1px;padding:0;border:0;overflow:hidden;
    clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;
  }

  /* ---------- standings ---------- */
  .standings{
    background:var(--card);
    border:1px solid var(--line);
    border-radius:20px;
    box-shadow:0 1px 2px rgba(8,29,73,.05), 0 6px 18px rgba(8,29,73,.05);
    padding:14px 15px 15px;
    display:flex;flex-direction:column;gap:12px;
  }
  .panel-title{
    font-size:10px;letter-spacing:.18em;text-transform:uppercase;
    color:var(--muted);font-weight:600;
    padding-bottom:9px;border-bottom:1px solid var(--line);
  }
  .grid-list{display:flex;flex-direction:column;gap:11px;}
  .entry{display:flex;flex-direction:column;gap:5px;}
  .entry-top{
    display:flex;align-items:center;gap:8px;
    font-size:14px;line-height:1.2;
  }
  .entry .pos{
    font-family:var(--display);font-weight:700;font-size:13px;
    color:var(--muted);width:15px;font-variant-numeric:tabular-nums;
  }
  /* THE CHIP IS THE CAR'S COLOUR AND THE CAR'S MARK. A 9px swatch said which
     car this row was in hue alone, which is the one channel that does not
     arrive for about one man in twelve; the seat mark inside it is what makes
     the row and the lozenge on the circuit the same car for everybody. Big
     enough to hold a character and no taller than the 16.8px line box it sits
     in, so no row anywhere grew by a pixel. */
  .chip{
    width:12px;height:12px;border-radius:8px;flex:none;
    font-family:var(--mono);font-size:8px;font-weight:700;line-height:12px;
    text-align:center;color:var(--paper);vertical-align:middle;
    overflow:hidden;
  }
  .entry .nm{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
  .entry .gap{
    font-family:var(--mono);font-size:11px;color:var(--muted);
    font-variant-numeric:tabular-nums;
  }
  .entry.you .nm{color:var(--player-ink);font-weight:700;}
  .entry.you .pos{color:var(--player-ink);}
  .lapbar{
    position:relative;height:4px;background:var(--sunken);border-radius:1px;
    overflow:hidden;
  }
  .lapbar span{position:absolute;inset:0 auto 0 0;width:0;border-radius:1px;}
  .lapbar u{
    position:absolute;top:0;bottom:0;width:1px;
    background:var(--line);text-decoration:none;
  }

  .form{
    border-top:1px solid var(--line);padding-top:11px;
    display:flex;flex-direction:column;gap:7px;
  }
  .form[hidden]{display:none;}
  .form-head{
    display:flex;justify-content:space-between;align-items:baseline;gap:8px;
    font-size:10px;letter-spacing:.16em;text-transform:uppercase;
    color:var(--muted);font-weight:600;
  }
  .spark{display:block;width:100%;height:auto;overflow:visible;}
  .form-stats{
    display:flex;justify-content:space-between;gap:8px;
    font-variant-numeric:tabular-nums;
  }
  .form-stats div{display:flex;flex-direction:column;gap:0;}
  .form-stats .k{
    font-size:9px;letter-spacing:.14em;text-transform:uppercase;
    color:var(--muted);font-weight:600;
  }
  .form-stats b{
    font-family:var(--display);font-weight:600;font-size:15px;color:var(--navy);
  }
  .form-stats .up b{color:var(--teal-ink);}

  .career{
    font-size:11px;line-height:1.45;color:var(--muted);
    border-top:1px solid var(--line);padding-top:8px;
  }
  .career:empty{display:none;}
  .career b{color:var(--navy);font-weight:600;}

  .drill{
    border-top:1px solid var(--line);padding-top:11px;
    display:flex;flex-direction:column;gap:7px;
  }
  .drill[hidden]{display:none;}
  .drill .hd{
    display:flex;justify-content:space-between;align-items:baseline;gap:8px;
    font-size:10px;letter-spacing:.16em;text-transform:uppercase;
    color:var(--muted);font-weight:600;
  }
  .drill .hd button{
    font-family:var(--ui);font-size:10px;letter-spacing:.1em;
    text-transform:uppercase;font-weight:600;
    background:none;border:0;padding:0;cursor:pointer;
    color:var(--muted);text-decoration:underline;text-underline-offset:2px;
  }
  .drill .hd button:hover{color:var(--navy);}
  .drill .hd button:focus-visible{outline:1px solid var(--navy);outline-offset:2px;}
  .keys{display:flex;gap:5px;flex-wrap:wrap;}
  .keys b{
    font-family:var(--mono);font-weight:700;font-size:13px;
    color:var(--accent);
    background:var(--sunken);
    border:1px solid var(--accent-soft);
    border-radius:8px;
    min-width:22px;padding:2px 0;text-align:center;
  }
  .drill .why{font-size:11px;color:var(--muted);line-height:1.4;}
  .verdict{
    font-size:11px;font-weight:600;color:var(--muted);
    letter-spacing:.04em;
  }
  .verdict:empty{display:none;}
  .verdict.better{color:var(--teal-ink);}
  .verdict.worse{color:var(--brake);}

  .pb-line{
    border-top:1px solid var(--line);padding-top:11px;
    display:flex;justify-content:space-between;align-items:baseline;gap:10px;
    font-size:10px;letter-spacing:.16em;text-transform:uppercase;
    color:var(--muted);font-weight:600;
  }
  .pb-line b{
    font-family:var(--display);font-size:16px;color:var(--navy);
    letter-spacing:.02em;font-variant-numeric:tabular-nums;
  }
  .pb-line b.none{color:var(--muted);}

  .entry.ghost .nm{opacity:.7;font-style:italic;}
  /* The ghost's chip is hollow — a ring, not a fill — which is already a
     non-colour cue, and its G has to be readable against the panel rather than
     against a livery, so it takes the ring's ink instead of the dark one. */
  .chip.hollow{
    background:transparent !important;box-shadow:inset 0 0 0 1.5px var(--accent);
    color:var(--accent);
  }

  .legend{
    border-top:1px solid var(--line);padding-top:11px;
    font-size:12px;color:var(--muted);line-height:1.5;
  }
  .legend b{color:var(--navy);font-weight:600;}
  .legend kbd{
    font-family:var(--mono);font-size:11px;
    background:var(--sunken);border:1px solid var(--edge);
    border-radius:8px;padding:1px 5px;color:var(--navy);
  }

  .colophon{
    color:var(--muted);font-size:12px;
    border-top:1px solid var(--line);padding-top:12px;
    display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;
  }

  #catcher{
    position:fixed;top:0;left:0;
    width:100%;height:1px;
    opacity:.01;border:0;padding:0;margin:0;
    background:transparent;color:transparent;
    font-size:16px;            /* anything smaller and iOS zooms the page */
    pointer-events:none;
  }

  .tap-hint{
    display:none;
    font-family:var(--ui);font-size:13px;font-weight:600;
    letter-spacing:.1em;text-transform:uppercase;
    color:var(--accent);
    background:var(--sunken);
    border:1px dashed var(--accent-soft);
    border-radius:10px;
    padding:11px 14px;width:100%;
    cursor:pointer;
  }
  body.touch .tap-hint:not([hidden]){display:block;}
  .tap-hint:focus-visible{outline:2px solid var(--navy);outline-offset:2px;}

  @media (max-width:880px){
    /* A tighter gap now that the pace card is an .arena child rather than a
       .panel-col one: it inherited the 18px desktop gap and that pushed the word
       4px under a landscape keyboard. */
    .arena{grid-template-columns:minmax(0,1fr);grid-template-rows:none;gap:10px;}
    /* One column: the explicit desktop placement is cleared so the three
       children flow, and .strip's order:-1 below puts the pace card back on top
       where a phone has always had it. */
    .arena > .panel-col,.arena > .standings,.arena > .strip{
      grid-column:auto;grid-row:auto;
    }
    .shell,.arena > *,.panel-col,.track-wrap,.overlay,.ov-card,
    .round-card,.round-list,.ways,.course,.champ{ min-width:0; max-width:100%; }
    .ov-head{flex-wrap:wrap;}
    .ov-head h2{flex:1 1 auto;min-width:0;}
    .brand h1{font-size:24px;}
    .readouts{gap:18px;}

    /* The soft keyboard swallows the bottom half of the screen, so the word
       being typed has to sit above the track, not below it. */
    .panel-col{display:flex;flex-direction:column;}
    .strip{order:-1;margin-top:0;margin-bottom:10px;}

    /* The setup card is taller than the track panel on a phone, so floating it
       over the canvas put START ENGINE below an internal scrollbar where nobody
       would find it. Let the overlays flow underneath the circuit instead: the
       panel grows to fit and nothing is ever hidden behind a fold. */
    .overlay{
      position:static;
      background:transparent;
      overflow:visible;
      padding:13px 2px 4px;
      margin-top:11px;
      border-top:1px solid var(--line);
    }
    .track-wrap > .overlay{ background:var(--paper); }
    /* THE CANVAS IS NOT HIDDEN WHILE A MENU IS UP, and the reason is a rule two
       files away. Hiding it with display:none was tried: a display:none canvas
       has a clientWidth of 0, unitsPerPx() collapses to 1, and drawMarks then
       sizes every seat mark at 11 CANVAS units instead of 11 screen pixels -
       the responsive matrix caught "Y" drawn at 3.66px against a 10px floor, on
       ten screens at once (rule 198). It is also unnecessary: the overlay above
       is opaque, so nothing shows through it, and min-height:0 means the panel
       no longer reserves height the canvas will not fill. */
    .overlay-foot{position:static;background:none;padding:6px 0 0;}
    .overlay h2{font-size:21px;}
    .overlay .sub{font-size:13px;}
    .go-word{font-size:38px;}
    .setup-why{font-size:10.5px;}

    /* Centred on the play panel and no queue beside it, so the word can have the
       whole width and be read at a glance from where the thumbs are. */
    /* 8vw clipped the pool's widest word by 2px on a 390px phone and left 2px
       on a 412px one — the box centres what it cannot fit, so that is 1px off
       each end and a cursor that can sit outside the box. */
    .word{font-size:clamp(25px,7.6vw,34px);padding:0;}
    .word-line{height:46px;}
    .word-stage{padding:11px 12px 10px;}
    .queue{display:none;}
    .ev{font-size:11px;padding:4px 8px 4px 7px;}
    .fin-grid{grid-template-columns:1fr;gap:20px;}
    .overlay{gap:12px;padding:16px;}
    .overlay h2{font-size:24px;}
    .tally{gap:18px;}
    .tally .v{font-size:19px;}
    .seg button{padding:7px 9px;font-size:10.5px;letter-spacing:.04em;}
    .seg-row{gap:8px;}
    .seg-label{width:auto;font-size:9px;letter-spacing:.1em;}
    .grid-preview{gap:9px;font-size:12px;}
    .shell{padding:14px 13px 28px;gap:13px;}
    .masthead{padding-bottom:10px;}
  }

  /* Short windows: the results are the tallest thing in the app, so trim them
     rather than make the reader scroll a scoreboard. */
  @media (max-height:720px){
    .overlay{padding:16px;gap:11px;}
    .overlay h2{font-size:25px;}
    /* Five setting rows now, and a short window is where they run out of panel */
    .setup-panel{gap:7px;}
    .help-body{gap:11px;}
    .help-sec p{font-size:12.5px;line-height:1.5;}
    .setup{gap:9px;}
    .seg button{padding:6px 14px;}
    /* The course row is one more thing above Start engine, and a short window is
       exactly where the settings below it got pushed out of reach. */
    .course{
      flex-direction:row;flex-wrap:wrap;align-items:baseline;
      justify-content:center;gap:3px 10px;padding-bottom:7px;
    }
    .course-line{font-size:13px;}
    .course-bar{font-size:11px;line-height:1.35;}
    .ways .btn{padding:8px 18px;font-size:13px;}
    .ways .btn-quiet{padding:6px 14px;font-size:11px;}
    .champ-result{gap:6px;}
    .course-verdict{font-size:12px;line-height:1.4;}
    .tally{gap:20px;}
    .tally .v{font-size:20px;}
    .fin-grid{gap:22px;}
    .results td{padding:5px 0;font-size:13px;}
    .results th{padding-bottom:4px;}
    .keychart{gap:4px;}
    .ktrack{height:11px;}
    .chart-note{font-size:10px;}
    .overlay-foot{padding:10px 0 0;}
  }

  /* A laptop in a short window is where the start screen runs out of room
     altogether: two entries, five setting rows and a grid preview above the
     start button. Everything shrinks by a few pixels rather than anything being
     taken away, because a control that is off the bottom cannot be pressed and
     a control that is not there cannot be found. Ordered after the 720px block
     so it wins where both apply. */
  @media (max-height:660px){
    .overlay{padding:12px;gap:8px;}
    .overlay h2{font-size:22px;}
    .overlay .sub{font-size:12.5px;}
    .setup{gap:6px;}
    .setup-panel{gap:5px;}
    .seg button{padding:4px 11px;font-size:11px;}
    .seg-label{font-size:10px;}
    .setup-why{font-size:10.5px;}
    .course{gap:2px 9px;padding-bottom:5px;}
    /* And the results screen, which now carries a standings table under the
       verdict on every championship round. */
    .tally .v{font-size:18px;}
    .course-verdict{font-size:11.5px;line-height:1.35;}
    .champ-result{gap:4px;}
    .champ-table td{padding:2px 0;font-size:11.5px;}
    .champ-table th{padding-bottom:2px;font-size:10px;}
    .coach{font-size:12px;line-height:1.4;}
    .coach-do{padding:5px 12px;font-size:11px;}
    .results td{padding:4px 0;font-size:12px;}
    .keychart{gap:3px;}
    .fin-grid{gap:16px;}
  }


  /* Short phones: an iPhone SE leaves about 367px above the keyboard, and the
     race view has to fit in it. Every rule here buys vertical space. */
  @media (max-height:720px) and (max-width:560px){
    .shell{padding:10px 12px 22px;gap:9px;}
    .masthead{padding-bottom:8px;gap:8px;}
    .brand h1{font-size:19px;}
    .brand p{font-size:9.5px;letter-spacing:.06em;}
    .readout .v{font-size:17px;}
    .readout .v small{font-size:11px;}
    .readout .k{font-size:9px;letter-spacing:.1em;}
    .strip{padding:0;}
    .word-stage{padding:8px 11px 8px;gap:6px;}
    .word-line{height:38px;}
    .word{font-size:clamp(22px,7vw,30px);}
    .events{height:19px;}
    .ev{font-size:10px;padding:3px 7px 3px 6px;}
    .tap-hint{padding:8px 12px;font-size:11.5px;}
  }

  /* Landscape phones. People turn the phone sideways because the keyboard is
     bigger — which leaves under 200px of height above it. Everything that is not
     the race has to go: the strapline, most of the header, and every spare pixel
     of padding. Measured target is the word, the event row and the pace bar all
     inside the top ~185px.

     RULE 78, and this block was on the wrong side of it in the shipped CSS. It
     sat ABOVE the (max-height:720px) and (max-width:560px) tier, which is the
     same specificity, so on any landscape viewport narrower than 560px — a
     small phone turned sideways, a floating or split-screen window — every
     saving here was overwritten by the taller short-phone tier that follows it.
     It went unseen because no row of the responsive matrix was landscape under
     560px (rule 70: the property every row shares is the one nobody chose).
     This block must stay LAST of the two: it is the narrowing tier. */
  @media (orientation:landscape) and (max-height:460px){
    /* THE PILL COSTS HEIGHT, AND HERE THERE IS NONE. The handoff's button is
       17px at 14px of vertical padding, which is right on a desktop and put
       "Championship" 56px below the fold on every landscape phone in the matrix.
       Same shape, same colour, two thirds of the box - the fold budget is not
       negotiable (rule 150) and a way into the game that has to be scrolled to
       is the defect this whole tier exists to prevent. */
    .btn{font-size:13px;padding:7px 18px;}
    .overlay h2{font-size:18px;}
    .overlay{gap:6px;}
    .ov-card{gap:6px;}
    /* The trophy legend is three pills of information on a screen that has room
       for the way into the game and little else. It is on the results screen
       too, where it is the point; here it is reference. */
    .course-bar,.champ .course-bar{font-size:10px;line-height:1.3;}
    .overlay .sub{font-size:11px;}
    .course-line{font-size:11px;}
    /* THE SIZE MOVES WITH THE FILL. paintCourse() swaps which of these two is
       the filled button, and check-layout asserts the loud one is also the
       BIGGER one - a quiet button the same size as the loud one leaves the
       abandoning control equally prominent. Written against .btn-quiet rather
       than against either id, exactly as the full-size rules above are, so the
       hierarchy survives the swap in both directions. */
    .ways .btn{font-size:12.5px;padding:6px 16px;}
    .ways .btn-quiet{font-size:11px;padding:4px 14px;}
    .shell{padding:5px 10px 14px;gap:5px;}
    .masthead{padding-bottom:4px;gap:10px;align-items:center;flex-wrap:nowrap;}
    .brand h1{font-size:14px;letter-spacing:0;}
    .brand p{display:none;}
    .readouts{gap:13px;}
    .readout{min-width:0;}
    .readout .k{font-size:8px;letter-spacing:.06em;}
    .readout .v{font-size:14px;}
    .readout .v small{font-size:9px;}

    /* THE LABEL AND THE NEXT WORD STAY, NOW THAT THEY COST NO HEIGHT.
       They were hidden here because stacked above the circuit they took ~36px of
       the ~185px a landscape phone leaves above the keyboard - measured, as 4px
       of the word under the keyboard on a landscape iPhone SE. Side by side with
       the circuit they take width instead, and width is the one thing this
       orientation has. */

    /* THE PANEL LABEL GOES, AND THE CONTAINER QUERY IS WHY IT HAD TO BE SAID
       HERE. "Your next move / Type this word" is shown on a panel wider than
       620px, and a landscape phone is 844px wide - so it appeared, cost two
       lines, and pushed the word 24px under the soft keyboard. Panel width was
       the right question for whether the label FITS and the wrong one for
       whether it is affordable; this tier answers the second. */
    .word-head{display:none;}

    /* A landscape phone leaves about 185px above the keyboard, and the word band
       now sits below the strip rather than inside it — so every pixel between
       the header and the band is one the word has to spend to stay visible. */
    .strip{padding:0;margin-bottom:2px;}
    .word-stage{padding:4px 10px 4px;gap:4px;}
    .word-line{height:28px;}
    .word{font-size:clamp(16px,3.4vw,23px);}
    .events{height:16px;}
    .ev{font-size:9.5px;padding:2px 6px 2px 5px;}
    .tap-hint{padding:6px 10px;font-size:10.5px;}

    /* The overlay's box is as tall as the circuit panel beside it, and on a wide
       landscape phone the canvas is taller than the card - so "safe center" was
       spending 55px of the scarcest height in the app on centring the card in a
       box it does not fill. On a screen this short the card starts at the top.
       Measured: it moved the championship entry from 375px to 320px on an
       844x390 phone, where the fold is at 390px. */
    .overlay{gap:7px;padding:9px 2px 2px;margin-top:5px;justify-content:flex-start;}
    .overlay h2{font-size:18px;}
    /* A 44ch column of strapline in a 667px-wide window is four lines of text
       where two will do, and in landscape every one of those lines pushes the
       ways in towards the fold. Let it use the width the orientation gives. */
    .overlay .sub{font-size:11.5px;max-width:min(66ch,100%);}
    /* The three ways in have to clear 375px of height between them, the header
       and the strip, so they are packed tighter here than anywhere else. */
    .ways{gap:5px;}
    /* The help dialog is nine sections tall and this is the shortest screen in
       the matrix, so it reads at the smallest size that still measures above AA
       rather than being cut down to fewer sections. */
    .ov-head{gap:8px;}
    .help-btn{font-size:9.5px;padding:4px 8px;}
    .help-body{gap:9px;}
    .help-sec h3{font-size:10px;margin-bottom:2px;}
    .help-sec p{font-size:11.5px;line-height:1.45;}
    .course{gap:2px 8px;padding-bottom:3px;}
    .setup{gap:7px;}
    .setup-why{font-size:10px;}
    .card-place{font-size:22px;}
    .card-stats b{font-size:18px;}
  }

  @media (max-width:520px){
    .brand p{font-size:11px;letter-spacing:.08em;}
    .readouts{gap:13px;width:100%;justify-content:space-between;}
    .readout{min-width:0;}
    .readout .v{font-size:20px;}
    .colophon{flex-direction:column;gap:5px;}
  }
  @media (prefers-reduced-motion:reduce){
    .ev{animation:none;}
    .btn{transition:none;}
    .btn:hover{transform:none;}
    .word.err{animation:none;}
    .kbk.on{animation:none;}
    .lights span{transition:none;}
    .seg button{transition:none;}
    *,*::before,*::after{scroll-behavior:auto;}
  }

  /* THE PHONE'S VERTICAL BUDGET, SPENT DELIBERATELY.
     Measured on an iPhone 15 (393x852) mid-race, before this block: masthead
     121px, pace card 127px, word card 109px, and the track running 402->612.
     A keyboard takes ~336px, so the usable height is ~516 and 96px of the track
     was behind the keyboard - which is what the real device showed.
     Nothing here shrinks the circuit or changes its aspect. The three blocks
     ABOVE it give up 121->68, 127->84 and 109->76, which moves the track up by
     ~129px and puts all of it above the keyboard with room to spare. */
  @media (max-width:720px) and (orientation:portrait){
    /* STICKY, AND SMALLER. The header is the one thing that should not scroll
       away mid-race - it carries the lap and the accuracy - so it stays put and
       pays for that by costing half of what it did. The brand and its strapline
       share a line instead of stacking. */
    .masthead{
      position:sticky;top:0;z-index:5;
      background:var(--paper);
      padding:2px 0 6px;margin:0;gap:6px;
      align-items:baseline;flex-wrap:wrap;
    }
    .brand{flex-direction:row;align-items:baseline;gap:8px;flex-wrap:wrap;}
    .brand h1{font-size:20px;}
    .brand p{font-size:11px;}
    .readouts{width:100%;justify-content:space-between;gap:8px;}
    .readout{min-width:0;gap:0;}
    .readout .k{font-size:9px;letter-spacing:.1em;}
    .readout .v{font-size:18px;}
    .readout .v small{font-size:11px;}

    /* The pace card was 127px for a 12px bar and one number. The Sound control
       and the live commentary both stay - a gate requires them - they are just
       not given a card's worth of padding each. */
    .strip{padding:0;margin-top:0;}
    .events{height:16px;}
    .ev{font-size:10px;padding:1px 6px;}

    /* The word card keeps all three of its rows - label, word, next word -
       and loses the padding between them. */
    .word-stage{padding:8px 12px 10px;gap:2px;}
    .word-stage .word-title{font-size:12px;line-height:16px;}
    .word-line{min-height:0;}
  }

  /* LANDSCAPE, WHERE THE CANVAS WAS NEVER CAPPED BY HEIGHT AT ALL.
     fitCanvas() sizes the bitmap from WIDTH and the element is width:100% with
     height:auto, so a landscape phone drew the circuit 489px tall inside a
     393px-tall window: the document ran to 1063px and the player saw a slice.
     Capping it is only half the answer, and capping it ALONE left the circuit
     marooned in the middle of an 815px panel with white space either side. The
     other half is below - the word takes the width the circuit cannot use.
     dvh rather than vh because a phone's toolbars move. The circuit keeps its
     shape and its unit space throughout; only its size changes, so unitsPerPx
     and the 11px seat marks are unaffected. */
  @media (orientation:landscape) and (max-height:560px){
    /* TWO COLUMNS, BECAUSE LANDSCAPE HAS WIDTH AND NOTHING ELSE.
       Stacked, the circuit had to be capped by the height left over and then sat
       in the middle of an 815px panel with white space either side - the shape
       of the window and the shape of the layout disagreeing.
       The canvas and the word band are already siblings in .track-wrap's grid,
       so this is a change of grid rather than of markup: the circuit takes the
       left column at whatever the height allows, and the word takes the width
       that is left instead of a strip of empty page.
       The overlay still spans both columns - a menu is not a race. */
    .track-wrap{
      grid-template-columns:auto minmax(0,1fr);
      grid-template-rows:auto;
      /* START, not centre. A landscape keyboard leaves ~190px and the header and
         pace card already spend ~110 of it, so the word has to be at the very
         top of this row or it is behind the keyboard - which is exactly what
         centring it did. */
      align-items:start;
      gap:14px;
    }
    /* The cap goes on the canvas's OWN box, never on the canvas. check-layout
       squeezes the element to 93% OF ITS PARENT (rule 243); capping the canvas
       below that made the squeeze a grow - 333px to 749px - and the gate said
       so in those words. Capping the parent keeps 93% a genuine 93%. */
    .track-wrap > .track-box{
      grid-area:1 / 1;
      /* 52vw, not 58: on a 667px landscape phone the right column was left with
         216px and the widest word the pools can serve needs 248 at 28px - the
         gate measured exactly that and called it clipped at both ends. The
         circuit gives up a little width so the word has somewhere to live. */
      width:min(52vw, calc((100dvh - 150px) / 0.6));
    }
    .track-wrap > .word-stage{
      grid-area:1 / 2;
      border-bottom:0;
      /* Tight at the top: a landscape keyboard leaves ~190px and the word has to
         finish inside it. The gate measured the last version 2px over. */
      padding:2px 12px 8px;
      gap:0;
    }
    /* The word gets the column it is now standing in. Sized on cqi - the word
       band's own inline size, which in this layout is the right column - so it
       grows with the space rather than with the window. check-layout measures
       the widest word the pools can serve against this box, so the ceiling is
       what fits rather than what looked right in one screenshot. */
    /* Sized on the column it is in, with a floor low enough for the narrowest
       landscape phone: at 216px of box the widest word needs 20px type, not 28.
       check-layout measures that word against this box, so the floor is a
       measurement rather than a preference. */
    .track-wrap > .word-stage .word{font-size:clamp(20px, 8cqi, 40px);}
    .track-wrap > .word-stage .word-title{font-size:12px;line-height:14px;}
    .track-wrap > .word-stage .queue{font-size:16px;}
    .track-wrap > .overlay{grid-area:1 / 1 / 2 / 3;}

    /* THE READOUTS STAY IN THE HEADER, AND A MEASUREMENT IS WHY.
       Stacking them as pills on the circuit was tried, to buy back the height a
       sideways row of four numbers spends. In landscape the masthead is already
       only 33px tall WITH the readouts inline - so the whole prize is about
       20px, and collecting it needs the readouts moved across containers in the
       DOM (they live in the header; the canvas is two levels away) or absolutely
       positioned against a box that is not their parent, which put them on top
       of the brand and the pace card on the first attempt.
       Twenty pixels is not worth that. The two-column layout above is where
       landscape's real space came from. */

  }
