    :root {
      color-scheme: dark;
      --glass: rgba(18, 28, 30, .48);
      --line: rgba(255,255,255,.18);
      --text: #f8fbfa;
      --muted: rgba(248,251,250,.68);
      --accent: #9fe0bd;
      --safe-bottom: max(14px, env(safe-area-inset-bottom));

      /* ARCADE TOKENS — microlet mode only (the chooser, the fare HUD, the shift card).
         The look is not "arcade cabinet" (scanlines, neon, a pixel face); it is the BUS.
         A Timorese mikrolet is already the loudest graphic object on the road: hand-cut
         vinyl numerals, a destination board wedged in the windscreen, stacked stripe
         bands in the flag's colours. That is a complete design language, it is specific
         to Timor-Leste rather than to Sega, and 31-microlet-livery.js already paints it.

         Five of the six colours are the flag plus one band off the game's own livery, so
         nothing here is invented and --gold/--blood are literally C.yellow and C.red from
         07-stage-select.js. --ink is green-biased rather than a neutral black so it sits
         with the landscape instead of punching a grey hole in it; --bone is warm because
         pure white on a low-poly sky reads as a browser overlay rather than as paint. */
      --ink: #0b0e0c;
      --ink2: #151b18;
      --bone: #f4f0e3;
      --gold: #ffc72c;
      --blood: #dc241f;
      --jade: #12a08a;
      /* Heavy condensed caps are the face of hand-made signage and destination boards.
         Impact ships on macOS and Windows, so this costs no webfont in a project that
         deliberately has none — but it does NOT ship everywhere, hence the condensed
         fallbacks. Impact also has NO BOLD and no italic: hierarchy has to come from
         size, colour and the skew, and every rule below sets font-weight:400 explicitly
         so the browser cannot synthesise a smeared faux-bold. */
      --arcade-display: Impact, Haettenschweiler, "Arial Narrow Bold", "Franklin Gothic Heavy", sans-serif;
      --arcade-util: "Arial Narrow", "Helvetica Neue Condensed", "Roboto Condensed", system-ui, sans-serif;
    }
    /* THE TWO SHARED DEVICES, so the chooser, the HUD and the shift card read as one set.

       Die-cut notched corners instead of a radius. A border-radius is the single
       strongest "modern app" signal on these screens, so there is not one left in
       microlet mode.

       NOTE THE FILTER, IT IS NOT A TIDY-UP: box-shadow is CLIPPED by clip-path, so a
       notched plate silently loses its hard offset shadow however the shadow is
       declared. filter:drop-shadow() follows the clipped alpha shape instead. The
       symptom of getting this wrong is a shadow that is simply absent. */
    .arcade-cut { clip-path: polygon(0 0, calc(100% - 13px) 0, 100% 13px, 100% 100%, 13px 100%, 0 calc(100% - 13px)); }
    .arcade-cut-lg { clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 20px 100%, 0 calc(100% - 20px)); }
    /* A true parallelogram, for the two controls that have to read as a lever. */
    .arcade-slant { clip-path: polygon(15px 0, 100% 0, calc(100% - 15px) 100%, 0 100%); }
    /* Three painted rules. Two read as a divider; three read as paint. */
    .arcade-band { display:flex; flex-direction:column; }
    .arcade-band i { display:block; font-style:normal; }
    .arcade-band i:nth-child(1) { height:5px; background:var(--gold); }
    .arcade-band i:nth-child(2) { height:3px; background:var(--blood); }
    .arcade-band i:nth-child(3) { height:2px; background:var(--bone); opacity:.55; }
    * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
    /* Holding a touch button selected its label and popped the iOS callout menu. The
       `user-select:none` on html/body below is correct but insufficient on WebKit for
       two reasons: the property needs the -webkit- prefix on older iOS, and FORM
       CONTROLS reset user-select in the UA stylesheet, so a <button> never inherited
       it however it was declared on an ancestor. Declared on * and again on the
       controls themselves. -webkit-touch-callout suppresses the long-press menu, which
       is a separate mechanism from selection and needs saying separately. */
    * { -webkit-user-select: none; -webkit-touch-callout: none; }
    [hidden] { display:none !important; }
    html, body { width:100%; height:100%; margin:0; overflow:hidden; background:#5b6794; font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif; touch-action:none; user-select:none; }
    canvas { display:block; width:100%; height:100%; outline:none; }
    body::after { content:""; position:fixed; inset:0; pointer-events:none; z-index:2; background:linear-gradient(180deg,rgba(255,255,255,.05),transparent 28%,transparent 70%,rgba(7,18,20,.13)), radial-gradient(circle at 50% 45%,transparent 48%,rgba(9,20,22,.14) 100%); }
    .glass { border:1px solid var(--line); background:var(--glass); backdrop-filter:blur(17px) saturate(135%); box-shadow:0 12px 34px rgba(15,31,32,.15); }
    .top-actions { position:fixed; z-index:6; top:max(12px,env(safe-area-inset-top)); right:max(12px,env(safe-area-inset-right)); display:flex; gap:4px; }
    /* iPhone does not expose arbitrary-element fullscreen in the owner's Safari build. Keep the
       control and tell the truth: Home Screen launch is the path that removes its 94px tab/address
       stack. Compact and anchored below the button so it never becomes another centre-screen HUD. */
    .fullscreen-help { position:fixed; z-index:23; top:calc(max(12px,env(safe-area-inset-top)) + 48px);
      right:max(12px,env(safe-area-inset-right)); width:min(286px,calc(100vw - 24px)); padding:12px 14px 13px;
      background:var(--ink); color:var(--bone); border:3px solid var(--gold);
      filter:drop-shadow(7px 7px 0 rgba(0,0,0,.58)); }
    .fullscreen-help strong, .fullscreen-help span { display:block; }
    .fullscreen-help strong { font-family:var(--arcade-display); font-size:21px; font-weight:400;
      letter-spacing:.04em; text-transform:uppercase; color:var(--gold); }
    .fullscreen-help span { margin-top:6px; font-family:var(--arcade-util); font-size:11px;
      font-weight:700; line-height:1.42; letter-spacing:.04em; }
    .fullscreen-help button { width:100%; min-height:36px; margin-top:10px; border:0; border-radius:0;
      background:var(--blood); color:var(--bone); font-family:var(--arcade-display); font-size:18px;
      letter-spacing:.05em; text-transform:uppercase; }
    /* The gear was rendering at 14px inside a 34px button — a small mark floating in a lot of glass,
       and the hardest thing on screen to aim at on a phone. The glyph is the whole label here, so it
       should fill its button the way the ♪ does. */
    #settingsBtn { font-size:19px; line-height:1; display:grid; place-items:center; min-width:34px; }
    /* Position and stage clock (scripts/22-race-hud.js). Top-left: settings are
       top-right, the stage card and profile strip run along the bottom. No glass panel —
       an ordinal this size reads fine on a drop shadow, and a panel here would crowd a
       frame that already carries five of them.

       Indented past the audio widget's toggle, which owns the very corner (its rule is
       in scripts/14-audio.js: fixed, top/left 12px, 34px square, z-index 9). A box-overlap
       sweep over .top-actions and the stage HUD reported no collision and was wrong,
       because the audio widget is neither: the ordinal was drawn straight through the ♪
       button. Caught by looking at the render, which is the one check the numbers cannot
       replace. 44px = 34 wide plus a 10px gap. */
    .race-hud { position:fixed; z-index:5; top:max(12px,env(safe-area-inset-top)); left:calc(max(14px,env(safe-area-inset-left)) + 44px); display:grid; justify-items:start; pointer-events:none; text-shadow:0 3px 18px rgba(10,20,24,.42); transition:transform .18s ease, opacity .18s ease; }
    /* Opening the audio panel drops a 176px sheet down the left edge. The HUD is a later
       sibling than .tdt-audio (14-audio.js appends before 22-race-hud.js), so it can be
       moved out from under it. Faded as well as shifted: with the panel open the player
       is in a menu, not racing. */
    .tdt-audio[data-open="1"] ~ .race-hud { transform:translateX(152px); opacity:.45; }
    .race-place { display:flex; align-items:flex-start; color:var(--text); line-height:.86; font-variant-numeric:tabular-nums; letter-spacing:-.05em; }
    .race-place b { font-size:46px; font-weight:750; }
    .race-place sup { font-size:17px; font-weight:650; margin-top:.18em; margin-left:.06em; }
    .race-hud.is-leading .race-place { color:var(--accent); }
    .race-field { margin-top:2px; color:var(--muted); font-size:9px; letter-spacing:.09em; text-transform:uppercase; }
    /* Reserve the gap line's height so the clock does not jump when the gap blanks
       out at the front of the race. */
    .race-gap { min-height:13px; margin-top:4px; color:var(--text); font-size:12px; font-weight:650; font-variant-numeric:tabular-nums; }
    /* Leading: unsigned and accent-coloured, matching the ordinal at 1st, so one rule
       covers both — green means you are winning. A deficit keeps its "+" in plain white. */
    .race-gap.is-lead { color:var(--accent); }
    .race-clock { margin-top:2px; color:var(--muted); font-size:13px; font-weight:600; font-variant-numeric:tabular-nums; letter-spacing:-.01em; }
    /* LEGS — the climbing effort budget (scripts/04-gameplay.js). A bar, not a number:
       mid-climb the rider needs "how much is left" at a glance, and the RATE it falls
       says how steep the ramp is before the gradient readout does. Fixed width so the
       bar itself is the scale; no label, because the colour and the fall rate say it. */
    .race-legs { width:74px; height:5px; margin-top:6px; border-radius:3px; overflow:hidden; background:rgba(248,251,250,.20); box-shadow:0 1px 6px rgba(10,20,24,.35); }
    .race-legs i { display:block; height:100%; width:100%; border-radius:3px; background:var(--accent); transition:width .18s linear, background-color .22s ease; }
    /* Amber getting thin, red once the power cap is actually applied. The `is-spent`
       class is driven by the physics' own hysteresis flag, so the colour flips exactly
       when the cap does rather than at a second threshold that could disagree. */
    .race-legs.is-low i { background:#f0c04b; }
    .race-legs.is-spent i { background:#e2603f; }
    @media (prefers-reduced-motion:reduce) { .race-legs i { transition:none; } }
    /* THE CLIMB PANEL (scripts/24-segments.js supplies the numbers, 22-race-hud.js drives
       it). Top-centre, because while a climb is being timed it IS the screen's most
       important information, and folding it into the corner block would shove the position
       ordinal down every time one started.

       Sat 74px down rather than at the top, and the reason corrects a wrong assumption I
       had written here: that 07-stage-select.js hides the engine's own #goalReadout so the
       centre is free. That rule is inside its `@media (max-width: 540px)` block, so it is
       true on a phone and FALSE on desktop — measured at 1440px, the readout occupies
       662-778 x 14-74 and the panel overlapped it by 116 x 60px. 74px clears it. */
    .race-climb { position:fixed; z-index:6; top:calc(max(12px,env(safe-area-inset-top)) + 74px); left:50%; transform:translateX(-50%);
      width:min(300px,calc(100vw - 140px)); padding:9px 12px 10px; border-radius:14px;
      display:grid; justify-items:center; gap:3px; pointer-events:none;
      border:1px solid rgba(255,255,255,.16); background:linear-gradient(158deg,rgba(20,34,36,.72),rgba(12,22,24,.60));
      backdrop-filter:blur(14px) saturate(130%); box-shadow:0 10px 30px rgba(10,20,24,.32); overflow:hidden;
      animation:climb-in .22s ease-out both; }
    /* nowrap keeps the name on one line; the ellipsis is insurance, because a longer KOM
       name on a future stage would otherwise overflow the rounded panel rather than wrap.
       I could not get a reliable scrollWidth measurement off a clone, so this is a guard
       rather than a verified fit. */
    .race-climb-name { max-width:100%; overflow:hidden; text-overflow:ellipsis; color:var(--muted); font-size:9px; letter-spacing:.07em; text-transform:uppercase; white-space:nowrap; }
    .race-climb-row { display:flex; align-items:baseline; gap:8px; }
    .race-climb-time { color:var(--text); font-size:26px; font-weight:700; line-height:1; font-variant-numeric:tabular-nums; letter-spacing:-.03em; }
    .race-climb-delta { font-size:12px; font-weight:650; font-variant-numeric:tabular-nums; color:var(--muted); }
    /* Faster than the best is green, slower is warm. Same rule as the position ordinal:
       green means you are winning. */
    .race-climb-delta.is-up { color:var(--accent); }
    .race-climb-delta.is-down { color:#f0a24b; }
    .race-climb-bar { width:100%; height:4px; border-radius:2px; overflow:hidden; background:rgba(248,251,250,.18); }
    .race-climb-bar i { display:block; height:100%; width:0%; border-radius:2px; background:var(--accent); transition:width .2s linear; }
    .race-climb-foot { color:var(--muted); font-size:9px; letter-spacing:.06em; text-transform:uppercase; font-variant-numeric:tabular-nums; }
    /* Finished: the bar completes and the whole panel warms up so the result reads as an
       event rather than as more telemetry. A personal best gets the accent border. */
    .race-climb.is-done .race-climb-bar i { background:rgba(248,251,250,.5); }
    .race-climb.is-record { border-color:var(--accent); box-shadow:0 10px 30px rgba(10,20,24,.32),0 0 22px rgba(159,224,189,.28); }
    .race-climb.is-record .race-climb-time { color:var(--accent); }
    @keyframes climb-in { from { opacity:0; transform:translate(-50%,-8px); } to { opacity:1; transform:translate(-50%,0); } }
    @media (prefers-reduced-motion:reduce) { .race-climb { animation:none; } .race-climb-bar i { transition:none; } }
    /* PER-FARE TIMER BAR (scripts/27-fare-hud.js). The second of the two clocks: the shift clock
       gets digits because you plan around it, this gets a bar because all you need to know is
       whether it is running out. Hidden entirely when nobody is aboard. */
    /* SEGMENTED, NOT SMOOTH. A rounded 5px gradient bar is a progress indicator in a
       settings dialog; blocks draining one at a time are a machine, and you can count
       them at 100 km/h without reading anything.

       The segments are a MASK over the existing single fill, not new elements, so
       27-fare-hud.js keeps setting one width and needs no change. 18 blocks across the
       310px width: each cell is 1/18th, inked for the first part and cut for the rest,
       which lands the gap at ~2px. The mask applies to the whole bar so the unlit track is
       cut to the same rhythm and the blocks line up.

       No container and no track fill: the unlit sockets are dark translucent blocks, so the
       bar has substance where the segments are and nowhere else. The whole strip takes one
       hard drop-shadow, which follows the masked block shapes rather than a rectangle and so
       reads against a bright sky without a panel behind it. */
    .fare-bar { height:11px; margin:0 0 5px; padding:0; overflow:hidden;
      background:rgba(11,14,12,.5);
      filter:drop-shadow(2px 2px 0 rgba(11,14,12,.55));
      -webkit-mask-image:repeating-linear-gradient(90deg, #000 0 calc(100% / 18 - 2px), transparent calc(100% / 18 - 2px) calc(100% / 18));
      mask-image:repeating-linear-gradient(90deg, #000 0 calc(100% / 18 - 2px), transparent calc(100% / 18 - 2px) calc(100% / 18)); }
    .fare-bar i { display:block; height:100%; width:100%; border-radius:0; background:var(--jade);
      transition:width .12s linear, background-color .3s ease; }
    .fare-bar.is-low i { background:var(--gold); }
    .fare-bar.is-critical i { background:var(--blood); animation:fare-bar-pulse .45s ease-in-out infinite; }
    @keyframes fare-bar-pulse { 0%,100% { opacity:1; } 50% { opacity:.5; } }
    @media (prefers-reduced-motion:reduce) { .fare-bar.is-critical i { animation:none; } }

    /* SLOW DOWN prompt. The target line normally whispers; this is the one thing it says that
       the player has to act on in the next second, so it gets the whole band in red. */
    /* THE INSTRUCTION LINE IS BIGGER THAN THE DISTANCE LINE. 14px was the size of "FOTI · 110 M", and
       "NEINEIK ATU FOTI" was inheriting it — the one string that has to be obeyed, set smaller than the
       clock. 19px with tighter tracking, wrapping to two lines rather than widening: the box is a fixed
       244px and r224 placed the ride buttons in the 58px beside it, so growth must go DOWN not sideways. */
    .fare-hud.is-instructing .fare-target { font-size:19px; letter-spacing:.08em; line-height:1.08;
      white-space:normal; }
    .fare-hud.is-urgent .fare-target { background:var(--blood); color:var(--bone); font-size:14px;
      letter-spacing:.14em; animation:fare-urgent .5s ease-in-out infinite; }
    /* Urgent AND instructing is the loudest state the HUD has: in the zone, too fast, being told to
       stop. It must not shrink back to the urgent rule's 14px just because that rule comes later. */
    .fare-hud.is-urgent.is-instructing .fare-target { font-size:19px; letter-spacing:.08em; }
    @keyframes fare-urgent { 0%,100% { opacity:1; } 50% { opacity:.6; } }
    @media (prefers-reduced-motion:reduce) { .fare-hud.is-urgent .fare-target { animation:none; } }

    /* IMPACT VIGNETTE (scripts/36-traffic-impact.js). A red edge pulse on contact with traffic.
       A DOM vignette rather than a WebGL post pass: it costs nothing, it cannot interact with the
       fog (which does not survive a render target — see CLAUDE.md), and it reads at any preset.
       Pointer-transparent and inert until the class lands, so it is never in the way. */
    /* A DEDICATED ELEMENT, NOT body::after — and this was a regression in BOTH modes. The engine
       already owns `body::after` for its global screen grade (styles.css line 22: a white top
       highlight, a dark bottom band and a corner vignette). A second bare `body::after` has
       identical specificity, so the later rule simply replaced it and the base grade stopped being
       drawn — in the cycling game as much as the microlet one. Pseudo-elements are a single slot per
       element and this one was already taken. */
    .impact-vignette { position:fixed; inset:0; z-index:19; pointer-events:none; opacity:0;
      background:radial-gradient(ellipse at center, rgba(220,36,31,0) 42%, rgba(220,36,31,.55) 100%); }
    body.impact-flash .impact-vignette { animation:impact-flash .42s ease-out both; }
    @keyframes impact-flash { 0% { opacity:0; } 14% { opacity:1; } 100% { opacity:0; } }
    @media (prefers-reduced-motion:reduce) { body.impact-flash .impact-vignette { animation-duration:.2s; } }

    /* TITLE CARD (scripts/35-title-card.js). Two seconds at the top of the shift, then the
       node is removed from the document entirely. Pointer-transparent because it overlays the
       canvas at the exact moment the player first reaches for a control, and the camera drag
       listens on pointerdown. */
    /* ALIGNED HIGH, NOT CENTRED. Dead centre put "DILI · TIMOR-LESTE" straight across the back
       of the microlet, where a thin letter-spaced line in white on red is genuinely hard to
       read — and it buried the one thing the player is looking for in their first second, which
       is their own vehicle. 30% down clears the roof and still reads as a title. */
    .title-card { position:fixed; z-index:22; inset:0; display:grid; align-content:start; justify-items:center;
      padding-top:clamp(56px,22vh,190px); gap:6px; pointer-events:none; text-align:center;
      animation:title-card 2.6s ease-out both; }
    .title-card-flag { width:66px; height:5px; border-radius:3px;
      background:linear-gradient(90deg,#dc241f 0 34%,#ffc72c 34% 67%,#f8fbfa 67% 100%);
      box-shadow:0 2px 14px rgba(0,0,0,.45); }
    .title-card-main { display:grid; color:#f8fbfa; font-size:clamp(34px,8.5vw,84px); font-weight:850;
      line-height:.94; letter-spacing:-.045em; text-shadow:0 4px 34px rgba(6,12,16,.72), 0 1px 0 rgba(0,0,0,.35); }
    .title-card-main span { display:block; color:#ffd23f; }
    /* A dark pill behind the subtitle: letter-spaced 11px type over a moving 3D scene needs its
       own ground or it flickers in and out of legibility as the background changes. */
    .title-card-sub { margin-top:10px; padding:4px 12px; border-radius:999px; background:rgba(10,18,20,.42);
      color:rgba(248,251,250,.9); font-size:clamp(9px,1.5vw,12px); font-weight:700;
      letter-spacing:.34em; text-shadow:0 1px 8px rgba(6,12,16,.8); }
    /* Punches in fast, sits, then lifts away — the sit is most of the duration, because a title
       that is still moving is a title you have to chase to read. */
    @keyframes title-card {
      0%   { opacity:0; transform:scale(1.12); }
      9%   { opacity:1; transform:scale(1); }
      74%  { opacity:1; transform:scale(1); }
      100% { opacity:0; transform:scale(1.03) translateY(-14px); }
    }
    @media (prefers-reduced-motion:reduce) {
      .title-card { animation:title-card-fade 2.6s linear both; }
      @keyframes title-card-fade { 0%,100% { opacity:0; } 10%,78% { opacity:1; } }
    }

    /* NEW DISTRICT (scripts/43-rounds.js). Opaque on purpose: the world performs a forward jump
       after this has painted, and showing the camera cross kilometres of unbuilt road would turn a
       reward beat into a streaming glitch. It is a full arcade interstitial rather than another
       centre card, so it also gives a long shift a clean chapter break. */
    .round-transition { position:fixed; z-index:24; inset:0; overflow:hidden; display:grid;
      place-items:center; padding:max(22px,env(safe-area-inset-top)) max(22px,env(safe-area-inset-right))
        max(22px,env(safe-area-inset-bottom)) max(22px,env(safe-area-inset-left));
      pointer-events:auto; touch-action:none; background:var(--ink); color:var(--bone);
      opacity:0; visibility:hidden; transition:opacity .18s linear,visibility 0s linear .18s; }
    .round-transition[hidden] { display:none; }
    .round-transition.is-active { opacity:1; visibility:visible; transition-delay:0s; }
    /* Timor flag colours as a road stripe. Three hard bands are clearer than a decorative logo at
       phone landscape height, and cost no asset request. */
    .round-transition-track { position:absolute; top:0; left:0; right:0; display:grid; gap:5px; }
    .round-transition-track i { display:block; transform-origin:left; transform:scaleX(0); }
    .round-transition-track i:nth-child(1) { height:11px; width:100%; background:var(--gold); }
    .round-transition-track i:nth-child(2) { height:7px; width:74%; background:var(--blood); }
    .round-transition-track i:nth-child(3) { height:4px; width:48%; background:var(--bone); opacity:.72; }
    .round-transition.is-active .round-transition-track i { animation:round-track .42s cubic-bezier(.2,.85,.3,1) both; }
    .round-transition.is-active .round-transition-track i:nth-child(2) { animation-delay:.06s; }
    .round-transition.is-active .round-transition-track i:nth-child(3) { animation-delay:.12s; }
    .round-transition-card { position:relative; z-index:1; width:min(760px,92vw); text-align:center;
      transform:skewY(-2deg); }
    .round-transition-kicker { display:inline-block; padding:7px 20px 9px; background:var(--gold);
      color:var(--ink); font-family:var(--arcade-display); font-size:clamp(27px,5vw,54px);
      font-weight:400; font-style:oblique 9deg; line-height:.9; letter-spacing:.035em;
      text-transform:uppercase; clip-path:polygon(12px 0,100% 0,calc(100% - 12px) 100%,0 100%);
      filter:drop-shadow(6px 6px 0 var(--blood)); }
    .round-transition-main { margin-top:18px; font-family:var(--arcade-display);
      font-size:clamp(58px,13vw,138px); font-weight:400; font-style:oblique 9deg;
      line-height:.78; letter-spacing:.015em; text-transform:uppercase;
      text-shadow:5px 5px 0 var(--blood),13px 13px 0 #000; }
    .round-transition.long-place .round-transition-main { font-size:clamp(42px,10vw,104px); }
    .round-transition-location { margin-top:22px; color:var(--bone); font-family:var(--arcade-util);
      font-size:clamp(11px,2vw,18px); font-weight:800; line-height:1; letter-spacing:.18em;
      text-transform:uppercase; opacity:.82; text-shadow:2px 2px 0 #000; }
    .round-transition-progress { display:inline-block; margin-top:14px; padding:8px 16px 9px;
      border-top:3px solid var(--gold); border-bottom:3px solid var(--gold);
      color:var(--gold); font-family:var(--arcade-util); font-size:clamp(12px,2.3vw,20px);
      font-weight:800; line-height:1; letter-spacing:.2em; text-transform:uppercase;
      font-variant-numeric:tabular-nums; }
    .round-transition-road { position:absolute; left:8vw; right:8vw; bottom:max(26px,env(safe-area-inset-bottom));
      height:11px; overflow:hidden; transform:skewX(-24deg); background:rgba(244,240,227,.12);
      -webkit-mask-image:repeating-linear-gradient(90deg,#000 0 44px,transparent 44px 58px);
      mask-image:repeating-linear-gradient(90deg,#000 0 44px,transparent 44px 58px); }
    .round-transition-road i { display:block; width:38%; height:100%; background:var(--gold);
      transform:translateX(-110%); }
    .round-transition.has-moved .round-transition-road i { animation:round-road .72s ease-out both; }
    @keyframes round-track { to { transform:scaleX(1); } }
    @keyframes round-road { to { transform:translateX(365%); } }
    @media (orientation:landscape) and (max-height:560px) {
      .round-transition-card { width:min(700px,78vw); }
      .round-transition-kicker { font-size:clamp(24px,5vh,38px); padding:5px 17px 7px; }
      .round-transition-main { margin-top:12px; font-size:clamp(52px,17vh,92px); }
      .round-transition.long-place .round-transition-main { font-size:clamp(36px,13vh,68px); }
      .round-transition-location { margin-top:15px; font-size:clamp(10px,2.5vh,14px); }
      .round-transition-progress { margin-top:10px; font-size:clamp(10px,2.6vh,14px); }
      .round-transition-road { bottom:max(15px,env(safe-area-inset-bottom)); height:8px; }
    }
    @media (prefers-reduced-motion:reduce) {
      .round-transition-track i { transform:scaleX(1); }
      .round-transition.is-active .round-transition-track i,
      .round-transition.has-moved .round-transition-road i { animation:none; }
      .round-transition.has-moved .round-transition-road i { transform:translateX(165%); }
    }

    /* END OF SHIFT (scripts/32-fare-result.js). A centred card, and the cash is the biggest
       thing on it because the cash is what the run was for. The best-ever figure sits
       directly under it so the comparison needs no thought — that pairing is the whole
       reason to start another shift. */
    /* A CONDUCTOR'S FARE BOARD, not a dialog. The hierarchy was right — the cash is the
       biggest thing because the cash is what the run was for — but a 20px radius over an
       18px blur is the shape of a system alert, and this is the one screen the player reads
       slowly and the only one that has to send them back in.

       PADDING MOVED OFF THE CARD AND ONTO ITS CHILDREN. The header has to be a full-bleed
       gold band, and it cannot bleed if its parent is holding 22px of inset — so the card
       has none and every other row carries its own. gap is 0 for the same reason: the
       bands stack edge to edge. */
    .shift-over { position:fixed; z-index:20; top:50%; left:50%; transform:translate(-50%,-50%);
      width:min(408px,calc(100vw - 32px)); max-height:calc(100vh - 32px); overflow-y:auto; padding:0;
      display:grid; justify-items:stretch; gap:0; text-align:center; pointer-events:auto;
      border:4px solid var(--gold); background:var(--ink);
      clip-path:polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 20px 100%, 0 calc(100% - 20px));
      filter:drop-shadow(10px 10px 0 rgba(0,0,0,.55));
      animation:shift-in .2s ease-out both; }
    .shift-title { padding:10px 20px 12px; background:var(--gold); color:var(--ink);
      font-family:var(--arcade-display); font-size:30px; font-weight:400; font-style:oblique 9deg;
      line-height:1; letter-spacing:.03em; text-transform:uppercase; }
    /* The money IS the screen: the two-colour arcade shadow, red behind black. */
    .shift-cash { padding:20px 20px 0; color:var(--gold);
      font-family:var(--arcade-display); font-size:104px; font-weight:400; font-style:oblique 9deg;
      line-height:.82; letter-spacing:.01em; font-variant-numeric:tabular-nums;
      text-shadow:4px 4px 0 var(--blood), 10px 10px 0 rgba(0,0,0,.6); }
    .shift-best { padding:12px 20px 0; color:rgba(244,240,227,.6);
      font-family:var(--arcade-util); font-size:13px; font-weight:700; letter-spacing:.2em;
      text-transform:uppercase; font-variant-numeric:tabular-nums; }
    /* A NEW BEST IS THE BIGGEST THING THAT CAN HAPPEN IN A SHIFT and it was rendered as an
       edit — the same line of text going from "BEST $92" to "NEW BEST" in green. 32-fare-result.js
       already writes that string and already toggles .is-record, so this is a restyle of a
       state that exists: a stuck-on sticker, rotated, over the corner of the number.
       Absolute so it does not push the grid, and the card is positioned. */
    .shift-over.is-record .shift-best { position:absolute; top:52px; right:6px; padding:5px 13px 7px;
      background:var(--blood); color:var(--bone); font-family:var(--arcade-display); font-size:21px;
      font-weight:400; font-style:normal; letter-spacing:.04em; line-height:1;
      transform:rotate(-6deg); filter:drop-shadow(3px 3px 0 rgba(0,0,0,.6)); }
    .shift-rows { padding:12px 20px 0; color:rgba(244,240,227,.72);
      font-family:var(--arcade-util); font-size:14px; font-weight:700; letter-spacing:.12em;
      text-transform:uppercase; font-variant-numeric:tabular-nums; }
    /* The career line was added to the panel and never given a rule, so it rendered at the browser
       default 16px — larger than the two lines above it and louder than the cash it is subordinate to. */
    .shift-career { padding:14px 20px 0; color:var(--gold);
      font-family:var(--arcade-display); font-size:24px; font-weight:400; font-style:oblique 9deg;
      line-height:1; letter-spacing:.02em; font-variant-numeric:tabular-nums; }
    .shift-again { margin:20px; min-height:52px; cursor:pointer;
      border:0; background:var(--blood); color:var(--bone);
      clip-path:polygon(15px 0, 100% 0, calc(100% - 15px) 100%, 0 100%);
      font-family:var(--arcade-display); font-size:29px; font-weight:400; font-style:oblique 9deg;
      line-height:1; letter-spacing:.03em; text-transform:uppercase;
      filter:drop-shadow(6px 6px 0 rgba(0,0,0,.65));
      transition:transform .1s ease, filter .1s ease; }
    .shift-again:hover { background:#f0322c; transform:translate(-2px,-2px); filter:drop-shadow(8px 8px 0 rgba(0,0,0,.65)); }
    .shift-again:focus-visible { outline:3px solid var(--gold); outline-offset:4px; }
    /* The fare HUD would otherwise sit over the card reading 0:00, which competes with the
       result for no reason.
       .1 rather than the .25 this was: the readouts now carry a 5px ink outline, so at .25
       the dead clock had far more mass than it used to and read as a pink smear across the
       sky above the card. Same intent, re-tuned for type that is no longer a thin glyph. */
    body.shift-is-over .fare-hud { opacity:.1; }
    @keyframes shift-in { from { opacity:0; transform:translate(-50%,-46%) scale(.97); } to { opacity:1; transform:translate(-50%,-50%) scale(1); } }
    @media (prefers-reduced-motion:reduce) { .shift-over { animation:none; } }

    /* MICROLET MODE hides the cycling instruments. None of them mean anything when you are
       driving a taxi: race position in a field of riders, a stage clock counting UP, the
       climbing-effort bar, the timed-climb panel, and the stage elevation profile are all
       answers to questions this mode does not ask — and the fare clock has to be the
       loudest thing on screen, which it cannot be while a 46px ordinal argues with it.

       Kept deliberately: the stage strip, because it carries KM/H and a driving game needs
       a speedometer; the minimap, because knowing where you are on the island is MORE
       useful hauling fares than it was racing; and the touch controls.

       Hidden in CSS off a body class rather than by editing each layer, so every one of
       them keeps owning its own DOM and this stays a one-line reversal when a real mode
       switch replaces the URL flag. */
    /* THE ROUTE PANEL WAS SAYING EVERYTHING THE MINIMAP ALREADY SAID. The minimap in the
       top-right carries the route name and "0.0 / 83.1 km"; the panel bottom-left carried the
       same name, the same GPS badge, the same distance and the same progress bar, plus elev
       and peak. Six readouts, two of them duplicates and two of them about climbing.

       What is left is the one row a driver actually reads: gradient and km/h. The rest of the
       layers keep owning and updating their own DOM harmlessly — it simply is not shown, which
       keeps this reversible the moment mode becomes a UI toggle rather than a URL. */
    body.microlet-mode .tdt-hud-top,
    body.microlet-mode .tdt-hud-bar,
    body.microlet-mode .tdt-hud-dist,
    body.microlet-mode .tdt-hud-row { display:none !important; }
    /* With four of six blocks gone the panel is one row, so it should not still be sized for
       six — otherwise the simplification reads as an empty box.

       AND IT LOSES THE PANEL ENTIRELY, for the same reason the fare HUD did: two readouts
       (gradient and speed) do not need a 16px-radius blurred glass box, and on a phone that
       box became the single most prominent thing on the screen — a full-width app pill sitting
       directly under an arcade clock. Same treatment as the fare board instead: outlined
       display type on no background. Bike mode keeps its panel, where the strip really does
       carry six fields and needs the container to group them. */
    body.microlet-mode .tdt-hud { width:auto !important; min-width:0 !important;
      padding:0 !important; border:0 !important; border-radius:0 !important;
      background:none !important; backdrop-filter:none !important; box-shadow:none !important; }
    body.microlet-mode .tdt-hud-main { margin-top:0 !important; }
    body.microlet-mode .tdt-grade-val,
    body.microlet-mode .tdt-kv b {
      font-family:var(--arcade-display); font-weight:400; font-style:oblique 9deg;
      letter-spacing:.01em; color:var(--bone);
      -webkit-text-stroke:4px var(--ink); paint-order:stroke fill;
      text-shadow:3px 3px 0 rgba(11,14,12,.5); }
    body.microlet-mode .tdt-grade-arrow { -webkit-text-stroke:3px var(--ink); paint-order:stroke fill; }
    body.microlet-mode .tdt-grade-pct,
    body.microlet-mode .tdt-kv span {
      font-family:var(--arcade-util); font-weight:700; letter-spacing:.18em; text-transform:uppercase;
      color:var(--gold); opacity:1;
      -webkit-text-stroke:2px var(--ink); paint-order:stroke fill; }

    /* .tdt-next is the "next mark" row in the route panel, and in microlet mode it was
       still announcing WATER STATIONS -- pure bicycle-race furniture, and the one thing on
       screen that gave the new mode away as a reskinned stage race. The panel's route name,
       distance, gradient and speed all still mean something to a driver, so only this row
       goes. */
    body.microlet-mode .tdt-next { display:none !important; }
    body.microlet-mode .race-hud,
    body.microlet-mode .race-climb,
    body.microlet-mode .tdt-profile,
    /* THE LAST TWO CYCLING OVERLAYS. `.tdt-cult` is the Tour commentary card stack — KOM approaches,
       feed zones, rider thoughts — and `.tdt-gc-open` is the button into the general classification,
       which is a bicycle stage race's overall standings and has no meaning at all for a mikrolet
       driver. Both were still on screen in microlet mode. 08-culture.js now also stops QUEUEING the
       race marks, so this is belt and braces rather than the only defence. */
    body.microlet-mode .tdt-cult,
    body.microlet-mode .tdt-gc-open,
    body.microlet-mode .goal-readout { display:none !important; }

    /* FARE HUD (microlet mode only, scripts/27-fare-hud.js). Top-centre and deliberately
       loud: in this loop the clock IS the game, so it is the largest element on screen and
       everything else is subordinate to it. Amber under 20s, red and pulsing under 10 —
       the last ten seconds carry the whole run. */
    /* A HOUSING FOR THE CLOCK. The hierarchy above was already right — the clock is the
       game, so it is the biggest thing on screen — but bare numerals over a 3D render
       read as a debug overlay, and they sat on whatever the sky happened to be doing.
       The same numerals inside a gold-framed board read as an instrument, and the frame
       fixes the legibility problem for free: the type is now on a field it controls.

       NO PANEL. The first pass built a gold-framed board and it was too heavy — a solid
       slab across the top of the frame pulls the eye and holds it, which is exactly wrong
       for a readout you are supposed to catch in passing while watching the road. So the
       housing is gone and only the TREATMENT stays: Impact on a 9-degree lean, hard offset
       shadows, segmented blocks, the flag palette.

       LEGIBILITY WITHOUT A BACKGROUND comes from an outline, not from a box: a 5px ink
       text-stroke with `paint-order: stroke fill` so the stroke is painted UNDER the fill
       instead of eating into the glyph, plus one hard offset shadow for weight. Without
       paint-order a text-stroke is drawn centred on the outline and a 5px one would close
       up Impact's counters at this size. Both are supported here (checked with CSS.supports,
       and paint-order normalises to "stroke").
       This is also what the panel was really for — the old bare numerals sat on whatever
       the sky happened to be doing — and an outline solves it without taking any focus.

       .fare-hud is a positioning shell and .fare-board is now only a grid, neither carrying
       paint. The split still matters: .fare-board is the row layout, while the payout and
       combo sit on rails outside it. */
    .fare-hud { position:fixed; z-index:7; top:max(10px,env(safe-area-inset-top)); left:50%; transform:translateX(-50%);
      width:318px; pointer-events:none; }
    .fare-board { position:relative; display:grid; justify-items:stretch; gap:2px; }
    /* THE LEAN IS font-style, NOT transform, and that is load-bearing rather than a
       preference. The obvious way to slant these is transform:skewX(-9deg) — but
       fare-pulse, fare-final and cash-bump all animate `transform`, so a skew declared
       that way is silently DISCARDED for the whole of the critical band and every time
       the cash ticks: the clock would un-skew exactly when it matters most.
       `font-style: oblique 9deg` shears the glyphs without touching transform at all.
       Verified in Chrome by rasterising a '1' both ways and measuring the lean of its
       inked centroid, top row against bottom: 3.5px upright, 21.5px oblique. Impact has
       no italic face, so this is a synthesised slant — which is what is wanted, and it
       is also why font-weight is pinned to 400 (there is no bold either, and a
       synthesised one smears). */
    .fare-clock { text-align:center;
      font-family:var(--arcade-display); font-size:76px; font-weight:400; line-height:.9;
      font-style:oblique 9deg;
      color:var(--bone); font-variant-numeric:tabular-nums; letter-spacing:.012em;
      -webkit-text-stroke:5px var(--ink); paint-order:stroke fill;
      text-shadow:4px 5px 0 rgba(11,14,12,.55); }
    /* Cash keeps the gold — it is the score, and gold is the fare box — but as outlined
       type rather than a band. */
    .fare-cash { text-align:center; color:var(--gold);
      font-family:var(--arcade-display); font-size:34px; font-weight:400; line-height:1;
      font-style:oblique 9deg;
      font-variant-numeric:tabular-nums; letter-spacing:.01em;
      -webkit-text-stroke:4px var(--ink); paint-order:stroke fill;
      text-shadow:3px 4px 0 rgba(11,14,12,.5); }
    /* The EARNED label went with the band. Without a panel it was a third line of chrome on
       a readout that has to be caught at a glance, and "$126" in gold needs no caption. */
    /* CASH AND THE SCORE-TO-BEAT SHARE ONE ROW, which is what keeps the phone layout intact: at
       360px the board ends at y=69 and the stage strip starts at 80, so a fourth stacked line
       would reopen the collision that was just closed. Baseline-aligned so the small HI sits on
       the money's own baseline instead of floating, and centred as a pair so the board stays
       symmetrical whether HI is present or not. */
    .fare-money { display:flex; align-items:baseline; justify-content:center; gap:10px; }
    .fare-hi { color:var(--bone); opacity:.78;
      font-family:var(--arcade-util); font-size:13px; font-weight:700; letter-spacing:.14em;
      text-transform:uppercase; white-space:nowrap; font-variant-numeric:tabular-nums;
      -webkit-text-stroke:3px var(--ink); paint-order:stroke fill;
      text-shadow:2px 2px 0 rgba(11,14,12,.5); }
    /* Absent, not blank: an empty box would still take its gap and pull the money off centre. */
    .fare-hi:empty { display:none; }
    /* Beaten. Gold and full opacity, and TWO pulses rather than an infinite loop — this is a
       moment, not a state that should keep asking for attention while the player is still driving. */
    .fare-hi.is-record { color:var(--gold); opacity:1;
      animation:fare-record .5s cubic-bezier(.2,.9,.3,1) 2 both; }
    @keyframes fare-record { 0%,100% { transform:scale(1); } 45% { transform:scale(1.18); } }
    @media (prefers-reduced-motion:reduce) { .fare-hi.is-record { animation:none; } }
    .fare-target { text-align:center; margin-top:3px; color:var(--bone);
      font-family:var(--arcade-util); font-size:14px; font-weight:700; letter-spacing:.17em;
      text-transform:uppercase; font-variant-numeric:tabular-nums;
      -webkit-text-stroke:3px var(--ink); paint-order:stroke fill;
      text-shadow:2px 2px 0 rgba(11,14,12,.5); }
    /* THE PAYOUT PUNCHES SIDEWAYS. It used to stack under the cash — adjacent to its cause,
       which was the right instinct, but that is directly under a 74px clock and therefore
       exactly where the eye is not. Lifted out of the grid to the right of the board: it
       never covers the clock, and it lands in peripheral vision, which is where a reward
       belongs while you are steering.
       Absolute against .fare-hud, which is already positioned. */
    .fare-flash { position:absolute; left:calc(100% + 14px); top:34px; min-width:96px;
      padding:6px 13px 8px; background:var(--jade); color:var(--ink);
      font-family:var(--arcade-display); font-size:34px; font-weight:400; font-style:oblique 9deg;
      line-height:.92; letter-spacing:.01em; opacity:0; white-space:nowrap;
      transition:opacity .16s ease; font-variant-numeric:tabular-nums;
      filter:drop-shadow(5px 5px 0 var(--ink)); }
    .fare-hud.is-paid .fare-flash { opacity:1; }
    .fare-hud.is-low .fare-clock { color:var(--gold); }
    .fare-hud.is-critical .fare-clock { color:#ff5a3c; animation:fare-pulse .62s ease-in-out infinite; }
    /* With the frame gone the state lives entirely in the digits, which is where it was
       before — but the ink outline means a red clock now holds against a bright sky instead
       of vanishing into it, so the band actually reads at the moment it matters.
       The is-final and is-over colours are further down this file with their animations; not
       repeated here, because two rules for one state is how they drift apart. */
    /* THE LAST FOUR SECONDS. Ten seconds of identical red pulsing flattens the ending into one
       long note, and these are the seconds where a desperate pickup either saves the shift or does
       not — so they get a faster beat and a hard shake. */
    .fare-hud.is-final .fare-clock { color:#ff3b22; animation:fare-final .28s ease-in-out infinite; }
    @keyframes fare-final {
      0%,100% { transform:scale(1.04) translateX(0); }
      25%     { transform:scale(1.12) translateX(-3px); }
      75%     { transform:scale(1.12) translateX(3px); }
    }
    @media (prefers-reduced-motion:reduce) { .fare-hud.is-final .fare-clock { animation:none; } }
    .fare-hud.is-over .fare-clock { color:#ff5a3c; }
    .fare-hud.is-over .fare-target { color:#ff5a3c; }
    @keyframes fare-pulse { 0%,100% { transform:scale(1); } 50% { transform:scale(1.075); } }

    /* WAIT → BÁ! — the Crazy Taxi boarding beat, requested off a reference screenshot of exactly
       this. Centre-screen because it narrates a moment when the bus is STOPPED: the one time a
       stamp over the road costs nothing to read. WAIT holds in gold while the queue files in
       (30-passengers' walk-and-stagger runs ~1.3s), then the go-word punches in jade and the
       player floors it. Fixed to the viewport, not to .fare-hud — the board is top-centre and
       this is not part of the board. */
    .fare-stamp { position:fixed; left:50%; top:34%; transform:translate(-50%,-50%); z-index:8;
      padding:4px 22px 8px; font-family:var(--arcade-display); font-size:clamp(44px,11vw,92px);
      font-weight:400; font-style:oblique 9deg; line-height:.9; letter-spacing:.04em;
      color:var(--gold); pointer-events:none; opacity:0; white-space:nowrap;
      text-shadow:3px 3px 0 var(--blood), 7px 7px 0 var(--ink); }
    .fare-stamp.wait { opacity:1; animation:stamp-wait 1s ease-in-out infinite; }
    .fare-stamp.go { opacity:1; color:#3ee0a8; text-shadow:3px 3px 0 #0d7362, 7px 7px 0 var(--ink);
      animation:stamp-go .5s cubic-bezier(.16,1.3,.3,1) both; }
    @keyframes stamp-wait { 0%,100% { transform:translate(-50%,-50%) scale(1); }
      50% { transform:translate(-50%,-50%) scale(1.06); } }
    @keyframes stamp-go { from { transform:translate(-50%,-50%) scale(.4); opacity:0; }
      to { transform:translate(-50%,-50%) scale(1); opacity:1; } }
    @media (prefers-reduced-motion:reduce) { .fare-stamp.wait, .fare-stamp.go { animation:none; } }

    /* CASH LANDING SHOULD HIT THE WHOLE FRAME. The payout was a number fading in beside a HUD, which
       is a receipt rather than a reward — and delivering a fare is the entire loop. A gold rim
       flashing around the edges reads instantly at 100 km/h without covering the road.

       ITS OWN ELEMENT, and both alternatives were tried and rejected for concrete reasons:
       - `body::after` is spoken for by the engine's screen grade, and a second bare rule at the same
         specificity silently REPLACES it (the one-slot-per-element trap, which has already deleted
         that grade once).
       - `.speed-vignette::after` looked free, but that element sits at `opacity:0` and is animated
         by the speed effect — a child inherits the parent's opacity, so the rim would have been
         invisible except while boosting.
       Nothing is transformed here either: transforming body or the canvas would make it the
       containing block for every fixed-positioned HUD element, which is exactly what put BÁ!
       through the clock. */
    .cash-rim { position:fixed; inset:0; z-index:9; pointer-events:none; opacity:0;
      box-shadow:inset 0 0 90px 14px rgba(255,199,44,.5), inset 0 0 26px 3px rgba(255,231,168,.45); }
    body.cash-pop .cash-rim { animation:cash-pop-rim .44s ease-out both; }
    @keyframes cash-pop-rim { 0% { opacity:1; } 60% { opacity:.35; } 100% { opacity:0; } }
    @media (prefers-reduced-motion:reduce) { body.cash-pop .cash-rim { animation:none; } }

    /* THE PAYOUT, MADE TO LAND. It was one grey line for the biggest moment in the loop. The money
       punches in and drifts up, the grade sits under it, and the colour comes from how well it was
       driven — so a PERFECT delivery and a SLOW one do not read identically. */
    .fare-hud.is-paid .fare-flash { animation:fare-payout .9s cubic-bezier(.18,.9,.24,1) both; }
    /* The grade sits directly under the money slab, on the same rail, in the darker jade —
       one object in two parts rather than two readouts. */
    .fare-grade { position:absolute; left:calc(100% + 14px); top:84px; min-width:96px;
      padding:3px 13px 4px; background:#0d7362; color:var(--bone);
      font-family:var(--arcade-util); font-size:11.5px; font-weight:700; letter-spacing:.2em;
      text-transform:uppercase; opacity:0; white-space:nowrap; font-variant-numeric:tabular-nums;
      filter:drop-shadow(4px 4px 0 var(--ink)); }
    .fare-hud.is-paid .fare-grade { animation:fare-grade .9s ease-out both; }
    /* The grade colours the slab now, not the text, so PERFECT still does not read like SLOW. */
    .fare-hud.g-perfect .fare-flash { background:#3ee0a8; }
    .fare-hud.g-great .fare-flash { background:var(--jade); }
    .fare-hud.g-good .fare-flash { background:var(--gold); }
    .fare-hud.g-slow .fare-flash { background:#f0a93c; }
    /* Slid out from behind the board rather than scaled up in place: the board is the
       thing it came out of, so the motion should say so. */
    @keyframes fare-payout {
      0%   { opacity:0; transform:translateX(-18px) scale(.82); }
      26%  { opacity:1; transform:translateX(0) scale(1.06); }
      44%  { transform:translateX(0) scale(1); }
      78%  { opacity:1; transform:translateX(0) scale(1); }
      100% { opacity:0; transform:translateX(0) translateY(-14px) scale(.98); }
    }
    @keyframes fare-grade { 0% { opacity:0; } 18%,74% { opacity:1; } 100% { opacity:0; } }

    /* A KICK ON THE CASH every time it changes. The number growing is the point of the game and it
       was changing in silence. */
    .fare-cash.bump { animation:cash-bump .34s cubic-bezier(.2,.9,.3,1) both; }
    @keyframes cash-bump { 0% { transform:scale(1); } 34% { transform:scale(1.22); } 100% { transform:scale(1); } }

    /* THE LIVE COMBO. 28-traffic-hazard.js has always measured near misses and built a multiplier;
       until now nothing consumed OR displayed it, so the player had no way to learn that driving
       close to traffic pays. It sits to the right of the clock so it never pushes the layout. */
    /* MOVED TO THE LEFT RAIL. It used to sit at `left: calc(50% + 66px)`, which was clear
       of an auto-width column of centred text — but the board is now a fixed 318px box and
       .fare-hud is its positioning context, so that same offset lands at x=225 INSIDE the
       board, straight over the clock. The payout owns the right rail, so the combo takes
       the left one and the two can never collide. */
    .fare-combo { position:absolute; top:34px; right:calc(100% + 14px); text-align:right;
      padding:5px 12px 7px; background:var(--jade); color:var(--ink); white-space:nowrap;
      font-family:var(--arcade-display); font-size:28px; font-weight:400; font-style:oblique 9deg;
      line-height:1; letter-spacing:.01em; opacity:0;
      transform:scale(.8); transition:opacity .18s ease, transform .18s ease, background-color .25s ease;
      font-variant-numeric:tabular-nums; filter:drop-shadow(4px 4px 0 var(--ink)); }
    .fare-combo.on { opacity:1; transform:scale(1); }
    .fare-combo.hot { background:var(--gold); animation:combo-hot .42s ease-in-out infinite; }
    @keyframes combo-hot { 0%,100% { transform:scale(1) rotate(-2deg); } 50% { transform:scale(1.14) rotate(2deg); } }

    @media (prefers-reduced-motion:reduce) {
      .fare-hud.is-paid .fare-flash, .fare-hud.is-paid .fare-grade,
      .fare-cash.bump, .fare-combo.hot { animation:none; }
      .fare-hud.is-paid .fare-flash, .fare-hud.is-paid .fare-grade { opacity:1; }
    }
    @media (prefers-reduced-motion:reduce) { .fare-hud.is-critical .fare-clock { animation:none; } }
    @media (max-width:540px) {
      /* The stage strip owns y 52-98 across the full width, so this sits above it and the
         clock comes down enough to fit.

         The readouts have no panel to shrink now, so this is just type sizing — but the
         clock and cash go inline on one row rather than stacked, because 76px + 34px of
         display type down the middle of a phone screen is most of the horizon.
         The payout and combo rails have to come inboard: at this width there is no room
         outside the box, and `left: calc(100% + 14px)` would put them off-screen. */
      /* -116px, NOT -20px, and the 96px difference is the two top corners. The board is
         centred between the audio toggle (12px in, 31px wide, so it ends at 43) and the
         settings button (34px wide, 12px in, so it starts 46 from the right) — a
         `100vw - 20px` cap ignored both and at 360px the 272px board ran 2px INTO the
         settings button while clearing the audio one by a single pixel. Reserving 58px each
         side gives 15px and 12px of clearance at 360 and changes nothing at 390 and up,
         where the 272px cap is what binds anyway.
         Caught only because the sweep printed the clearances as signed numbers; the overlap
         test itself used a >2px threshold and called a 2px collision "none". */
      .fare-hud { top:max(6px,env(safe-area-inset-top)); width:min(272px,calc(100vw - 116px)); }
      /* THE TWO COLUMNS ARE 1fr 1fr AND MEET IN THE MIDDLE, not 1fr auto. With `1fr auto`
         the clock took all the slack on the left and the cash was pinned to the far right
         edge, so a 272px box had the shift clock and the money at opposite ends with a hole
         between them — they read as two unrelated widgets instead of one score line.
         Equal tracks with the clock ending and the cash starting at the centre keeps the
         pair together, and baseline alignment sits the smaller $ figure on the clock's own
         baseline rather than floating it mid-height. */
      .fare-board { grid-template-columns:1fr 1fr; grid-template-areas:"bar bar" "clock cash" "target target"; align-items:baseline; column-gap:8px; }
      .fare-bar { grid-area:bar; }
      .fare-clock { grid-area:clock; justify-self:end; font-size:46px; text-align:right; -webkit-text-stroke-width:4px; }
      /* The GRID AREA MOVED to the wrapper: cash is now inside .fare-money alongside HI, so
         placing .fare-cash itself would leave the wrapper auto-flowing into the next cell and
         push the target line out of its row. The wrapper stacks its two children here rather
         than sitting them side by side, because 272px has no room for a 46px clock, the money
         and a score-to-beat all on one line. */
      .fare-money { grid-area:cash; justify-self:start; display:grid; justify-items:start; gap:0; }
      .fare-cash { font-size:26px; -webkit-text-stroke-width:3px; }
      .fare-hi { font-size:11px; letter-spacing:.1em; -webkit-text-stroke-width:2px; }
      /* Back on .fare-target directly: the .fare-trip wrapper existed only to sit the load cluster
         beside the target, and with the cluster gone the wrapper went with it. */
      .fare-target { grid-area:target; font-size:12px; letter-spacing:.13em; -webkit-text-stroke-width:2px; }
      .fare-flash { left:auto; right:0; top:calc(100% + 8px); font-size:28px; }
      .fare-grade { left:auto; right:0; top:calc(100% + 52px); font-size:10.5px; }
      .fare-combo { right:auto; left:0; top:calc(100% + 8px); font-size:22px; }
    }
    .goal-readout { position:fixed; z-index:5; top:max(14px,env(safe-area-inset-top)); left:50%; transform:translateX(-50%); display:grid; justify-items:center; pointer-events:none; text-shadow:0 3px 18px rgba(10,20,24,.35); }
    .goal-readout strong { color:var(--text); font-size:28px; line-height:1; font-weight:650; font-variant-numeric:tabular-nums; letter-spacing:-.04em; }
    .goal-falls { display:flex; align-items:baseline; gap:5px; margin-top:5px; color:rgba(248,251,250,.68); font-size:9px; letter-spacing:.08em; text-transform:uppercase; }
    .goal-falls b { color:var(--text); font-size:10px; font-weight:650; font-variant-numeric:tabular-nums; }
    .goal-bonus { position:relative; display:flex; align-items:baseline; gap:4px; margin-top:4px; color:rgba(248,251,250,.66); font-size:9px; letter-spacing:.06em; text-transform:uppercase; }
    .goal-bonus b { color:var(--accent); font-size:10px; font-weight:750; font-variant-numeric:tabular-nums; }
    .goal-readout.bonus-flash .goal-bonus { animation:bonus-flash 3.4s ease-in-out both; will-change:opacity,transform,filter,text-shadow; }
    .goal-readout.bonus-flash .goal-bonus b { animation:bonus-value-flash 3.4s ease-in-out both; }
    @keyframes bonus-flash { 0%,100% { opacity:.72; transform:none; filter:none; text-shadow:none; } 9% { opacity:1; transform:scale(1.055); filter:brightness(1.34); text-shadow:0 0 13px rgba(159,255,218,.72); } 22% { opacity:.78; transform:scale(1); filter:brightness(1.04); text-shadow:0 0 5px rgba(159,255,218,.24); } 37% { opacity:.98; transform:scale(1.04); filter:brightness(1.24); text-shadow:0 0 10px rgba(159,255,218,.58); } 50% { opacity:.76; transform:scale(1); filter:none; text-shadow:none; } 65% { opacity:.96; transform:scale(1.032); filter:brightness(1.18); text-shadow:0 0 8px rgba(159,255,218,.42); } 78% { opacity:.78; transform:scale(1); filter:none; text-shadow:none; } }
    @keyframes bonus-value-flash { 0%,100% { color:var(--accent); } 9% { color:#e8fff5; } 22% { color:var(--accent); } 37% { color:#d9ffeb; } 50% { color:var(--accent); } 65% { color:#d2ffe7; } 78% { color:var(--accent); } }
    @media (prefers-reduced-motion:reduce) { .goal-readout.bonus-flash .goal-bonus, .goal-readout.bonus-flash .goal-bonus b { animation:none; } }
    .settings-panel { position:fixed; z-index:15; top:calc(env(safe-area-inset-top) + 66px);
      right:max(12px,env(safe-area-inset-right)); width:min(242px,calc(100vw - 24px));
      max-height:calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 78px);
      padding:13px; box-sizing:border-box; border-radius:18px; display:grid; gap:8px;
      overflow-y:auto; overscroll-behavior:contain; touch-action:pan-y; }
    @supports (height:100dvh) {
      .settings-panel { max-height:calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 78px); }
    }
    .settings-panel.open { animation:panel-in .18s ease-out both; }
    .settings-title { display:flex; align-items:center; justify-content:space-between; color:var(--text); font-size:12px; font-weight:650; }
    .settings-title small { color:var(--muted); font-size:9px; font-weight:500; }
    .settings-panel button { width:100%; min-height:42px; text-align:left; touch-action:pan-y; }
    .settings-quality { display:grid; gap:5px; }
    .settings-quality label { color:var(--muted); font-size:9px; letter-spacing:.08em; text-transform:uppercase; }
    .settings-quality select { width:100%; min-height:42px; padding:0 10px; border:1px solid var(--line); border-radius:12px; color:var(--text); background:rgba(17,28,29,.58); font:inherit; font-size:11px; font-weight:600; touch-action:auto; }
    .settings-quality small { color:var(--muted); font-size:9px; line-height:1.35; }
    .settings-panel > #languageSelect { width:100%; min-height:42px; padding:0 10px; border:1px solid var(--line); border-radius:12px; color:var(--text); background:rgba(17,28,29,.58); font:inherit; font-size:11px; font-weight:600; touch-action:auto; }
    /* uiBlur=false has to cover the base button skin too. It previously disabled only `.glass`,
       leaving both fixed top buttons paying a live backdrop-filter compositing pass on the exact
       performance tier selected by a low-power iPhone. Solid fallback fills are already authored. */
    .low-graphics-ui .glass, .low-graphics-ui button, .low-graphics-ui .start-screen,
    .low-graphics-ui .confirm-backdrop { backdrop-filter:none; }
    .settings-panel .danger { color:#ffd8cf; border-color:rgba(255,158,144,.3); background:rgba(91,43,43,.32); }
    .confirm-backdrop { position:fixed; inset:0; z-index:19; display:grid; place-items:center; padding:20px; background:rgba(7,14,19,.34); backdrop-filter:blur(8px); }
    .confirm-card { width:min(330px,100%); padding:20px; border-radius:20px; }
    .confirm-card h2 { margin:0 0 7px; font-size:17px; }
    .confirm-card p { margin:0; color:var(--muted); font-size:11px; line-height:1.45; }
    .confirm-actions { display:flex; justify-content:flex-end; gap:8px; margin-top:18px; }
    .confirm-actions button { min-height:42px; }
    .confirm-actions .danger { color:#ffd8cf; border-color:rgba(255,158,144,.3); background:rgba(91,43,43,.44); }
    @keyframes panel-in { from { opacity:0; transform:translateY(-6px) scale(.98); } to { opacity:1; transform:none; } }
    .boost-action { background:linear-gradient(135deg,rgba(37,88,89,.66),rgba(42,58,65,.52)); border-color:rgba(164,247,229,.36); }
    .boost-action.active { background:linear-gradient(135deg,rgba(38,143,133,.82),rgba(52,90,104,.76)); border-color:rgba(220,255,245,.82); box-shadow:0 0 24px rgba(104,243,219,.28),0 10px 28px rgba(10,25,27,.16); }
    button, .steer-base, .steer-knob, .pedals, .round-btn { -webkit-user-select:none; user-select:none; -webkit-touch-callout:none; }
    button { appearance:none; border:1px solid var(--line); color:var(--text); background:rgba(17,28,29,.47); backdrop-filter:blur(16px); min-height:42px; padding:0 13px; border-radius:14px; font:inherit; font-size:11px; font-weight:600; letter-spacing:.02em; touch-action:none; box-shadow:0 10px 28px rgba(10,25,27,.12); }
    button:active, button.pressed { transform:scale(.97); background:rgba(24,42,41,.68); }
    button.active { background:rgba(96,162,131,.56); border-color:rgba(211,255,229,.5); }
    /* z-index ABOVE .mobile-controls (7), not below it. The steering surface is a wide invisible
       catchment that reaches the bottom centre, so at a lower z-index it silently swallowed every
       tap on the camera and On Road buttons — a control that looks live and is not. */
    .center-actions { position:fixed; z-index:9; left:50%; bottom:calc(var(--safe-bottom) + 4px); transform:translateX(-50%); display:flex; gap:6px; }
    .center-actions button { min-height:38px; padding:0 12px; background:rgba(17,28,29,.42); }
    .center-actions .camera-cycle { min-width:38px; padding:0 10px; font-size:13px; font-variant-numeric:tabular-nums; }
    /* TOUCH ANYWHERE TO STEER — the full-screen steer catchment (scripts/05-camera-ui.js, r261).
       z-index 1 is load-bearing and is the whole safety argument: it must sit ABOVE the canvas
       (so it receives the tap) and BELOW every interactive layer (.top-actions 6,
       .mobile-reveal-layer 6, .mobile-controls 7, .center-actions 9, .settings-panel 15), so a
       control's own pixels always outrank it. Microlet only — bike mode keeps its floating stick. */
    .steer-anywhere { position:fixed; inset:0; z-index:1; pointer-events:none; touch-action:none; }
    body.microlet-mode .steer-anywhere { pointer-events:auto; }
    .mobile-reveal-layer { position:fixed; inset:0; z-index:6; pointer-events:none; }
    .mobile-reveal-layer.active { pointer-events:auto; }
    .mobile-reveal-zone { position:absolute; bottom:0; width:230px; height:270px; touch-action:none; }
    .mobile-reveal-zone.steer-zone { left:0; }
    .mobile-reveal-zone.action-zone { right:0; }
    .mobile-controls { position:fixed; inset:0; z-index:7; pointer-events:none; transition:opacity .2s ease; }
    .mobile-controls.controls-hidden { opacity:0 !important; }
    /* THE STEERING SURFACE IS A CATCHMENT AREA, NOT A CONTROL. It is invisible and it has no
       centre: the stick below floats to wherever the thumb lands inside it. Sized so a thumb
       resting anywhere in the bottom-left of a phone starts a valid steer — the old 104px pad
       had to be hit, and missing it did nothing at all. `right:auto` and the width cap keep it
       off the pedal cluster; the pedals are later siblings so they win any residual overlap. */
    .steer-surface { position:absolute; left:0; bottom:0; width:min(52%,430px); height:min(64%,320px); pointer-events:auto; touch-action:none; }
    /* The stick itself. Positioned by 05-camera-ui.js in px from the surface's top-left, so it
       can sit at the thumb; the translate(-50%,-50%) makes those coordinates the CENTRE. */
    /* Eased when it springs back to its resting place, INSTANT while a thumb is on it — a
       transition left on during the drag is input lag you can feel. */
    /* Ring and nub sizes live in two custom properties so the size tiers below change numbers,
       not geometry: the arrow inset and the JS visual-travel scaling both derive from these. */
    .steer-stick { --sbase:150px; --snub:58px; position:absolute; left:0; top:0; width:0; height:0; transform:translate(0,0); transition:transform .17s cubic-bezier(.22,.9,.3,1); }
    .steer-surface[data-live="1"] .steer-stick, .steer-surface[data-live="1"] .steer-knob { transition:none; }
    /* Absolute + translate(-50%,-50%), NOT a centred grid. A 0x0 grid container still sizes its
       implicit column to its widest child, so `place-items:center` centred the track inside a
       242px track that began at the anchor — measured 93px to the right of where the thumb was. */
    .steer-stick > * { position:absolute; left:0; top:0; transform:translate(-50%,-50%); }
    /* The BASE ring. A circle with the three direction arrows ON the ring, not on the nub — a
       thumb covers the nub completely, so anything drawn there is invisible exactly while it is
       being used (the old design's mistake, twice). */
    .steer-base { width:var(--sbase); height:var(--sbase); border-radius:50%; border:2px solid rgba(255,255,255,.22); background:radial-gradient(circle,rgba(12,25,27,.10) 40%,rgba(12,25,27,.30)); box-shadow:inset 0 0 0 1px rgba(255,255,255,.05); opacity:.6; transition:opacity .18s ease; }
    /* Gamepad arrows: CSS border triangles (no font dependency — Impact has no arrow glyphs,
       measured the hard way). Positioned just inside the ring on their axis. */
    .steer-base i { position:absolute; width:0; height:0; opacity:.55; transition:opacity .12s ease,transform .12s ease; }
    .steer-base .sc-l { left:9px; top:50%; transform:translateY(-50%); border-top:8px solid transparent; border-bottom:8px solid transparent; border-right:11px solid rgba(255,255,255,.85); }
    .steer-base .sc-r { right:9px; top:50%; transform:translateY(-50%); border-top:8px solid transparent; border-bottom:8px solid transparent; border-left:11px solid rgba(255,255,255,.85); }
    .steer-base .sc-b { left:50%; bottom:9px; transform:translateX(-50%); border-left:8px solid transparent; border-right:8px solid transparent; border-top:11px solid rgba(255,255,255,.85); }
    /* The NUB: a plain round thumb puck, nothing printed on it (see above). */
    .steer-knob { width:var(--snub); height:var(--snub); border-radius:50%; border:1px solid rgba(255,255,255,.36); background:radial-gradient(circle at 36% 30%,rgba(255,255,255,.34),rgba(38,74,65,.72)); box-shadow:0 8px 20px rgba(7,23,20,.3); }
    /* Live: the surface is being held — ring brightens; the arrow of the axis in use lights. */
    .steer-surface[data-live="1"] .steer-base { opacity:1; border-color:rgba(159,224,189,.4); }
    .steer-surface[data-live="1"] .steer-knob { border-color:rgba(211,255,229,.6); box-shadow:0 8px 24px rgba(7,23,20,.36),0 0 18px rgba(120,230,190,.22); }
    .steer-surface[data-lean="L"] .sc-l { opacity:1; transform:translateY(-50%) scale(1.3); }
    .steer-surface[data-lean="R"] .sc-r { opacity:1; transform:translateY(-50%) scale(1.3); }
    .steer-surface[data-brake="1"] .sc-b { opacity:1; transform:translateX(-50%) scale(1.3); border-top-color:#ffb0a4; }
    .steer-surface[data-brake="1"] .steer-knob { background:radial-gradient(circle at 36% 30%,rgba(255,214,206,.5),rgba(138,48,40,.86)); }
    /* The boost meter. 04-gameplay drains 0.27/s and regens 0.13/s, so this bar is 3.7 seconds wide
       and takes 7.7 to refill — numbers the player had no way to see before, because the only
       readout was a battery on the CYCLIST'S BACK and microlet mode does not draw the cyclist. */
    .boost-action::after { content:""; position:absolute; left:0; right:0; bottom:0; height:6px; background:linear-gradient(to right,var(--accent) 0 var(--boost-fill,100%),rgba(255,255,255,.14) var(--boost-fill,100%) 100%); }
    .boost-action.empty { opacity:.55; }

    /* THE RIGHT HAND IS A COLUMN, NOT A ROW, AND THE ORDER IS BY REACH.
       Measured on an 844x390 phone, straight-line distance from the bottom-right corner (where the
       thumb actually rests) to each control's centre:

                        was          now
         gas            88px    →     95px    held for essentially the whole shift
         handbrake     145px    →    156px    the thumb rolls UP off the throttle onto the lever
         brake         209px    →   (gone in microlet mode — it is the lever, see below)
         boost         169px    →    (moved to the left hand, it is a tap not a hold)

       The old flat row of four put BRAKE furthest out and the handbrake nearer, which is backwards
       for how often each is wanted, and a flat row cannot get a fourth control anywhere near the
       thumb at all. Stacking the lever over gas makes the whole slide ONE motion, which is what the
       manoeuvre physically is.

       Sizes and offsets are custom properties so a tier changes four numbers, not twelve; a second
       copy of the arithmetic is what drifts. */
    .mobile-controls { --gas:min(108px,25vw); --brk:min(88px,21vw); --hb:min(74px,18vw); --bo:min(66px,16vw); --horn:min(58px,14vw);
                       --pedal-gap:min(12px,2.4vw); --pedal-rise:min(16px,3.5vw); --pedal-edge:max(14px,env(safe-area-inset-right)); --pedal-base:calc(var(--safe-bottom) + 12px); --pedal-lift:10px;
                       --hand-edge:max(14px,env(safe-area-inset-left)); --lever-w:min(176px,42vw); }
    .pedals { position:absolute; right:var(--pedal-edge); bottom:var(--pedal-base); display:flex; align-items:flex-end; gap:var(--pedal-gap); pointer-events:auto; }
    .round-btn { border-radius:50%; padding:0; display:grid; place-items:center; touch-action:none; }
    .pedal-btn { width:var(--gas); height:var(--gas); background:radial-gradient(circle at 34% 28%,rgba(255,255,255,.2),rgba(52,105,82,.76)); font-size:13px; }
    .brake-btn { width:var(--brk); height:var(--brk); margin-bottom:var(--pedal-rise); background:radial-gradient(circle at 34% 28%,rgba(255,255,255,.14),rgba(103,54,45,.66)); font-size:12px; }
    /* Centred over gas: the right offset is the edge plus half the difference in diameters. */
    .hop-btn { position:absolute; right:calc(var(--pedal-edge) + (var(--gas) - var(--hb)) / 2); bottom:calc(var(--pedal-base) + var(--gas) + var(--pedal-lift)); width:var(--hb); height:var(--hb); min-height:0; background:radial-gradient(circle at 34% 28%,rgba(255,255,255,.12),rgba(46,72,92,.62)); font-size:11px; pointer-events:auto; }
    /* BOOST AND HORN ARE TAPS, AND THEY GO ON THE RIGHT — which only became possible once the gas
       button went away. With auto-throttle the right thumb holds nothing except when it is braking,
       so a second row above the lever is free real estate; the left thumb, by contrast, is on the
       stick continuously and every spot near it is already under the stick's own travel. Measured
       from the bottom-right corner on an 844x390 phone: horn 167px, boost 199px, both inside the
       reach the old boost button missed at 169px while ALSO demanding to be held. */
    .horn-btn { position:absolute; right:var(--pedal-edge); bottom:calc(var(--pedal-base) + var(--hb) + var(--pedal-lift)); width:var(--horn); height:var(--horn); min-height:0; color:var(--text); pointer-events:auto; background:rgba(17,28,29,.5); }
    .boost-action { position:absolute; right:calc(var(--pedal-edge) + var(--horn) + var(--pedal-lift)); bottom:calc(var(--pedal-base) + var(--hb) + var(--pedal-lift)); width:var(--bo); height:var(--bo); min-height:0; font-size:19px; pointer-events:auto; }
    .horn-btn svg { width:62%; height:62%; }

    /* ==========================================================================================
       THE MICROLET SKIN. Everything above is the bicycle game's glass, and the two games do not
       share a look: the arcade language is scoped to body.microlet-mode exactly as the chooser,
       the minimap and the fare HUD already scope theirs.

       The rule the rest of that language states outright (see the ARCADE TOKENS block at the top
       of this file) is that a border-radius is the single strongest "modern app" signal on these
       screens, and there is not one left in microlet mode. These controls were four blurred glass
       circles — the most web-app object in the entire game, sitting on top of it for the whole
       shift. So: die-cut notches, a true parallelogram for the lever, flag colours, Impact caps,
       and hard offset shadows instead of soft ones.

       backdrop-filter goes too. It is not merely off-language, it is the most expensive thing on
       a phone GPU that already has a 3D scene to draw, and it was being applied to four elements
       every frame.

       NOTE THE FILTER, IT IS NOT A TIDY-UP: box-shadow is CLIPPED by clip-path, so a notched plate
       silently loses its offset shadow however the shadow is declared, and a BORDER is clipped on
       the notched edge too. Hence drop-shadow() for the shadow and painted background bands
       instead of borders. Getting this wrong looks like a shadow that is simply absent. */
    body.microlet-mode .round-btn,
    /* The reset that makes the arcade plates possible: the base skin's .round-btn is a blurred
       glass CIRCLE, and every microlet button below is a notched or slanted CLIP-PATH — border-
       radius composes with clip-path (round first, then cut), so without this the parallelogram
       lever renders as an ellipse and the boost meter bar pokes out of a circle's edge. This line
       was deleted once during the stick redraw and all three plates reverted to blobs on the live
       site — .round-btn must stay in it even though the stick itself is now legitimately round. */
    body.microlet-mode .round-btn { border:0; border-radius:0; backdrop-filter:none; box-shadow:none; }
    body.microlet-mode .round-btn { font-family:var(--arcade-display); font-weight:400; letter-spacing:.04em; text-transform:uppercase; }

    /* THE LEVER. A parallelogram, wide and low, in the flag's red with a gold cap rule — a lever is
       a thing you pull, and it should not be the same shape as the taps above it. It is the only
       right-hand hold in this mode: handbrake, foot brake and reverse in one (05-camera-ui.js). */
    body.microlet-mode .hop-btn {
      right:var(--pedal-edge); bottom:var(--pedal-base); width:var(--lever-w); height:var(--hb);
      clip-path:polygon(16px 0, 100% 0, calc(100% - 16px) 100%, 0 100%);
      background:linear-gradient(to bottom, var(--gold) 0 6px, var(--blood) 6px 100%);
      color:var(--bone); font-size:min(22px,5.2vw); line-height:1;
      filter:drop-shadow(5px 5px 0 rgba(0,0,0,.55));
    }
    body.microlet-mode .hop-btn.pressed { background:linear-gradient(to bottom, var(--bone) 0 6px, #9d1a16 6px 100%); transform:translate(3px,3px); filter:drop-shadow(2px 2px 0 rgba(0,0,0,.55)); }
    /* The opener teaches the verb with the control itself. A gold pulse starts before the pickup
       ring, when braking is still useful, and repeats for the drop. No extra overlay or draw call. */
    body.microlet-mode.fare-brake-cue .hop-btn:not(.pressed) {
      animation:fare-brake-cue .7s ease-in-out infinite;
      filter:drop-shadow(0 0 12px rgba(255,199,44,.95)) drop-shadow(5px 6px 0 rgba(0,0,0,.65));
    }
    @keyframes fare-brake-cue { 0%,100% { transform:scale(1); } 50% { transform:scale(1.09); } }
    @media (prefers-reduced-motion:reduce) {
      body.microlet-mode.fare-brake-cue .hop-btn:not(.pressed) { animation:none; outline:4px solid var(--gold); }
    }
    /* Gas and the separate foot brake are gone here — the throttle is automatic and the brake is
       the lever. Left in the DOM because the bike game still uses both. */
    body.microlet-mode .pedal-btn, body.microlet-mode .brake-btn { display:none; }

    /* THE STEER PLATES. Same arcade plate as the lever opposite them, mirrored: notch on the outside
       edge, gold cap over blood, drop-shadow rather than box-shadow because clip-path clips both
       borders and box-shadow on the notched edge.
       They REPLACE the floating stick in microlet mode, which was measured not being found — the
       first real touch beacon braked to full authority on a visible button (bmax=1.00) and never
       steered at all on the invisible catchment (smax=0.03). Sized off the same --hb token as the
       lever so both thumbs get the same target, floored at 62px so a 320px-wide handset cannot
       shrink them under a comfortable thumb.
       `hidden` is respected with !important so the pair cannot appear in bike mode or on a keyboard;
       05-camera-ui.js removes the attribute only when it binds them. */
    .steer-buttons[hidden] { display:none !important; }
    body.microlet-mode .steer-buttons {
      position:absolute; left:max(14px,env(safe-area-inset-left)); bottom:var(--pedal-base);
      display:flex; gap:min(10px,2.2vw); pointer-events:none;
    }
    body.microlet-mode .steer-btn {
      width:max(62px,var(--hb)); height:var(--hb); min-height:0; pointer-events:auto;
      background:linear-gradient(to bottom, var(--gold) 0 6px, var(--blood) 6px 100%);
      filter:drop-shadow(5px 5px 0 rgba(0,0,0,.55));
    }
    body.microlet-mode .steer-btn-l { clip-path:polygon(16px 0, 100% 0, 100% 100%, 0 100%); }
    body.microlet-mode .steer-btn-r { clip-path:polygon(0 0, 100% 0, calc(100% - 16px) 100%, 0 100%); }
    /* `.echo` is the touch-anywhere catchment lighting the plate that matches the side of the screen
       being held (r261) — the forgiving surface teaching the visible control rather than replacing
       it. Its OWN class, not `.pressed`: that one belongs to the plate's own pointer handler, and
       two writers of one class is the same trap in miniature as two writers of one input. */
    body.microlet-mode .steer-btn.pressed, body.microlet-mode .steer-btn.echo {
      background:linear-gradient(to bottom, var(--bone) 0 6px, #9d1a16 6px 100%);
      transform:translate(3px,3px); filter:drop-shadow(2px 2px 0 rgba(0,0,0,.55));
    }
    /* CSS-drawn triangles, not ‹ › characters: --arcade-display is Impact with condensed fallbacks
       and renders those as 3px parentheses — the trap the settings knob's carets already hit. */
    body.microlet-mode .steer-btn i {
      width:0; height:0; display:block;
      border-top:min(15px,3.4vw) solid transparent; border-bottom:min(15px,3.4vw) solid transparent;
    }
    body.microlet-mode .steer-btn-l i { border-right:min(19px,4.2vw) solid var(--bone); margin-right:min(4px,1vw); }
    body.microlet-mode .steer-btn-r i { border-left:min(19px,4.2vw) solid var(--bone); margin-left:min(4px,1vw); }
    /* The stick stands down where the plates stand up: ONE owner for the steer input, and an
       invisible 230x320 catchment left live would keep swallowing taps meant for the world. */
    body.microlet-mode .steer-surface { display:none; }

    /* PORTRAIT IS THE ONLY ORIENTATION ANYONE ACTUALLY ARRIVES IN — 14 of 14 real phone beacons
       reported `or=p`, with the rotate nudge live the whole time. So the ride HUD is laid out for it
       here rather than treated as a degraded landscape.
       WHAT WAS WRONG, measured by overlapping every positioned element pairwise rather than checking a
       list: the top-right 126px was triple-booked. `.top-actions` sat ON the fare HUD (80x42 of
       overlap), and the stage strip's right end — which is where KM/H lives — ran straight through the
       leaderboard (117x21), so the speed read on top of a score. The existing three-row trim is gated
       on `max-height:430px`, which a 725px-tall portrait screen never matches, so the board ran five
       rows to y192 and collided with both.
       The fix is a stack with no shared bands: fare HUD, then the gradient/KM-H strip, then the
       buttons, then a three-row board. Short landscape has its own rule immediately below. */
    @media (orientation:portrait) and (max-width:820px) {
      /* THE BUTTONS ARE IN THE CORNER, STACKED — and the reason they were not is worth keeping.
         r206 pushed them 110px down because two 42px buttons side by side need 80px and the
         top-right has only 58px free beside the fare HUD, so they overlapped it 80x42. Stacking
         them uses that 58px instead of fighting it, which is only safe because the fare HUD is a
         FIXED 244px wide (`width:244px` + a -122px translate) and was measured NOT to grow with
         the cash figure — forced to $999,999 its right edge stayed at 302. So the gap is stable,
         not merely empty today.
         This returns the third HUD row to the road, which is what the owner actually asked for:
         at y122-164 the pair sat over the drive view. Targets go UP, 42/34 -> 44/44.
         Height budget: 44 + 2 + 44 = 90 from y4, ending exactly at the KM/H strip's y94. */
      body.microlet-mode .top-actions {
        top:max(4px,env(safe-area-inset-top)); right:max(8px,env(safe-area-inset-right));
        flex-direction:column; gap:2px;
      }
      body.microlet-mode .top-actions button {
        min-width:44px; width:44px; height:44px; min-height:44px; padding:0; text-align:center;
      }
      /* Below the buttons. Three rows, matching the landscape trim's reasoning: a driver glances at
         this, and five rows down a 360px-wide screen is a column of text over the road. */
      body.microlet-mode .tdt-rideboard { top:calc(max(12px,env(safe-area-inset-top)) + 162px); }
      body.microlet-mode .tdt-rideboard-line:nth-child(n+5) { display:none; }
      /* The name column is 128px on a screen that is 360 wide; give the cash room instead. */
      body.microlet-mode .tdt-rideboard-who { max-width:84px; }
    }

    /* SAFARI LANDSCAPE WITH BROWSER CHROME IS WIDE AND EXTREMELY SHORT. The owner's iPhone capture
       is 683x222 CSS pixels after the address and tab bars take their share. Width breakpoints leave
       the desktop 76px clock intact there, where the complete fare stack covers the mikrolet from
       roof to bumper. Make it a 43px instrument strip, keep the target, and remove redundant HI
       (already visible in the score board). At the captured 222px height, the tighter rule below
       also drops that board because it occupies the same right-hand pixels as Boost/Horn; a normal
       fullscreen landscape keeps it. Controls retain 44px+ targets but stop consuming a third of
       the remaining height. Specificity on the custom properties beats the later generic
       max-height:520px tier without duplicating its whole layout. */
    @media (orientation:landscape) and (max-height:430px) and (hover:none),
           (orientation:landscape) and (max-height:430px) and (pointer:coarse) {
      body.microlet-mode .fare-hud { top:max(5px,env(safe-area-inset-top));
        width:min(310px,calc(100vw - 260px)); }
      body.microlet-mode .fare-board { grid-template-columns:auto auto minmax(0,1fr);
        grid-template-areas:"bar bar bar" "clock cash target"; align-items:center; column-gap:9px; row-gap:1px; }
      body.microlet-mode .fare-bar { grid-area:bar; height:7px; margin:0 0 2px; }
      body.microlet-mode .fare-clock { grid-area:clock; font-size:38px; line-height:.88;
        -webkit-text-stroke-width:3px; text-shadow:2px 3px 0 rgba(11,14,12,.55); }
      body.microlet-mode .fare-money { grid-area:cash; display:block; }
      body.microlet-mode .fare-cash { font-size:22px; -webkit-text-stroke-width:2px;
        text-shadow:2px 2px 0 rgba(11,14,12,.5); }
      body.microlet-mode .fare-hi { display:none; }
      body.microlet-mode .fare-target { grid-area:target; min-width:0; margin:0; text-align:left;
        overflow:hidden; white-space:nowrap; text-overflow:ellipsis; font-size:10px; letter-spacing:.08em;
        -webkit-text-stroke-width:2px; }
      body.microlet-mode .fare-flash { left:auto; right:0; top:calc(100% + 5px); font-size:24px; }
      body.microlet-mode .fare-grade { left:auto; right:0; top:calc(100% + 42px); font-size:9px; }
      body.microlet-mode .fare-combo { right:auto; left:0; top:calc(100% + 5px); font-size:19px; }
      body.microlet-mode .fare-stamp { top:26%; font-size:38px; }
      body.microlet-mode .mobile-controls { --hb:56px; --bo:46px; --horn:46px; --lever-w:138px;
        --pedal-lift:8px; --pedal-base:calc(var(--safe-bottom) + 8px); }
      body.microlet-mode .steer-btn { width:56px; height:56px; }
      body.microlet-mode .steer-btn i { border-top-width:12px; border-bottom-width:12px; }
      body.microlet-mode .steer-btn-l i { border-right-width:15px; }
      body.microlet-mode .steer-btn-r i { border-left-width:15px; }
      body.microlet-mode .hop-btn { font-size:18px; }
      body.microlet-mode .boost-action { font-size:20px; }
      body.microlet-mode .fullscreen-help { width:min(260px,calc(100vw - 24px)); padding:9px 11px 10px; }
      body.microlet-mode .fullscreen-help strong { font-size:18px; }
      body.microlet-mode .fullscreen-help span { font-size:10px; }
      body.microlet-mode .fullscreen-help button { min-height:32px; margin-top:7px; }
    }
    /* At 260px and below the upper Boost/Horn row reaches the score board. A normal fullscreen
       landscape (roughly 390px tall) keeps the board; only the browser-chrome squeeze drops it. */
    @media (orientation:landscape) and (max-height:260px) and (hover:none),
           (orientation:landscape) and (max-height:260px) and (pointer:coarse) {
      body.microlet-mode .tdt-rideboard { display:none !important; }
    }

    /* The two taps. Notched plates, gold for the one that costs a resource and ink for the one
       that costs nothing, which is the same colour logic the chooser uses. */
    body.microlet-mode .boost-action, body.microlet-mode .horn-btn {
      clip-path:polygon(0 0, calc(100% - 11px) 0, 100% 11px, 100% 100%, 11px 100%, 0 calc(100% - 11px));
      filter:drop-shadow(4px 4px 0 rgba(0,0,0,.5));
    }
    body.microlet-mode .boost-action { background:var(--ink); color:var(--gold); font-size:min(26px,6vw); }
    body.microlet-mode .boost-action.active { background:var(--gold); color:var(--ink); }
    /* The meter in the flag's gold, matching the lever's cap rule, and 7px so it reads as paint on
       a plate rather than as a progress bar. It is the ONLY thing on screen that says how much
       boost is left. */
    body.microlet-mode .boost-action::after { height:7px; background:linear-gradient(to right,var(--gold) 0 var(--boost-fill,100%),rgba(244,240,227,.16) var(--boost-fill,100%) 100%); }
    body.microlet-mode .boost-action.active::after { background:var(--ink); }
    body.microlet-mode .horn-btn { background:var(--ink); color:var(--bone); }
    body.microlet-mode .horn-btn.pressed { background:var(--gold); color:var(--ink); }
    body.microlet-mode .boost-action.pressed, body.microlet-mode .horn-btn.pressed { transform:translate(2px,2px); filter:drop-shadow(2px 2px 0 rgba(0,0,0,.5)); }

    /* THE STICK AS A DESTINATION BOARD. A mikrolet's route board is a slotted plate with a card
       that slides in it, which is the same object as a horizontal one-axis stick — so the track is
       the slot (ink, gold rule under it) and the knob is the card (bone, notched, gold chevrons). */
    /* The slot is a RULE, not a panel. At .72 ink across the full 242px it read as a black UI band
       laid over the bottom-left of the road; the gold line under it is what actually says "this
       thing slides", so the fill only has to lift the knob off the grass. */
    /* THE STICK AS CABINET HARDWARE. The arcade rule bans the rounded-rect app look, not circles —
       a circle here is the one shape that IS the reference object: a ball-top joystick on an ink
       bezel. Ring in ink with the gold rule as its rim, arrows in bone, and the nub is the blood
       ball with a bone catch-light. The previous notched plate + underline read as a T-shaped
       mystery object, per the owner, and printed its arrows where the thumb hides them. */
    body.microlet-mode .steer-base { border:0; background:radial-gradient(circle,rgba(11,14,12,.10) 44%,rgba(11,14,12,.44)); box-shadow:inset 0 0 0 3px var(--gold),inset 0 0 0 6px rgba(11,14,12,.55); opacity:.8; }
    body.microlet-mode .steer-base .sc-l { border-right-color:var(--bone); }
    body.microlet-mode .steer-base .sc-r { border-left-color:var(--bone); }
    body.microlet-mode .steer-base .sc-b { border-top-color:var(--bone); }
    body.microlet-mode .steer-knob { border:0; background:radial-gradient(circle at 34% 28%,#ff8a7e 0 8%,var(--blood) 46%,#8f1512 100%); box-shadow:0 5px 0 rgba(0,0,0,.45),0 8px 14px rgba(0,0,0,.3); filter:none; }
    body.microlet-mode .steer-surface[data-live="1"] .steer-base { opacity:1; }
    body.microlet-mode .steer-surface[data-lean="L"] .sc-l,
    body.microlet-mode .steer-surface[data-lean="R"] .sc-r { border-right-color:var(--gold); border-left-color:var(--gold); }
    body.microlet-mode .steer-surface[data-brake="1"] .sc-b { border-top-color:var(--gold); }
    /* Braking: the ball drops to bone so the blood ▼ state reads even with the ball at the bottom
       of the ring — blood-on-blood was exactly the collision the old skin had with its arrows. */
    body.microlet-mode .steer-surface[data-brake="1"] .steer-knob { background:radial-gradient(circle at 34% 28%,#fffdf4 0 8%,var(--bone) 52%,#b8b09a 100%); }

    /* The speed pill is drawn by 07-stage-select.js at bottom-left — which is now where the steering
       stick floats, and lifting it merely moved the collision onto the horn button. So on touch it
       moves to the TOP-left instead, under the ♪ widget (12 + 34 = 46, hence 54), where nothing else
       lives: the fare HUD starts at x=263 and the settings gear is on the far right. Desktop keeps
       the bottom-left placement — it has no stick to collide with. Specificity beats the injected
       .tdt-hud rule, which is why this is `body .tdt-hud` and not `.tdt-hud`. */
    @media (hover:none), (pointer:coarse) {
      body .tdt-hud { top:calc(env(safe-area-inset-top) + 54px); bottom:auto; }
    }
    /* WHERE AM I — the place readout (scripts/44-place-readout.js).
       Text only on no panel, like everything else in the ride HUD: the sky behind it is bright and
       the road is dark, so one dark text-shadow carries both, and a blurred glass box would make a
       label the most prominent thing on the screen (the mistake the fare board and the HUD strip
       were both stripped back for).
       LEFT COLUMN, UNDER THE KM/H STRIP, because the right side is spoken for: `.tdt-rideboard`
       owns the top-right and the fare HUD owns the top-centre. `pointer-events:none` and
       aria-hidden, so it cannot eat a tap the way `.center-actions` once ate the camera button —
       which matters because it overlaps the invisible steer catchment. */
    .tdt-place { position:fixed; z-index:4; pointer-events:none;
      display:flex; align-items:baseline; gap:8px; max-width:min(52vw,260px);
      text-shadow:2px 2px 0 rgba(0,0,0,.85), 0 0 10px rgba(0,0,0,.5);
      transition:opacity .3s ease; }
    .tdt-place[hidden] { display:none; }
    /* Desktop sits under the bottom-left HUD panel; touch moves with it to the top-left. */
    .tdt-place { left:max(12px, env(safe-area-inset-left)); bottom:calc(var(--safe-bottom) + 34px); }
    /* Empty means the stage has no place within range — collapse rather than reserve a gap, the
       same reason #gateRoad uses :empty. Checked on the NAME, since meta is empty inside a town. */
    .tdt-place:has(.tdt-place-name:empty) { opacity:0; }
    .tdt-place-name { font-family:var(--arcade-display); font-weight:400; font-style:oblique 9deg;
      font-size:19px; line-height:1.1; letter-spacing:.02em; color:var(--bone);
      overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
    /* The distance is the secondary half of a road sign, so it is smaller and gold — the same
       colour the cash figure uses for "this is a number", not the bone used for names. */
    .tdt-place-meta { flex:none; font-family:var(--arcade-util); font-size:11px; font-weight:800;
      letter-spacing:.14em; color:var(--gold); }
    /* Heading somewhere gets a caret drawn in CSS, NOT a glyph: --arcade-display is Impact with
       condensed fallbacks and narrow decorative coverage, which is why the plates' arrows are
       border triangles and the carets on the settings knob had to be too. */
    .tdt-place.is-heading .tdt-place-name::before {
      content:''; display:inline-block; width:0; height:0; margin-right:7px;
      border:5px solid transparent; border-left-color:var(--gold); vertical-align:middle; }
    /* ARRIVING is the one moment worth animating, and it is what makes a district hop legible:
       the bus is moved forward, the name changes, and this says so. Two pulses, then still. */
    .tdt-place.is-arriving .tdt-place-name { animation:tdt-place-pop 1.2s ease-out 2; }
    @keyframes tdt-place-pop {
      0% { transform:none; color:var(--bone); }
      18% { transform:translateY(-2px) scale(1.06); color:var(--gold); }
      100% { transform:none; color:var(--bone); }
    }
    @media (prefers-reduced-motion:reduce) {
      .tdt-place.is-arriving .tdt-place-name { animation:none; color:var(--gold); }
    }
    @media (hover:none), (pointer:coarse) {
      /* Under the KM/H strip. THE ARITHMETIC HAS TO COME FROM A MEASUREMENT, NOT FROM THE CSS —
         `.tdt-hud`'s own rule reads `env(safe-area-inset-top) + 54px` but it COMPUTES to top:94px,
         so trusting the declaration put this element at y84 and straight through the strip's
         y94–115 band (found by the pairwise sweep, an 11px collision). The measured portrait stack
         is: fare HUD y6–82 → KM/H strip y94–115 → corner buttons y4–94 → three-row board y174–258.
         That leaves y115–174, and 124 sits in it with 9px of air above and 29px below.
         **Do not add anything else to this column without re-running the sweep.** */
      .tdt-place { top:calc(env(safe-area-inset-top) + 124px); bottom:auto; }
    }
    /* Landscape on a phone is ~683x222 CSS px with the playfield in the middle; the fare board is
       already a compact single row there and there is no vertical room for a second line of
       chrome. The world's own town signs still name the place in this orientation. */
    @media (hover:none) and (orientation:landscape) and (max-height:430px) {
      .tdt-place { display:none; }
    }
    .speed-vignette { position:fixed; inset:0; z-index:3; pointer-events:none; opacity:0; background:radial-gradient(circle at 50% 52%,transparent 38%,rgba(98,241,220,.16) 78%,rgba(86,207,229,.42) 100%),linear-gradient(90deg,rgba(130,255,226,.13),transparent 18%,transparent 82%,rgba(130,255,226,.13)); mix-blend-mode:screen; transition:opacity .12s linear; }
    /* LOADING SCREEN. It was a pale blue-green gradient with 11px letter-spaced "CREATING ROAD" —
       the last survivor of the pre-arcade design, and the screen with the most guaranteed audience
       in the whole game, because everybody waits here while the world builds.
       Now it is the title card: ink ground, the wordmark with its two-colour arcade shadow, the
       three painted rules, and a segmented bar marching. Nothing here knows the real progress —
       the world build reports none — so the bar is honestly INDETERMINATE rather than a fake
       percentage creeping to 90 and stopping. */
    .loading { position:fixed; inset:0; z-index:20; display:grid; place-items:center; align-content:center; gap:0;
      background:
        radial-gradient(120% 90% at 50% 38%, rgba(255,199,44,.10), transparent 62%),
        linear-gradient(180deg, #101714, var(--ink) 60%);
      color:var(--bone); transition:opacity .55s,visibility .55s; }
    .loading.hidden { opacity:0; visibility:hidden; }
    .loading-art { display:grid; justify-items:center; gap:0; padding:0 20px; }
    .loading-flag { width:34px; height:20px; margin-bottom:14px;
      background:linear-gradient(96deg, var(--gold) 0 30%, var(--ink) 30% 47%, var(--blood) 47%);
      box-shadow:inset 0 0 0 1px rgba(244,240,227,.3); }
    .loading-word { font-family:var(--arcade-display); font-size:clamp(40px,11vw,74px); font-weight:400;
      font-style:oblique 9deg; line-height:.86; letter-spacing:.006em; text-transform:uppercase;
      text-align:center; color:var(--bone);
      text-shadow:3px 3px 0 var(--blood), 7px 7px 0 rgba(0,0,0,.55); }
    .loading-word i { font-style:oblique 9deg; color:var(--gold); }
    .loading-band { display:flex; flex-direction:column; width:min(300px,72vw); margin-top:20px; }
    .loading-band i:nth-child(1) { height:5px; background:var(--gold); }
    .loading-band i:nth-child(2) { height:3px; background:var(--blood); }
    .loading-band i:nth-child(3) { height:2px; background:var(--bone); opacity:.55; }
    /* Segmented, masked the same way the fare timer is, so the loading screen and the HUD are
       speaking one visual language. The fill is a sweep rather than a width, because there is no
       progress figure to bind a width to. */
    .loading-bar { width:min(300px,72vw); height:12px; margin-top:18px; overflow:hidden;
      background:rgba(244,240,227,.10);
      -webkit-mask-image:repeating-linear-gradient(90deg,#000 0 calc(100% / 16 - 3px), transparent calc(100% / 16 - 3px) calc(100% / 16));
      mask-image:repeating-linear-gradient(90deg,#000 0 calc(100% / 16 - 3px), transparent calc(100% / 16 - 3px) calc(100% / 16)); }
    .loading-bar i { display:block; width:45%; height:100%; background:var(--gold);
      animation:loading-sweep 1.15s linear infinite; }
    @keyframes loading-sweep { from { transform:translateX(-105%); } to { transform:translateX(325%); } }
    .loading-text { margin-top:22px; font-family:var(--arcade-util); font-size:13px; font-weight:700;
      letter-spacing:.24em; text-transform:uppercase; color:rgba(244,240,227,.6); text-align:center;
      padding:0 20px; }
    /* The failure path un-hides the screen and writes here; make it unmissable rather than a
       whisper in the same grey as "creating road". */
    .loading.is-error .loading-text { color:#ff8a5c; }
    .loading.is-error .loading-bar { display:none; }
    /* THE BIKE GAME IS NOT THIS GAME. Stamped on <html> by the head script before first paint, so
       the cycling side keeps its own name instead of being told it is a mikrolet. Two spans rather
       than a pseudo-element: hiding the <i> would leave the leading text node and read
       "Mikrolet Tour de Timor". */
    .loading-word .lw-bike { display:none; }
    html[data-vehicle="bike"] .loading-word .lw-taxi { display:none; }
    html[data-vehicle="bike"] .loading-word .lw-bike { display:inline; }
    @media (prefers-reduced-motion:reduce) { .loading-bar i { animation:none; width:100%; opacity:.5; } }

    /* THE NAME GATE — the microlet game's first screen, above the loading overlay (z-index 20) and
       the start screen (12). It outlives the loading screen on purpose: #loading hides itself when
       the world is built, and this stays until the player presses START, so the engine can finish
       behind a screen the player is already using.
       No display:none on the bike branch alone — the wiring removes the node — but the rule is here
       as well so the cycling game never paints it for even one frame. */
    html[data-vehicle="bike"] .name-gate { display:none; }
    /* A GAMEPLAY STILL BEHIND THE NAME BOX, so the first screen shows what the game IS. The gate
       replaced a route chooser that at least had road names on it; a flat dark panel asks people to
       type their name into something they have no picture of.
       It is the same frame as the Facebook share card (og-mikroletbulak.jpg), re-encoded at 900px /
       q34 down to 49KB — it sits under a heavy scrim, so fidelity buys nothing and bytes cost real
       time on this audience's connection.
       A CSS background, deliberately: the browser fetches it at low priority and it can never block
       the gate's text from painting, which is the one thing this screen must do fast (measured at
       ~1.8s). On a slow link the text is up first and the picture arrives behind it. */
    .name-gate { position:fixed; inset:0; z-index:30; display:grid; place-items:center;
      padding:max(16px,env(safe-area-inset-top)) 16px max(16px,env(safe-area-inset-bottom));
      background:
        /* The scrim earns its keep by being as LIGHT as the text allows, not as dark as looks safe.
           The first attempt sat at .86-.94 and buried the picture entirely, which defeats the only
           reason it is here. The two solid controls (bone input, gold button) are opaque and need no
           help; the small labels get their own text-shadow below instead of the whole image being
           dimmed to protect them. */
        radial-gradient(115% 85% at 50% 44%, rgba(11,14,12,.72) 0%, rgba(11,14,12,.55) 45%, rgba(11,14,12,.34) 100%),
        linear-gradient(180deg, rgba(16,23,20,.30), rgba(11,14,12,.52)),
        url("./gate-bg.jpg") center/cover no-repeat,
        linear-gradient(180deg, #101714, var(--ink) 62%);
      color:var(--bone); overflow:auto; }
    .name-gate-card { display:flex; flex-direction:column; align-items:center; width:min(340px,100%); }
    .name-gate-word { font-family:var(--arcade-display); font-size:clamp(34px,10vw,52px); line-height:.92;
      letter-spacing:.01em; text-transform:uppercase; text-align:center; color:var(--bone);
      text-shadow:3px 3px 0 var(--blood), 7px 7px 0 rgba(0,0,0,.55); }
    .name-gate-word i { font-style:oblique 9deg; color:var(--gold); }
    .name-gate-band { display:flex; flex-direction:column; width:min(300px,72vw); margin-top:16px; }
    .name-gate-band i:nth-child(1) { height:5px; background:var(--gold); }
    .name-gate-band i:nth-child(2) { height:3px; background:var(--blood); }
    .name-gate-band i:nth-child(3) { height:2px; background:var(--bone); opacity:.55; }
    /* THE ROAD THIS VISIT DREW. Sits between the band and START, so it reads as the destination on the
       ticket rather than as a setting to change. Empty until the engine names it, and `:empty` keeps it
       from reserving vertical space on the way — the gate's whole job is to look finished while the
       load finishes, and a blank gap above the button reads as something failed to render.
       Wraps rather than ellipsising: "Baucau → Lospalos · 89 KM" does not fit one line at 320px, and
       the cramped 91px `.tdt-hud` strip is precisely the mistake this element exists to correct. */
    .name-gate-road { margin-top:14px; font-family:var(--arcade-util); font-size:12px; font-weight:800;
      letter-spacing:.14em; text-transform:uppercase; color:var(--gold);
      opacity:0; transition:opacity .35s ease; text-wrap:balance; }
    .name-gate-road:empty { display:none; }
    .name-gate-road.is-shown { opacity:1; }
    /* 16px minimum, because anything smaller makes iOS Safari zoom the viewport on focus — on a
       landscape game that is a layout the player then has to pinch their way back out of. */
    .name-gate-input { width:100%; margin-top:10px; padding:13px 14px; box-sizing:border-box;
      font-family:var(--arcade-display); font-size:26px; letter-spacing:.06em; text-align:center;
      text-transform:uppercase; color:var(--ink); background:var(--bone);
      border:0; border-radius:0; outline:0;
      /* clip-path clips borders and box-shadow on the notched edge, so the frame is a painted band
         and the lift is a drop-shadow filter — same rule as .arcade-cut. */
      filter:drop-shadow(5px 5px 0 rgba(0,0,0,.55)); }
    .name-gate-input:focus-visible { box-shadow:inset 0 0 0 3px var(--gold); }
    .name-gate-hint { margin-top:9px; font-family:var(--arcade-util); font-size:11px; font-weight:700;
      letter-spacing:.18em; text-transform:uppercase; color:rgba(244,240,227,.66);
      text-shadow:0 2px 6px rgba(0,0,0,.9), 0 0 2px rgba(0,0,0,.85); }
    /* THE HANDLE ROW sits BELOW START, because START is the primary action and this is a disclosure —
       it tells the player the name they are about to appear under rather than asking them for one.
       The dotted rule is the whole affordance, drawn in CSS rather than set as a pencil glyph:
       --arcade-display is Impact with condensed fallbacks and its coverage of decorative characters
       is narrow, which is why the settings knob's carets had to become border triangles. */
    .name-gate-who { display:flex; flex-direction:column; align-items:center; width:100%; margin-top:18px; }
    .name-gate-who[hidden] { display:none !important; }
    .name-gate-handle { padding:2px 3px 4px; font-family:var(--arcade-display); font-size:20px;
      letter-spacing:.08em; text-transform:uppercase; color:var(--bone); background:none;
      border:0; border-bottom:2px dotted rgba(244,240,227,.5); border-radius:0; cursor:pointer;
      text-shadow:0 2px 6px rgba(0,0,0,.9), 0 0 2px rgba(0,0,0,.85); }
    .name-gate-handle:hover { color:var(--gold); border-bottom-color:var(--gold); }
    .name-gate-who .name-gate-hint { margin-top:7px; }
    .name-gate-go { width:100%; margin-top:20px; padding:15px 18px 17px; box-sizing:border-box;
      font-family:var(--arcade-display); font-size:30px; line-height:1; letter-spacing:.06em;
      text-transform:uppercase; color:var(--ink); background:var(--bone); border:0; border-radius:0;
      cursor:pointer; filter:drop-shadow(6px 6px 0 var(--ink)); transition:transform .09s, filter .09s; }
    /* Disabled is the honest state while the world builds — it says loading rather than pretending
       to be pressable and then doing nothing, which is the failure this whole screen replaces. */
    .name-gate-go[disabled] { cursor:default; color:rgba(11,14,12,.45); background:rgba(244,240,227,.42);
      filter:drop-shadow(6px 6px 0 rgba(0,0,0,.4)); }
    .name-gate.is-ready .name-gate-go { background:var(--gold); color:var(--ink); }
    .name-gate.is-ready .name-gate-go:hover { transform:translate(-2px,-2px); filter:drop-shadow(8px 8px 0 var(--ink)); }
    .name-gate.is-ready { cursor:pointer; }
    .name-gate.is-starting .name-gate-go {
      background:var(--bone); color:var(--ink); transform:translate(3px,3px);
      filter:drop-shadow(2px 2px 0 var(--ink));
    }
    .name-gate.is-error .name-gate-go { background:#ff8a5c; color:var(--ink); }
    .name-gate-prog[hidden] { display:none; }
    /* BROWSER TOO OLD. html.tdt-too-old is stamped by the ES5 detect in the head BEFORE first
       paint, so this is a swap the player never sees happen: the progress bar and START (which
       could never light) give way to the static message in the same card. Sentence case, not the
       gate's uppercase — two sentences of Tetun in caps is a wall, and this one has to be read. */
    .name-gate-old, .loading-too-old { display:none; }
    .tdt-too-old .name-gate-old { display:block; margin-top:18px; }
    .tdt-too-old .name-gate-go, .tdt-too-old .name-gate-prog { display:none; }
    .name-gate-old p, .loading-too-old p { margin:0 0 10px; font-family:var(--arcade-util);
      font-size:14px; font-weight:700; line-height:1.45; letter-spacing:.04em; color:var(--bone);
      text-shadow:0 2px 6px rgba(0,0,0,.9), 0 0 2px rgba(0,0,0,.85); }
    .name-gate-old .name-gate-old-en, .loading-too-old .loading-too-old-en {
      font-size:11px; font-weight:600; color:rgba(244,240,227,.66); }
    .tdt-too-old .loading-too-old { display:block; margin-top:20px; padding:0 24px; max-width:420px; text-align:center; }
    .tdt-too-old .loading-bar, .tdt-too-old .loading-text { display:none; }
    .name-gate.is-ready .name-gate-go:active { transform:translate(3px,3px); filter:drop-shadow(2px 2px 0 var(--ink)); }

    /* GATE PROGRESS. Segmented and masked exactly like .loading-bar, because it is the same idea one
       layer up — but DETERMINATE: the width is real, driven by which critical resources have landed.
       The loading screen's own bar is a sweep because it has no figure to bind to; this one does, and
       an honest bar is the difference between "still working" and "possibly dead". */

    .name-gate-prog { width:100%; height:10px; margin-top:12px; overflow:hidden;
      background:rgba(244,240,227,.13);
      -webkit-mask-image:repeating-linear-gradient(90deg,#000 0 calc(100% / 20 - 3px), transparent calc(100% / 20 - 3px) calc(100% / 20));
      mask-image:repeating-linear-gradient(90deg,#000 0 calc(100% / 20 - 3px), transparent calc(100% / 20 - 3px) calc(100% / 20));
      transition:opacity .45s ease; }
    /* THE WIDTH IS TRUTH, THE SHIMMER IS LIFE, and the two do different jobs on purpose. Measured on
       a throttled phone the bar sits at 8% for about seven seconds while the 149 KB Three bundle
       downloads — everything else is requested in parallel and lands together at the end, so the
       honest profile really is flat-then-jump. A bar that appears frozen reads as a crash, which is
       the exact thing this element exists to prevent. So a highlight sweeps ACROSS the filled part:
       it never overstates how far along the load is, it only shows that work is still happening. */
    .name-gate-prog i { display:block; width:4%; height:100%; background:var(--gold);
      transition:width .3s ease; position:relative; overflow:hidden; }
    .name-gate-prog i::after { content:""; position:absolute; inset:0;
      background:linear-gradient(90deg, transparent, rgba(255,255,255,.85), transparent);
      animation:gate-prog-shimmer 1.1s linear infinite; }
    @keyframes gate-prog-shimmer { from { transform:translateX(-100%); } to { transform:translateX(100%); } }
    /* Once it is done the sweep stops — motion after completion would be a lie in the other direction. */
    .name-gate-prog.is-done i::after { animation:none; opacity:0; }
    @media (prefers-reduced-motion:reduce) { .name-gate-prog i::after { animation:none; opacity:.35; } }
    .name-gate-prog.is-done { opacity:0; }
    @media (max-height:430px) { .name-gate-prog { height:8px; margin-top:9px; } }
    .name-gate-hi { margin-top:16px; min-height:1em; font-family:var(--arcade-display); font-size:17px;
      letter-spacing:.05em; color:var(--gold); opacity:0; transition:opacity .3s;
      text-shadow:0 2px 6px rgba(0,0,0,.9), 0 0 2px rgba(0,0,0,.85); }
    .name-gate-hi.is-shown { opacity:.92; }
    /* Landscape on a phone is especially short: drop the wordmark and tighten
       the rhythm so the input and START are both above the fold without scrolling. */
    @media (max-height:430px) {
      .name-gate-word { font-size:clamp(26px,7vh,34px); }
      .name-gate-band, .name-gate-hint { display:none; }
      .name-gate-who { margin-top:12px; }
      .name-gate-handle { font-size:17px; }
      .name-gate-go { margin-top:13px; padding:11px 16px 13px; font-size:25px; }
      .name-gate-hi { margin-top:10px; }
    }
    .start-screen { position:fixed; z-index:12; left:max(16px,env(safe-area-inset-left)); top:50%; width:min(334px,calc(100vw - 32px)); padding:22px; border:1px solid rgba(255,255,255,.28); border-radius:24px; color:var(--text); background:linear-gradient(145deg,rgba(21,40,40,.82),rgba(28,48,48,.68)); backdrop-filter:blur(22px) saturate(135%); box-shadow:0 20px 60px rgba(15,31,32,.26),inset 0 1px 0 rgba(255,255,255,.1); transform:translateY(-50%); transition:opacity .42s,transform .42s,visibility .42s; }
    .start-screen.hidden { opacity:0; visibility:hidden; transform:translate(-18px,-50%); pointer-events:none; }
    .run-record { margin-top:16px; padding:11px 12px; border:1px solid rgba(159,224,189,.22); border-radius:14px; background:rgba(105,174,143,.12); }
    .run-record-line { display:flex; align-items:baseline; gap:5px; }
    .run-record strong { color:var(--accent); font-size:20px; line-height:1; font-variant-numeric:tabular-nums; }
    .run-record span { color:var(--text); font-size:10px; }
    .run-record small { margin-left:auto; color:var(--muted); font-size:9px; line-height:1.35; white-space:nowrap; }
    .run-record small b { color:var(--text); font-weight:650; font-variant-numeric:tabular-nums; }
    .start-section { margin-top:19px; }
    .start-section-label { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:8px; color:var(--text); font-size:10px; font-weight:650; letter-spacing:.08em; text-transform:uppercase; }
    .control-guide { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:9px; }
    .control-card { min-width:0; padding:12px 12px 11px; border:1px solid rgba(159,224,189,.2); border-radius:14px; background:linear-gradient(145deg,rgba(112,193,162,.16),rgba(7,19,21,.2)); box-shadow:inset 0 1px 0 rgba(255,255,255,.06); }
    .control-card-head { display:flex; align-items:center; gap:7px; color:var(--text); }
    .control-card-head strong { font-size:12px; font-weight:700; }
    .control-mark { width:21px; height:21px; display:grid; place-items:center; border:1px solid rgba(211,255,229,.28); border-radius:7px; color:var(--accent); background:rgba(159,224,189,.12); font-size:11px; font-weight:750; }
    .control-card small { display:block; margin-top:9px; color:var(--muted); font-size:10px; line-height:1.3; white-space:nowrap; }
    /* The microlet paint preview (built in 05-camera-ui.js). Sits above the swatches so the order
       reads as "here is your bus, here are the colours" rather than the reverse. */
    .paint-preview { display:flex; justify-content:center; margin:2px 0 9px; }
    .paint-preview svg { width:100%; max-width:172px; height:auto;
      filter:drop-shadow(0 5px 12px rgba(0,0,0,.34)); }
    .paint-preview .pp-body { transition:fill .18s ease; }
    @media (prefers-reduced-motion:reduce) { .paint-preview .pp-body { transition:none; } }
    .color-row { display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
    .color-choice { width:31px; min-height:31px; padding:0; border-radius:50%; border-color:rgba(255,255,255,.34); box-shadow:inset 0 1px 3px rgba(255,255,255,.3),0 4px 12px rgba(0,0,0,.16); }
    .color-choice.selected { border-color:#effff8; box-shadow:0 0 0 2px rgba(168,255,229,.48),inset 0 1px 3px rgba(255,255,255,.34),0 5px 17px rgba(34,124,100,.34); }
    .color-choice[data-bike-color="d9342b"] { background:#d9342b; }
    .color-choice[data-bike-color="2d77d9"] { background:#2d77d9; }
    .color-choice[data-bike-color="e5b52f"] { background:#e5b52f; }
    .color-choice[data-bike-color="f0f2ee"] { background:#f0f2ee; }
    .color-choice[data-bike-color="25383b"] { background:#25383b; }
    .color-choice[data-bike-color="ef6aa5"] { background:#ef6aa5; }
    .color-choice[data-bike-color="2bd86b"] { background:#2bd86b; }
    .color-choice[data-bike-color="a8e6b5"] { background:#a8e6b5; }
    .color-choice[data-bike-color="f08a4b"] { background:#f08a4b; }
    .color-choice[data-bike-color="8b6de8"] { background:#8b6de8; }
    .start-primary { width:100%; margin-top:17px; min-height:46px; color:#173a32; background:linear-gradient(135deg,#a8ead0,#d9ffe9); border-color:rgba(225,255,242,.8); box-shadow:0 12px 26px rgba(25,105,82,.2); font-size:13px; font-weight:800; letter-spacing:.12em; }
    @media (max-width:720px) { .start-screen { top:116px; transform:none; padding:18px; } .start-screen.hidden { transform:translate(-12px,0); } }
    /* The steer surface is a 430x320 invisible catchment, and on a mouse it would sit over the
       bottom-left third of the canvas swallowing the camera-look drag for a control the mouse does
       not use anyway (desktop steers on WASD). The old fixed pad was 142px and small enough not to
       matter; this one is not, so it stands down where there is a fine pointer. */
    @media (hover:hover) and (pointer:fine) { .mobile-reveal-layer { display:none; } .mobile-controls { opacity:.52; } .steer-surface { pointer-events:none; } body.microlet-mode .steer-anywhere { pointer-events:none; } button:hover { background:rgba(28,45,44,.66); } .boost-action { display:none; } }
    @media (max-width:720px) {
      .goal-readout { top:calc(max(14px,env(safe-area-inset-top)) + 2px); }
      .goal-readout strong { font-size:27px; }
      /* Smaller on a phone, and the field count goes: "4th +2.4s 12:07" is the whole
         read, and screen space beside the centred distance is the scarcest there is.
         This is the same overlap problem the HUD already had once. */
      .race-place b { font-size:34px; }
      .race-place sup { font-size:13px; }
      .race-field { display:none; }
      .race-gap { font-size:11px; min-height:12px; }
      .race-clock { font-size:12px; }
    }
    /* At 540px the stage HUD becomes a FULL-WIDTH strip across the top (its rule lives
       in scripts/07-stage-select.js, keyed to the same width). Measured at 500px: the
       strip occupies y 52-98 spanning the whole width, and this block at y 12-74 ran
       into it by 56 x 22px. There is no room above the strip -- the top actions already
       have y 12-54 -- so it goes below it. Clear on all four sides there: strip above,
       minimap top-right from y 128, touch controls at the bottom.

       One row rather than a column at this size, because below the strip the free space
       is wide and shallow, and two stacked lines would reach the minimap. */
    @media (max-width:540px) {
      /* Below the strip, so the audio toggle is above rather than beside it and the
         44px indent is not needed. */
      .race-hud { top:calc(env(safe-area-inset-top) + 106px); left:max(10px,env(safe-area-inset-left)); display:flex; align-items:baseline; gap:9px; }
      .tdt-audio[data-open="1"] ~ .race-hud { transform:none; opacity:.35; }
      /* The stage strip owns y 52-98 across the full width at this size, so the climb
         panel goes under it and the corner block moves down to clear both. */
      /* 208px, not 280. Measured at 500px wide: a 280px centred panel spans 110-390 and
         the minimap sits at 366-488 from y 128, so they overlapped by 24 x 41px. At 208
         the panel spans 146-354 and clears it. The widest line it has to hold is the name
         row -- "D1 KOM1 · 3.8km · 14%" at 9px is about 110px, so there is room. */
      .race-climb { top:calc(env(safe-area-inset-top) + 104px); width:min(208px,calc(100vw - 32px)); padding:7px 9px 8px; }
      .race-climb-time { font-size:22px; }
      .race-hud { top:calc(env(safe-area-inset-top) + 178px); }
      .race-place b { font-size:27px; }
      .race-place sup { font-size:11px; }
      .race-gap, .race-clock { margin-top:0; }
      .race-gap { font-size:12px; min-height:0; }
      .race-clock { font-size:12px; }
      /* Single row at this width, so the bar sits inline and shrinks rather than
         adding a fourth stacked line under the stage strip. */
      .race-legs { width:52px; height:4px; margin-top:0; align-self:center; }
      .top-actions button { min-width:42px; padding:0 10px; font-size:0; }
      /* 14px was the icon size on a 42px touch target. The glyph IS the button at this width, since
         font-size:0 hides the text label, so it may as well be legible. */
      .top-actions button::first-letter { font-size:19px; }
      #settingsBtn { font-size:19px; }
      /* Nothing to re-declare for the pedals here. Going from one flat row of four to two columns
         of two halved the cluster's width demand: at 390px it now asks for 14 + 97 + 9 + 82 = 202
         against the old row's 275, which leaves 188px of steer surface to land a thumb in instead
         of 115. The base rule's min(px,vw) terms carry it; a second copy of the sizes is what
         drifts, so this stays a note. */
      .steer-stick { --sbase:min(150px,36vw); --snub:min(58px,14vw); }
    }
    @media (max-height:520px) {
      .start-screen { top:12px; max-height:calc(100vh - 24px); overflow:auto; }
      .start-screen.hidden { transform:translate(-12px,0); }
      /* A landscape phone is SHORT, not narrow, so here the constraint is vertical and the vw terms
         in the base rule are all slack (25vw of 844 is 211px). Four numbers, and the whole cluster
         follows: lever row 10 + 74, tap row + 10 + 58, so 152 of 390 with the road untouched. */
      .mobile-controls { --gas:96px; --brk:78px; --hb:74px; --bo:58px; --horn:58px; --lever-w:172px;
                         --pedal-base:calc(var(--safe-bottom) + 10px); }
      .steer-stick { --sbase:132px; --snub:52px; }
      .pedal-btn { font-size:12px; }
      .brake-btn { margin-bottom:14px; }
      .hop-btn { font-size:10px; }
      .boost-action { font-size:17px; }

      .center-actions { bottom:var(--safe-bottom); }
    }

/* FORCED TOUCH (`?touch=1` — see the head script in index.html). A URL param cannot flip a CSS
   @media, so the touch rules a 1920x1080 kiosk actually needs are mirrored under the class the
   override stamps on <html>. Only the positional/visibility ones are mirrored: the small-phone
   height clamps above are meaningless on a 65" panel and stay media-gated. The .tdt-keys and
   .tdt-profile mirrors reach into CSS injected by 38-controls-hint.js and 11-profile.js — the
   html.class prefix outranks their injected selectors regardless of stylesheet order. */
html.tdt-force-touch body .tdt-hud { top:calc(env(safe-area-inset-top) + 54px); bottom:auto; }
html.tdt-force-touch .tdt-place { top:calc(env(safe-area-inset-top) + 124px); bottom:auto; }
html.tdt-force-touch .tdt-keys { display:none; }
html.tdt-force-touch .tdt-profile { display:none; }

/* THE KIOSK CONTROL CLUSTER. `?touch=1` makes the plates BIND on a 65" panel; it does not make them
   REACHABLE, and the difference was invisible until it was measured with getBoundingClientRect at
   1920 wide rather than read off the CSS above. The steer group lands x14-172 and the brake lever
   x1730-1906, so the two hands are asked to work **1,558px apart — about 1.17 METRES of glass on a
   65" 16:9 panel** — while both targets are only 74px tall (~55mm) and sit 12px off the bottom edge,
   inside the band where an IR touch frame loses accuracy. The phone layout is right for two thumbs
   on a 360px screen and wrong for two hands at a wall; it is not a skin problem.

   AND THAT PANEL IS SINGLE-TOUCH (`maxTouchPoints=1`, recorded when r257 was built), so steer and
   brake cannot overlap anyway. That is what makes clustering the correct fix rather than a cosmetic
   one: the player ALTERNATES between two controls, so the cost that matters is the travel between
   them, not the ability to hold both at once.

   Two things this also has to undo, both from the single desktop rule `?touch=1` structurally cannot
   flip (`@media (hover:hover) and (pointer:fine)` above) — the param forces only hover:none and
   pointer:coarse true, and the panel still answers pointer:fine natively, so that rule fires: the
   kiosk was drawing its controls at **opacity .52** and hiding **#boostBtn** outright.

   The row, left to right, and the arithmetic IS the design:
     [boost 92][28][ <- 170 ][28][ -> 170 ][ 120 dead centre ][ BRAKE 340 ][28][ horn 92 ]
   = 1,068px of 1920 (56%), about 800mm on a 65" panel, which is inside a standing two-hand span
   where 1.17m is not. Steer stays LEFT of centre and brake RIGHT of it so the phone's muscle memory
   and every steer/brake label in UI_TEXT still describe the same hands. The 120px dead band on the
   centre line is deliberate: any closer and the two hands foul each other mid-corner.

   Anchored `right:calc(50% + ...)` for the left half and `left:calc(50% + ...)` for the right, so
   the cluster self-centres at any panel width with NO transform on an ancestor — a transform there
   would make it the containing block for every fixed HUD element, which is the bug that once put
   BA! through the clock (see the body::after note near the top of this file). */
/* THE KIOSK MIRROR FOR TOUCH-ANYWHERE-TO-STEER, and it is required rather than tidy. `?touch=1`
   forces only hover:none/pointer:coarse through the matchMedia wrapper, so the panel still answers
   pointer:fine natively and the desktop rule above fires and sets this catchment to
   pointer-events:none. Without this line the 65" panel loses the whole feature — which is the one
   screen it helps most, because a touch anywhere on 1.4m of glass beats reaching for a plate. */
html.tdt-force-touch body.microlet-mode .steer-anywhere { pointer-events:auto; }

html.tdt-force-touch .mobile-controls {
  /* Semi-transparent, but nowhere near the .52 the desktop rule was imposing. That value is for a
     mouse user who never touches the plates; here they are the only way to drive, and at 150px tall
     across the bottom of a 65" panel they are a lot of solid red to put over the road. .82 keeps the
     plate unambiguous at three metres while letting the tarmac through. NOT applied to the buttons
     individually — one opacity on the wrapper composites the whole cluster as a single layer, so the
     drop-shadows do not show through the plates they belong to. */
  opacity:.82;
  --k-h:150px; --k-steer-w:170px; --k-lever-w:340px; --k-gap:28px; --k-mid:120px; --k-aux:92px;
  --k-bottom:72px;
  /* The steer pair's own width, needed to anchor the boost plate immediately outside it. */
  --k-steer-group:calc(var(--k-steer-w) * 2 + var(--k-gap));
}

/* KEYBOARD RECOVERY HAS TO SURVIVE HERE, and on this device it is not a hypothetical. `controls-
   hidden` is toggled ONLY by inputSource==='keyboard' (05-camera-ui.js), and an Android TV REMOTE
   sends real key events — so one stray D-pad press hides the controls AND stands down auto-throttle,
   leaving a stationary bus with nothing on screen to touch. The layer whose pointerdown sets the
   source back to 'touch' and restores both was display:none under the desktop rule. It takes
   pointers only while `.active`, so bringing it back costs nothing the rest of the time. */
html.tdt-force-touch .mobile-reveal-layer { display:block; }

html.tdt-force-touch body.microlet-mode .steer-buttons {
  left:auto; right:calc(50% + var(--k-mid) / 2); bottom:var(--k-bottom); gap:var(--k-gap);
}
html.tdt-force-touch body.microlet-mode .steer-btn { width:var(--k-steer-w); height:var(--k-h); }
/* The arrows are border triangles, not glyphs (--arcade-display is Impact, which has no < >), so
   they do not scale with the plate and have to be restated: a 15px chevron on a 170px plate reads
   as a blemish rather than a direction. */
html.tdt-force-touch body.microlet-mode .steer-btn i {
  border-top-width:34px; border-bottom-width:34px;
}
html.tdt-force-touch body.microlet-mode .steer-btn-l i { border-right-width:42px; margin-right:8px; }
html.tdt-force-touch body.microlet-mode .steer-btn-r i { border-left-width:42px; margin-left:8px; }

/* The lever is twice a steer plate because it is the one control you HOLD, and on a single-touch
   panel it is also the only way to stop for a fare. */
html.tdt-force-touch body.microlet-mode .hop-btn {
  right:auto; left:calc(50% + var(--k-mid) / 2); bottom:var(--k-bottom);
  width:var(--k-lever-w); height:var(--k-h); font-size:38px;
}
html.tdt-force-touch body.microlet-mode .horn-btn {
  right:auto; left:calc(50% + var(--k-mid) / 2 + var(--k-lever-w) + var(--k-gap));
  bottom:var(--k-bottom); width:var(--k-aux); height:var(--k-aux);
}
html.tdt-force-touch body.microlet-mode .boost-action {
  display:grid;
  left:auto; right:calc(50% + var(--k-mid) / 2 + var(--k-steer-group) + var(--k-gap));
  bottom:var(--k-bottom); width:var(--k-aux); height:var(--k-aux); font-size:40px;
}
