/* No Excuses — styles (build 18, refactor stage 4). One file, in sections that match the folders: a change to one screen
   or one game touches one section. Within a section the rules keep the order they had in build 13 — the later "v10 / v11 /
   v13" layers sit under the base rules they refine. The gate's cssdiff tool (_smoke/cssdiff.mjs) proved the computed styles
   identical to the single-block version across every screen and game state when this split was made. */

/* ==================================================================
   fonts (v18 B.32, build 33) — self-hosted, latin only
   ==================================================================
   Was three families pulled off fonts.googleapis.com on every visit: a stylesheet request, a DNS
   lookup and a TLS handshake to a second origin before a single glyph could be asked for. These are
   the same files Google serves, latin subset only, in `../fonts/`.

   Archivo and JetBrains Mono are VARIABLE fonts and one file carries every weight of each, so the
   two blocks per family point at the same URL — exactly as Google's own CSS does. Only Syncopate
   ships a static face. Three files, 82KB.

   No `unicode-range`: the app is English. `←`, `→` and `✓` are outside the latin subset and already
   fell back to the system font under Google Fonts, so nothing about them changes.

   `font-display: swap` — the app paints in the fallback stack and swaps when the file lands, which
   is what the fallbacks in `--title` / `--display` / `--mono` are for. The title's two (Syncopate
   700 for the wordmark, Archivo for the menu under it) are preloaded in index.html. */
  @font-face{font-family:"Syncopate";font-style:normal;font-weight:700;font-display:swap;src:url(../fonts/syncopate-700.woff2) format("woff2")}
  @font-face{font-family:"Archivo";font-style:normal;font-weight:500;font-display:swap;src:url(../fonts/archivo-var.woff2) format("woff2")}
  @font-face{font-family:"Archivo";font-style:normal;font-weight:800;font-display:swap;src:url(../fonts/archivo-var.woff2) format("woff2")}
  @font-face{font-family:"JetBrains Mono";font-style:normal;font-weight:500;font-display:swap;src:url(../fonts/jetbrains-mono-var.woff2) format("woff2")}
  @font-face{font-family:"JetBrains Mono";font-style:normal;font-weight:700;font-display:swap;src:url(../fonts/jetbrains-mono-var.woff2) format("woff2")}

/* ==================================================================
   shell — index.html, core/ — the root, type, buttons, chips, the screen frame, tables, the build bar
   ================================================================== */
  /* No Excuses — styles (build 18, refactor stage 4). Moved out of index.html as one block; the sections come next. */
  #build{position:fixed;left:0;right:0;bottom:max(6px,env(safe-area-inset-bottom));text-align:center;font:500 11px/1 var(--mono);color:var(--mute);pointer-events:none;z-index:50;opacity:.7}
  #update{display:none;position:fixed;left:16px;right:16px;bottom:max(28px,calc(env(safe-area-inset-bottom) + 24px));z-index:60;padding:14px;border-radius:12px;background:var(--ok);color:#000;font:800 14px var(--display);text-align:center;border:0}
  #update.on{display:block}
  :root{
    --ground:#050506; --ink:#E8E6E1; --mute:#6E6C68;
    --panel:color-mix(in srgb,var(--ground) 94%,#fff); --line:color-mix(in srgb,var(--ground) 86%,#fff); --sq:color-mix(in srgb,var(--ground) 90%,#fff);
    --sq-live:#FFFFFF; --cue:#C8322A; --miss:#B3261E; --ok:#3DD68C;
    --glow:rgba(232,230,225,.30);
    --title:"Syncopate","Michroma",var(--display); --display:"Archivo",system-ui,sans-serif; --mono:"JetBrains Mono",ui-monospace,Menlo,monospace;
  }
  *{box-sizing:border-box;margin:0;padding:0}
  /* v13 (3.1): a `.chips` row set display:flex, which outranks the browser's own [hidden] rule — that is why Solo still showed Pass & play / Versus */
  [hidden]{display:none!important}
  html,body{height:100%;background:var(--ground);color:var(--ink);font-family:var(--display);overflow:hidden;touch-action:manipulation;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}
  body{position:fixed;inset:0}
  .screen{position:absolute;inset:0;display:none;flex-direction:column;align-items:center;justify-content:center;gap:16px;padding:24px;padding-top:max(24px,env(safe-area-inset-top))}
  .screen.on{display:flex}
  .screen.top{justify-content:flex-start;gap:12px;padding-top:max(18px,env(safe-area-inset-top))}
  .wordmark{font-family:var(--title);font-weight:700;font-size:clamp(24px,6.6vw,44px);letter-spacing:.2em;text-transform:uppercase;text-align:center;padding-left:.2em;color:var(--ink);pointer-events:none;
    text-shadow:0 0 12px var(--glow);animation:breathe 14s ease-in-out infinite,float 13s ease-in-out infinite}
  @keyframes breathe{50%{text-shadow:0 0 22px var(--glow),0 0 44px rgba(232,230,225,.10)}}
  @keyframes float{0%,100%{translate:0 0;rotate:0deg}30%{translate:4px -5px;rotate:.4deg}65%{translate:-4px 3px;rotate:-.35deg}}
  .glow{animation:softglow 9s ease-in-out infinite}
  @keyframes softglow{50%{text-shadow:0 0 10px rgba(232,230,225,.22)}}
  .eyebrow{font-family:var(--mono);font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--mute)}
  button{font:inherit;color:var(--ink);background:none;border:0;cursor:pointer}
  button:focus-visible{outline:1px solid var(--ink);outline-offset:4px}
  /* build 33: Send feedback is the one <a> in the app — a mailto, so the link does its own work. It reads as an .item
     and nothing else; without this it would arrive underlined and browser-blue */
  a.item{color:var(--ink);text-decoration:none;cursor:pointer;display:inline-block}
  .item{font-weight:500;font-size:20px;letter-spacing:.14em;text-transform:uppercase;padding:11px 18px;opacity:.85}
  .item:hover,.item:focus-visible{opacity:1;outline:none;text-decoration:underline;text-underline-offset:8px}
  .item.sub{font-size:17px}
  .item.big{font-size:22px;border:1px solid var(--ink);padding:14px 40px;opacity:1}
  .explain{max-width:36ch;text-align:center;color:var(--mute);font-size:15px;line-height:1.55}
  .row{display:flex;gap:6px;flex-wrap:wrap;justify-content:center;align-items:center}
  .back{font-family:var(--mono);font-size:12px;letter-spacing:.18em;color:var(--mute);padding:10px 14px;margin-bottom:6px;align-self:flex-start}
  .hint{font-family:var(--mono);font-size:10px;letter-spacing:.18em;color:var(--mute);opacity:.6;text-transform:uppercase;margin-top:8px;text-align:center;line-height:1.6}
  .chips{display:flex;gap:6px;flex-wrap:wrap;justify-content:center}
  .chip{font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;padding:7px 11px;border:1px solid var(--line);color:var(--mute)}
  .chip.sel{color:var(--ink);border-color:var(--ink)}
  .chip.lbl{border:0;padding-left:0;padding-right:4px}
  .big{font-family:var(--mono);font-weight:700;font-size:clamp(56px,15vw,110px);line-height:1;font-variant-numeric:tabular-nums}
  .big.sm{font-size:clamp(40px,11vw,80px)}
  /* lists */
  table{border-collapse:collapse;font-family:var(--mono);font-size:12px;font-variant-numeric:tabular-nums;width:min(92vw,440px)}
  th{font-weight:500;color:var(--mute);letter-spacing:.16em;text-transform:uppercase;font-size:10px;text-align:left;padding:6px 8px;border-bottom:1px solid var(--line);white-space:nowrap}
  td{padding:9px 8px;border-bottom:1px solid var(--line);color:var(--mute);white-space:nowrap}
  td:nth-child(3){color:var(--ink);font-weight:700}
  td:nth-child(2){color:var(--ink);letter-spacing:.06em}
  tr.best td{color:var(--sq-live)}
  tr.cur td{background:color-mix(in srgb,var(--ink) 6%,transparent)}
  /* v14 (8.2): overflow-y:auto makes the other axis `auto` too, so a stray sideways drag panned a scroller and it stayed
     panned. `contain` stops it chaining out of the box; the achievements list also loses the axis outright, below */
  .scroll{max-height:60vh;overflow-y:auto;width:min(92vw,440px);overscroll-behavior:contain}
  /* ---------- v11 ---------- */
  :root{--p1:#E0453B;--p2:#6EC6FF}
  .p1{color:var(--p1)}
  .p2{color:var(--p2)}
  @keyframes brighten{0%{opacity:.35}100%{opacity:.85}}
  .chip.x::after{left:6px;right:6px}
  /* everything that appears on a tap rises in (v11): ~12px and a fade over 180ms */
  @keyframes rise{0%{opacity:0;translate:0 12px}100%{opacity:1;translate:0 0}}
  .big .dn{font-size:.32em;vertical-align:.5em;color:var(--mute);margin-left:.1em}
  /* v15 (3.8 answer, build 25): a Streak's number is the round it reached, and the result screen says so under it */
  .big .unit{display:block;font-family:var(--mono);font-size:.14em;font-weight:400;letter-spacing:.24em;text-transform:uppercase;color:var(--mute);margin-top:.42em}
  /* ---------- v13 ---------- */
  /* 2.1 / L8: anything newly unlocked swells and tints green the first time it is on screen, then is marked seen. One animation, every screen */
  /* v14 (8.7): the 100% frame must NOT name background-color. `both` holds the last frame forever, so ending on `transparent`
     left every first-seen Customise swatch permanently blank — which is exactly "target colours are blank on first load".
     Leaving it out ends the animation on the element's own background instead: the green fades off, the colour comes back. */
  @keyframes newthing{0%{scale:1;background-color:color-mix(in srgb,var(--ok) 34%,transparent)}30%{scale:1.06;background-color:color-mix(in srgb,var(--ok) 34%,transparent)}100%{scale:1}}
  .newthing{animation:newthing .6s ease-out both!important;border-color:var(--ok)!important}
  /* 7.2: a locked chip on the pick sheet reads like every other locked thing.
     v15 (2.1): pointer-events:none is GONE. It is what made "tapping a locked mode jumps to game select" — a dead chip let
     the tap fall through to the screen, and ui/actions.js reads a tap on no control as Back, which from the result screen
     is s-pick. A locked chip is tappable and says what it takes, like every other locked thing on every other screen. */
  .chip.locked{opacity:.5}

/* ==================================================================
   atmosphere — ui/atmosphere.js
   ================================================================== */
  #stars{position:absolute;inset:0;width:100%;height:100%;display:block;transition:opacity .3s}

/* ==================================================================
   title — the sequence that opens the menu (ui/screens/menu.js)
   ================================================================== */
  /* v14 (1.2): there is one NO EXCUSES, it lives on the menu screen, and the sequence plays around it — so it never moves,
     never re-renders and never re-animates when the menu arrives. `.story` hides the menu's own rows WITHOUT taking their
     space, so the layout the title sits in is already final; `.run` drives the three beats; `.storyend` fades the lines out. */
  .titlewrap{position:relative;display:flex;flex-direction:column;align-items:center}
  #s-menu .stline{position:absolute;left:50%;transform:translateX(-50%);width:92vw;text-align:center;font-family:var(--title);font-size:clamp(12px,3.8vw,20px);letter-spacing:.2em;text-transform:uppercase;color:var(--ink);opacity:0;line-height:1.6;pointer-events:none;display:none}
  #st1{bottom:calc(100% + 26px)}
  #st2{top:calc(100% + 26px)}
  /* v15 (6.1): "tap to begin" was missed. It was a 10px .hint at 60% opacity pinned to the bottom edge, so it read as
     furniture rather than the one instruction on the screen. It is display type now, centred in its own full-width band
     and lifted clear of the home indicator; the animation still ends on .9 like the two story lines it arrives after */
  /* v17 (B.19, build 29): it sits HIGHER — a third of the way up rather than on the bottom edge, so it reads as part of
     the title block instead of as furniture under it — and it fades in at 1.5x the old pace (.8s -> 1.2s). L1 is quoted:
     placement and pace only. Nothing is removed, nothing is shortened, and it still arrives last of the four beats. */
  #storyhint{position:absolute;left:0;right:0;bottom:max(30vh,calc(env(safe-area-inset-bottom) + 44px));opacity:0;display:none;margin:0;
    font-family:var(--title);font-size:clamp(13px,4.4vw,19px);letter-spacing:.3em;color:var(--ink);line-height:1.6;text-align:center}
  #s-menu.story .stline,#s-menu.story #storyhint{display:block}
  #s-menu.story .nextup,#s-menu.story .menu-tag,#s-menu.story .item,#s-menu.story .menu-key{visibility:hidden}
  /* 1.1 — three beats: the first line at the top, NO EXCUSES in the middle, the second line under it. The title's own
     arrival is on .wmin, never on .wordmark, so breathe and float are never restarted underneath it */
  #s-menu.story .wmin{opacity:0}
  #s-menu.story.run #st1{animation:stline .8s ease-out .3s both}
  #s-menu.story.run .wmin{animation:titlein 1.1s ease-out 1.9s both}
  #s-menu.story.run #st2{animation:stline .8s ease-out 3.3s both}
  #s-menu.story.run #storyhint{animation:stline 1.2s ease-out 4.6s both}
  @keyframes stline{0%{opacity:0;translate:0 6px}100%{opacity:.9;translate:0 0}}
  #s-menu.storyend .stline,#s-menu.storyend #storyhint{animation:stout .3s ease-out both!important}
  @keyframes stout{0%{opacity:.9}100%{opacity:0}}

/* ==================================================================
   menu — ui/screens/menu.js
   ================================================================== */
  /* v14 (1.3): the first menu a profile ever sees reveals its items one at a time, 140ms apart; every open after that is
     instant. The title is NOT in this — it is already on screen and animating it again would be the jump 1.2 asks us to stop */
  @keyframes titlein{0%{opacity:0;scale:.98}100%{opacity:1;scale:1}}
  #s-menu.intro .item{animation:menufade .7s ease-out both}
  #s-menu.intro .item:nth-of-type(1){animation-delay:.9s}
  #s-menu.intro .item:nth-of-type(2){animation-delay:1.04s}
  #s-menu.intro .item:nth-of-type(3){animation-delay:1.18s}
  #s-menu.intro .item:nth-of-type(4){animation-delay:1.32s}
  #s-menu.intro .item:nth-of-type(5){animation-delay:1.46s}
  /* v15 (2.3 / 2.4): the list runs past five now — Unlocks joined it — and an item with no delay of its own arrived FIRST,
     out of order, which was the other half of what read as a glitch. The last rule catches anything added later */
  #s-menu.intro .item:nth-of-type(6){animation-delay:1.6s}
  #s-menu.intro .item:nth-of-type(7){animation-delay:1.74s}
  #s-menu.intro .item:nth-of-type(8){animation-delay:1.88s}
  #s-menu.intro .item:nth-of-type(n+9){animation-delay:2.02s}
  /* v15 (2.3): the 100% frame is gone, and that is the whole fix for the double-grey. `both` holds the last keyframe
     forever, so ending on a literal .85 overrode .item.dim's own .35 for as long as #s-menu.intro was up — every item
     faded in at full strength and then snapped greyer when the class came off 2.4s later. It read as a rendering fault
     because it was one. With no 100% frame the animation ends on the element's own opacity: a dimmed item arrives grey
     and stays grey, a live one arrives at .85. Same trick as the v14 8.7 newthing fix above. */
  @keyframes menufade{0%{opacity:0}}
  @keyframes menuin{0%{opacity:0;translate:0 -16px}100%{opacity:.85;translate:0 0}}
  /* first open: only Play is live until one run is on the record */
  .item.dim{opacity:.18;pointer-events:none;text-decoration:none}
  /* v17 (B.20, build 29): .menu-note is gone with the line it carried — the struck-through items say it already */
  /* v17 (A.6.7): percentage complete on the front of the app. Same mono line as .menu-note, one step brighter, because it
     is a figure that moves rather than a standing instruction — and it is a control: tapping it opens the keys screen */
  .menu-key{cursor:pointer;font-family:var(--mono);font-size:11px;letter-spacing:.18em;color:var(--ink);text-align:center;margin-top:2px}
  #s-menu.intro .menu-key{animation:menufade .7s ease-out 1.5s both}
  .menu-tag{font-family:var(--title);font-size:11px;letter-spacing:.24em;text-transform:uppercase;color:var(--mute);margin-top:-6px}
  #s-menu.intro .menu-tag{animation:menufade .7s ease-out .8s both}
  /* crossed out (v11): a line drawn across a locked item, wiping off when it opens */
  .x,.item.dim{position:relative}
  .x::after,.item.dim::after,.item.unx::after{content:"";position:absolute;left:-4px;right:-4px;top:50%;height:2px;background:var(--cue);transform-origin:left center;pointer-events:none}
  .item.dim{opacity:.35;pointer-events:none;text-decoration:none}
  .item.dim::after{left:14px;right:14px}
  /* v15 (6.2): the strikes come off top to bottom. --ud is set per item in ui/screens/menu.js and has to reach the
     ::after as well, which is why it is a variable and not an animation-delay on the element */
  .item.unx{opacity:.85;animation:brighten .5s ease-out var(--ud,0s) both}
  .item.unx::after{left:14px;right:14px;animation:unstrike .5s ease-out var(--ud,0s) both}
  @keyframes unstrike{0%{scale:1 1;opacity:1}100%{scale:0 1;opacity:.6}}
  /* next up (v11): one line under Play, from the same data as the in-run goal line */
  .nextup{cursor:pointer;font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute);border:1px solid var(--line);padding:8px 12px;max-width:92vw;line-height:1.5;text-align:center;margin-top:-6px}
  .nextup span{color:var(--ink);display:block}
  .nextup em{display:block;font-style:normal;font-family:var(--title);font-size:9px;letter-spacing:.26em;color:var(--mute);margin-bottom:6px}
  #s-menu.intro .nextup{animation:menufade .7s ease-out 1.1s both}

/* ==================================================================
   pick — ui/screens/pick.js
   ================================================================== */
  /* game picker */
  /* eight games (v7): three across so the whole set fits one phone screen */
  /* v17 (B.23, build 29): the gap opened from 14/10 to 24/16 to leave the unlock-order lines somewhere to be. The
     tiles lose a few pixels of width for it, which is the trade — the path is the thing being added. */
  .grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:24px 16px;width:min(94vw,520px)}
  @media (min-width:700px){.grid{grid-template-columns:repeat(4,1fr)}}
  .tile{display:flex;flex-direction:column;gap:6px;text-align:left;transition:opacity .35s}
  .tile .name{font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink);transition:color .35s;white-space:nowrap}
  .mini i.c{border-radius:50%}
  .mini i.t{clip-path:polygon(50% 0,100% 100%,0 100%);border-radius:0}
  .mini i.h{flex:none;height:22%;align-self:center;width:100%}
  .mini.tm{align-items:center}
  .mini.tm i.w{flex:none;width:36%;height:36%;border-radius:50%}
  .mini.tm i.h{flex:1;height:14%}
  .mini.rx i{background:var(--sq-live);opacity:.9}
  .mini.ct{flex-wrap:wrap;gap:12%}
  .mini.ct i{flex:none;width:28%;height:28%}
  .mini.fd{flex-wrap:wrap;gap:10%}
  .mini.fd i{flex:none;width:26%;height:26%}
  .tile .pic{aspect-ratio:1;border:1px solid var(--line);background:var(--panel);display:grid;place-items:center;position:relative;overflow:hidden;transition:opacity .35s}
  .tile.soon .name,.grid.dim .tile:not(.keep) .name{color:var(--mute);animation:none;text-shadow:none}
  .tile.soon .pic,.grid.dim .tile:not(.keep) .pic{opacity:.45}
  .tile.soon .pic::after{content:"soon";position:absolute;bottom:8px;right:10px;font-family:var(--mono);font-size:9px;letter-spacing:.2em;color:var(--mute);text-transform:uppercase}
  /* locked game: greyed, the condition written on the tile so the goal is always in view (v6) */
  .tile.locked .name{color:var(--mute)}
  .tile.locked .pic{opacity:.4}
  .tile.locked .pic::after{content:attr(data-need);position:absolute;inset:auto 5px 5px 5px;font-family:var(--mono);font-size:8px;letter-spacing:.08em;color:var(--ink);text-transform:uppercase;line-height:1.4;text-align:left}
  .tile.locked .mini{opacity:.35}
  .choice.locked{opacity:.45}
  .choice.locked b::after{content:" · locked";color:var(--cue);letter-spacing:.1em}
  .grid.dim .tile:not(.keep){pointer-events:none}
  .mini{display:flex;gap:8%;width:56%;height:56%}
  .mini i{flex:1;background:var(--sq);border-radius:1px;position:relative}
  .mini i.w{background:var(--sq-live)}
  .mini i.r::after{content:"";position:absolute;left:0;right:0;top:-14%;height:16%;background:var(--cue);border-radius:2px}
  .mini.dots i{background:none;display:grid;place-items:center}
  .mini.dots i::before{content:"";width:46%;aspect-ratio:1;border-radius:50%;background:var(--sq)}
  .mini.dots i.w::before{background:var(--sq-live)}
  .mini.dots i.r::after{content:none}
  .mini.dots i.r::before{background:color-mix(in srgb,var(--cue) 25%,transparent);border:2px solid var(--cue)}
  /* Estimate (v9): Grow shows a dashed target with a filled shape inside it; Cut shows a shape with a dotted line through it and a result */
  .mini.hold{align-items:flex-end}
  .mini.hold i{height:100%;background:none}
  .mini.hold i::before{content:"";position:absolute;left:0;right:0;bottom:0;height:64%;background:none;border:2px dashed var(--cue)}
  .mini.hold i.w::before{height:52%;background:var(--sq-live);border:0}
  .mini.hold i.e::before{border-radius:50%;height:70%}
  .mini.cut{display:grid;place-items:center;position:relative;width:70%;height:70%}
  .mini.cut i{position:absolute;inset:4% 18% 30% 18%;background:var(--sq-live);clip-path:polygon(0 30%,55% 0,100% 45%,80% 100%,20% 90%)}
  .mini.cut i.ln{inset:auto;left:-6%;right:-6%;top:38%;height:0;border-top:2px dashed var(--cue);background:none;clip-path:none;rotate:-14deg}
  .mini.cut b{position:absolute;left:-20%;right:-20%;bottom:-6%;text-align:center;font:700 8px var(--mono);color:var(--ok);letter-spacing:.06em;white-space:nowrap}
  .mini.four{flex-wrap:wrap;gap:8%}
  .mini.four i{flex:none;width:46%;height:46%}
  .mini.tmw{flex-direction:column;align-items:center;justify-content:center;gap:6%;width:80%}
  .mini.tmw i{flex:none;background:none;width:100%;text-align:center;font:700 13px var(--mono);color:var(--sq-live);letter-spacing:0;height:auto}
  .mini.tmw i.u{font-size:10px;color:var(--mute);font-weight:500}
  .mini.tmw i.u b{color:var(--ok);font-weight:500}
  .mini.tmh{position:relative;width:76%;height:56%;display:block}
  .mini.tmh i{position:absolute;background:none}
  .mini.tmh i.b{left:0;top:32%;width:22%;height:36%;border-radius:50%;background:var(--sq-live)}
  .mini.tmh i.wall{left:42%;right:0;top:0;bottom:0;background:var(--panel);border-left:1px solid var(--line)}
  .mini.tmh i.m{left:64%;top:32%;width:22%;height:36%;border-radius:50%;border:2px dashed var(--cue)}
  /* v14 (2.2): Reaction reads as a press — a smaller white square with lines coming out of it, not a filled block */
  .mini.rx{background:none;width:100%;height:100%;display:grid;place-items:center;position:relative}
  .mini.rx::before{content:"";width:34%;height:34%;background:var(--sq-live)}
  .mini.rx::after{content:"";position:absolute;inset:14%;opacity:.85;
    background:linear-gradient(to right,var(--sq-live) 0 13%,transparent 13% 87%,var(--sq-live) 87% 100%) center/100% 7% no-repeat,
               linear-gradient(to bottom,var(--sq-live) 0 13%,transparent 13% 87%,var(--sq-live) 87% 100%) center/7% 100% no-repeat}
  .mini.rx i{display:none}
  .mini.nogo{gap:14%;align-items:center;justify-content:center}
  .mini.nogo i{flex:none;width:34%;height:34%;background:var(--sq-live)}
  .mini.nogo i.x::after{content:"";position:absolute;inset:-30%;background:linear-gradient(45deg,transparent 44%,var(--miss) 44%,var(--miss) 56%,transparent 56%),linear-gradient(-45deg,transparent 44%,var(--miss) 44%,var(--miss) 56%,transparent 56%)}
  .mini.seq i{background:var(--sq)}
  .mini.seq i:nth-child(1){background:var(--sq-live)}
  .mini.seq i:nth-child(2){background:#8a8883}
  .mini.seq i:nth-child(3){background:#45443f}
  /* bottom sheet: mode row stays, dims to the unpicked one, and the length row builds on it */
  .sheet{position:absolute;left:0;right:0;bottom:0;background:var(--panel);border-top:1px solid var(--line);padding:18px 22px max(28px,env(safe-area-inset-bottom));transform:translateY(100%);transition:transform .32s cubic-bezier(.2,.8,.2,1);display:flex;flex-direction:column;gap:12px;align-items:center}
  .sheet.up{transform:none}
  .diffs{display:flex;flex-direction:column;gap:10px;width:min(88vw,400px);transition:gap .18s}
  .diffs.single{display:none}
  .choice{display:flex;flex-direction:row;align-items:center;gap:16px;padding:10px 12px;border:1px solid var(--line);text-align:left;width:100%;transition:opacity .18s,padding .18s}
  .choice .pic{width:92px;height:92px;flex:none;border:1px solid var(--line);background:var(--ground);display:grid;place-items:center;transition:width .18s,height .18s}
  .choice .txt{display:flex;flex-direction:column;gap:6px;min-width:0}
  .choice b{font-family:var(--mono);font-weight:700;font-size:13px;letter-spacing:.22em;text-transform:uppercase}
  .choice small{color:var(--mute);font-size:14px;line-height:1.45;max-height:60px;overflow:hidden;transition:max-height .18s,opacity .18s}
  .choice:hover,.choice:focus-visible,.choice.sel{border-color:var(--ink);outline:none}
  .sheet.len .diffs{gap:6px}
  .sheet.len .choice:not(.sel){opacity:.3}
  .sheet.len .choice{padding:6px 12px}
  .sheet.len .choice .pic{width:56px;height:56px}
  .sheet.len .choice small{max-height:0;opacity:0}
  #lenwrap{display:flex;flex-direction:column;gap:12px;align-items:center;max-height:0;opacity:0;overflow:hidden;transition:max-height .18s cubic-bezier(.2,.8,.2,1),opacity .18s;width:100%}
  .sheet.len #lenwrap{max-height:340px;opacity:1}
  .times{display:flex;gap:8px;justify-content:center;width:min(88vw,400px)}
  .tbtn{display:flex;flex-direction:column;align-items:center;gap:4px;padding:10px 10px;border:1px solid var(--line);min-width:76px;flex:1;max-width:150px}
  .tbtn b{font-weight:500;font-size:16px;letter-spacing:.08em;white-space:nowrap}
  .tbtn b small{display:block;font-size:12px;color:var(--mute);letter-spacing:.06em;text-transform:none}
  .tbtn .lsub{font-family:var(--mono);font-size:9.5px;letter-spacing:.08em;color:var(--mute);text-transform:none;line-height:1.35;text-align:center;max-width:15ch}
  .tbtn small{font-family:var(--mono);font-size:10px;letter-spacing:.14em;color:var(--mute);text-transform:uppercase}
  .tbtn:hover,.tbtn:focus-visible,.tbtn.sel{border-color:var(--ink);outline:none}
  /* the pick sheet: mode is one level, length is the next — the mode row leaves when the length row arrives (v6) */
  /* the picked mode keeps its picture while the length is chosen (v8); the other mode leaves */
  .sheet.len .diffs{display:flex}
  .sheet.len .choice:not(.sel){display:none}
  .sheet.len .choice.sel{opacity:1}
  .sheet .lvl{font-family:var(--mono);font-size:11px;letter-spacing:.2em;color:var(--mute);text-transform:uppercase;padding:6px 10px;display:none}
  .sheet.len .lvl{display:none}
  /* pick sheet (v10): the length buttons only select; a Go button under them starts. The options slide up together for every game */
  #go-btn{margin-top:2px}
  .sheet.len #lenwrap{max-height:420px}
  /* two-player pictures on the pick sheet (v10): you cannot mistake pass & play or versus for solo */
  #vs-row .chip{display:flex;align-items:center;gap:7px}
  #vs-row .chip svg{width:22px;height:16px;overflow:visible}
  #vs-row .chip svg *{stroke:currentColor;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
  #vsart{display:none;flex-direction:column;align-items:center;gap:6px;font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ok);text-align:center;line-height:1.6}
  #vsart.on{display:flex}
  #vsart svg{width:120px;height:56px;overflow:visible}
  #vsart svg *{stroke:var(--ok);fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
  #vsart small{color:var(--mute);letter-spacing:.12em;text-transform:none;font-size:12px}
  .sheet.two{border-top:2px solid var(--ok);background:color-mix(in srgb,var(--ok) 5%,var(--panel))}
  .choice.locked{opacity:.5}
  .choice.locked b::after{content:""}
  .tbtn.locked{opacity:.5}
  .tbtn b.x::after{top:45%}
  /* ---------- v17 (B.22 / B.23 / B.24, build 29): the pressed outline, the unlock-order lines, the chest ---------- */
  /* B.22 — the game you pressed wears an outline. Not green: green is L8's first-seen mark and would say "new". Not
     white: white is L7's unplayed tile and would say "never played". Amber, named `PRESS` in config/theme.js and
     published as --press by ui/theme.js, so no colour is chosen in this file. */
  .tile.keep .pic{outline:2px solid var(--press);outline-offset:-2px}
  .tile.keep .name{color:var(--press)}
  /* B.23 — the lines. #gridlines sits over the grid and takes no cell of its own, so the seven tiles keep their layout
     and the path is drawn on top of the gaps between them. A segment is green when the game it points AT is open. */
  .grid{position:relative}
  .gridlines{position:absolute;left:0;top:0;pointer-events:none;overflow:visible;z-index:1}
  .gridlines .gl{fill:none;stroke:var(--mute);stroke-width:1.5;opacity:.55;stroke-linecap:round;
    stroke-dasharray:var(--len);stroke-dashoffset:var(--len);animation:gline .5s ease-out var(--gd,0s) both}
  .gridlines .gl.open{stroke:var(--ok);opacity:.8}
  @keyframes gline{to{stroke-dashoffset:0}}
  .grid.dim .gridlines{opacity:.25;transition:opacity .35s}
  /* B.24 — the chest. Locked it is grey and says what key 1 asks for; openable it breathes; opened the lid is up and it
     says what it gave. The art is three shapes in one svg so the lid can hinge on its own left edge. */
  .tile.chest .pic{display:grid;place-items:center}
  .chestart{width:62%;height:62%;overflow:visible}
  .chestart .box{fill:var(--panel);stroke:var(--ink);stroke-width:1.4}
  .chestart .lid{fill:var(--panel);stroke:var(--ink);stroke-width:1.4;transform-origin:5px 14px;transition:rotate .5s ease-out}
  .chestart .lock{fill:var(--cue)}
  .tile.chest.locked .name{color:var(--mute)}
  .tile.chest.locked .chestart{opacity:.4}
  .tile.chest.locked .pic::after{content:attr(data-need);position:absolute;inset:auto 5px 5px 5px;font-family:var(--mono);font-size:8px;letter-spacing:.08em;color:var(--ink);text-transform:uppercase;line-height:1.4;text-align:left}
  .tile.chest.ready .chestart .lock{fill:var(--ok)}
  .tile.chest.ready .pic{border-color:var(--ok);animation:chestready 2.6s ease-in-out infinite}
  .tile.chest.ready .pic::after{content:attr(data-need);position:absolute;inset:auto 5px 5px 5px;font-family:var(--mono);font-size:8px;letter-spacing:.16em;color:var(--ok);text-transform:uppercase;text-align:left}
  @keyframes chestready{50%{box-shadow:0 0 16px color-mix(in srgb,var(--ok) 40%,transparent)}}
  .tile.chest.open .chestart .lid{rotate:-118deg}
  .tile.chest.open .chestart .lock{fill:var(--ok)}
  .tile.chest.open .pic::after{content:attr(data-need);position:absolute;inset:auto 5px 5px 5px;font-family:var(--mono);font-size:8px;letter-spacing:.08em;color:var(--mute);text-transform:uppercase;text-align:left;line-height:1.4}
  /* the opening itself: the lid swings, the box glows once. .opening comes off after 900ms and the lid stays up */
  .tile.chest.opening .pic{animation:chestpop .9s ease-out}
  .tile.chest.opening .chestart .lid{animation:chestlid .9s cubic-bezier(.2,1.4,.4,1) both}
  @keyframes chestlid{0%{rotate:0deg}55%{rotate:-132deg}100%{rotate:-118deg}}
  @keyframes chestpop{0%{box-shadow:0 0 0 color-mix(in srgb,var(--ok) 0%,transparent)}40%{box-shadow:0 0 34px color-mix(in srgb,var(--ok) 70%,transparent)}100%{box-shadow:0 0 0 transparent}}
  /* ---------- v18 (B.18, build 32): the outline fills with key-1 progress ----------
     An svg rect over the picture, pathLength 1, dashed to the fraction progress/key.js hands over as --kf. Lilac, named
     KEYFILL in config/theme.js and published as --keyfill — not green (L8), not white (L7), not the pressed amber. A
     complete game closes the outline and washes the picture in the same colour, so it reads as done, not merely full. */
  .tile .kfill{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:1;overflow:visible}
  .tile .kfill rect{fill:none;stroke:var(--keyfill);stroke-width:2.4;vector-effect:non-scaling-stroke;stroke-dasharray:var(--kf,0) 1;stroke-dashoffset:0;opacity:.9;transition:stroke-dasharray .6s ease}
  .tile.locked .kfill,.tile.unplayed .kfill{display:none}
  .tile.kdone .pic{box-shadow:inset 0 0 0 100vmax color-mix(in srgb,var(--keyfill) 16%,transparent)}
  .tile.kdone .kfill rect{stroke-width:3;opacity:1;filter:drop-shadow(0 0 4px color-mix(in srgb,var(--keyfill) 70%,transparent))}
  .tile.kdone .name{color:var(--keyfill)}
  /* B.19 / B.20: three chests. A ready chest wears its key above the lock; the opening (build 29) is EXTENDED — the key
     drops into the lock and the lid swings, 1.6s where it was .9s, then the box flares. Chests 2 and 3 are hidden tiles
     until chest 1 is open and take no cell of the grid while hidden. */
  .tile.chest[hidden]{display:none}
  .chestart .ckey{fill:none;stroke:var(--ok);stroke-width:1.4;stroke-linecap:round;opacity:0;transform-origin:20px 4px}
  .tile.chest.ready .chestart .ckey{opacity:1;animation:chestkey 2.6s ease-in-out infinite}
  @keyframes chestkey{50%{translate:0 -2px;filter:drop-shadow(0 0 4px var(--ok))}}
  .tile.chest.opening .chestart .ckey{opacity:1;animation:chestkeyin 1.6s cubic-bezier(.4,0,.2,1) both}
  @keyframes chestkeyin{0%{translate:0 -6px;opacity:1}45%{translate:0 10px;opacity:1;rotate:0deg}60%{translate:0 10px;rotate:90deg}100%{translate:0 10px;rotate:90deg;opacity:0}}
  .tile.chest.opening .pic{animation:chestpop 1.6s ease-out}
  .tile.chest.opening .chestart .lid{animation:chestlid 1.6s cubic-bezier(.2,1.4,.4,1) both}
  @keyframes chestlid{0%{rotate:0deg}55%{rotate:0deg}80%{rotate:-132deg}100%{rotate:-118deg}}
  @keyframes chestpop{0%{box-shadow:0 0 0 color-mix(in srgb,var(--ok) 0%,transparent)}70%{box-shadow:0 0 34px color-mix(in srgb,var(--ok) 70%,transparent)}100%{box-shadow:0 0 0 transparent}}
  /* the grid can be five rows once the chests exist, so the screen scrolls */
  #s-pick{overflow-y:auto;justify-content:flex-start;padding-top:max(24px,env(safe-area-inset-top))}
  /* B.20 / B.16: the ask box, the lock box's shape */
  #askwrap{position:absolute;inset:0;display:none;place-items:center;background:color-mix(in srgb,var(--ground) 84%,transparent);z-index:8;padding:24px}
  #askwrap.on{display:grid}
  #askbox{display:flex;flex-direction:column;gap:14px;align-items:center;background:var(--panel);border:1px solid var(--ok);padding:22px 20px;max-width:34ch;text-align:center;animation:rise .18s ease-out both}
  #askbox h3{font-family:var(--title);font-size:14px;letter-spacing:.2em;text-transform:uppercase;color:var(--ok)}
  #askbox p{font-size:14px;line-height:1.55;color:var(--ink)}
  #askbox .row{gap:10px}
  /* the grid reveal (v11): tiles arrive one by one on the first visit; locked tiles wear a padlock that wipes off when the game opens */
  .tile.reveal{animation:tilein .5s ease-out both}
  /* v14 (2.1): unseen to being seen — each tile pulses up from nothing rather than sliding in */
  @keyframes tilein{0%{opacity:0;scale:.15}65%{opacity:1;scale:1.08}100%{opacity:1;scale:1}}
  .tile .pic::before{content:"";position:absolute;top:6px;right:6px;width:14px;height:16px;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 16'%3E%3Crect x='1' y='7' width='12' height='8' rx='1.5' fill='%23E8E6E1'/%3E%3Cpath d='M4 7V4.5a3 3 0 0 1 6 0V7' fill='none' stroke='%23E8E6E1' stroke-width='1.8'/%3E%3C/svg%3E") center/contain no-repeat;opacity:0;transition:opacity .3s;z-index:2}
  .tile.locked .pic::before,.tile.unlocking .pic::before{opacity:.9}
  .tile.unlocking .pic::before{animation:padoff .5s ease-in both}
  @keyframes padoff{0%{opacity:.9;scale:1}100%{opacity:0;scale:.2}}
  .tile.unlocking .pic{animation:brightpic .5s ease-out both}
  @keyframes brightpic{0%{opacity:.4}100%{opacity:1}}
  .tile.newthing{border-color:transparent!important}
  .tile.newthing .pic{border-color:var(--ok)}
  /* v15 (6.3): a newly unlocked game ARRIVES on the grid the first time it is seen. Deliberately not `tilein`, which is
     the first-visit reveal (v11) and belongs to the whole grid: this one drops in and settles, on one tile, once */
  @keyframes tilearrive{0%{opacity:0;translate:0 -22px;scale:.9;rotate:-2deg}55%{opacity:1;translate:0 4px;scale:1.04;rotate:.5deg}100%{opacity:1;translate:0 0;scale:1;rotate:0deg}}
  /* both marks, one after the other, never on top of each other — .newthing's own rule carries !important and would
     otherwise simply replace this. It arrives, THEN it is marked green; the green border on .pic stays after both */
  .tile.arrive{animation:tilearrive .72s cubic-bezier(.2,.85,.3,1.1) .1s both!important}
  .tile.arrive.newthing{animation:tilearrive .72s cubic-bezier(.2,.85,.3,1.1) .1s both,newthing .6s ease-out .9s forwards!important}
  /* v14 (3.1): the requirement under a locked mode is the way in, so it reads in the same green everything unlockable wears */
  .choice small.need{color:var(--ok)}
  /* v14 (3.4): "best" / "closest" is green on every best-of row, on the sheet and on the result screen */
  .tbtn small i.bw,.stats i.bw{font-style:normal;color:var(--ok)}
  /* v14 (4.6): picking a mode turns it green and darkens the other for a beat, then the length row and Go push up */
  .choice.picked{border-color:var(--ok)!important;background:color-mix(in srgb,var(--ok) 12%,transparent)}
  .diffs.picking .choice:not(.picked){opacity:.25}
  .diffs .choice{transition:opacity .18s,padding .18s,background .18s,border-color .18s}
  .sheet.len #go-btn{animation:goup .3s cubic-bezier(.2,.8,.2,1) .06s both}
  @keyframes goup{0%{opacity:0;translate:0 28px}100%{opacity:1;translate:0 0}}
  /* v14 (4.2 / 4.3 / 4.5): no caption, no grey sub-line — the phones carry the player labels, in the player colours */
  #vsart svg{width:120px;height:74px}
  #vsart .vsp{display:flex;justify-content:center;font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--mute)}
  #vsart .vsp.two{width:180px}
  #vsart .vsp>span{white-space:nowrap}
  #vsart .vsp.two>span{flex:1;text-align:center}
  #vsart .vsp .p1,#vsart .vsp .p2{font-weight:700}
  #chal{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--ok);border:1px solid var(--ok);padding:8px 12px;text-align:center;line-height:1.5}
  #chal b{color:var(--ink);font-size:15px;letter-spacing:0}

/* ==================================================================
   board — ui/screens/board.js
   ================================================================== */
  /* profile (v9): the name and a radar of your best across the games sit on top of the scores */
  .prof{display:flex;flex-direction:column;align-items:center;gap:6px;flex:none}
  #radar{width:min(58vw,230px);height:min(58vw,230px);overflow:visible}
  #radar polygon.web{fill:none;stroke:var(--line)}
  #radar line{stroke:var(--line)}
  #radar polygon.me{fill:color-mix(in srgb,var(--sq-live) 22%,transparent);stroke:var(--sq-live);stroke-width:2;stroke-linejoin:round}
  #radar text{fill:var(--mute);font-family:var(--mono);font-size:8px;letter-spacing:.12em;text-transform:uppercase}
  #radar circle{fill:var(--sq-live)}
  /* v14 (8.6): past the Author's number is past the outer ring, and it is marked — the shape and the axis that did it go green */
  #radar polygon.me.past{stroke:var(--ok)}
  #radar circle.past{fill:var(--ok)}
  #radar text.past{fill:var(--ok)}
  /* v18 (B.24, build 32): after chest 1 the web is the three rungs — key 1, Pro, Author — and a shell tier's rung is
     dashed at no value (A.2). A vertex past the Author rung wears a little flame. */
  #radar polygon.rung{stroke:var(--line)}
  #radar polygon.rung[data-rung="author"]{stroke:var(--ink);opacity:.7}
  #radar polygon.rung.shell{stroke-dasharray:3 4;opacity:.6}
  #radar .flame path{fill:var(--ok);stroke:none;transform-origin:0 2px;animation:flame .9s ease-in-out infinite alternate}
  @keyframes flame{0%{scale:1 1;opacity:.85}100%{scale:1.15 1.3;opacity:1}}
  .who{display:flex;align-items:baseline;gap:8px;font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--mute)}
  .who input{font:inherit;color:var(--ink);background:none;border:0;border-bottom:1px solid var(--line);width:11ch;text-transform:uppercase;letter-spacing:.18em;padding:4px 0;outline:none;text-align:center}
  .who input:focus{border-color:var(--ink)}
  /* supporter (v10): a star by the name, a tier line on the About screen */
  .who .star{color:#FFB020;font-size:14px}

/* ==================================================================
   progress — ui/screens/progress.js (build 29, v17 B.21; was unlocks.js + achievements.js)
   ================================================================== */
  /* v17 (B.21): two tabs, one screen. The tab row is the app's own chip row so it needs no furniture of its own, and a
     panel is hidden with the `hidden` attribute rather than a class — [hidden] is display:none!important at the top of
     this file, which a .chips/.scroll display rule would otherwise outrank (the v13 3.1 trap). */
  .ptabs{margin-bottom:2px}
  .ptab{display:flex;flex-direction:column;align-items:center;gap:10px;width:100%;min-height:0}
  #p-ach{gap:12px}

  /* the same row shape as the achievements list, on purpose — they are two halves of one idea and used to be one screen.
     Same no-sideways-axis rule as v14 8.2: an accidental left-swipe must not leave the rows off-centre */
  .unl{display:flex;flex-direction:column;width:min(92vw,440px);font-family:var(--mono);font-size:12px;overflow-x:hidden}
  .unl h4{font-weight:500;font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--ink);padding:18px 4px 6px}
  .unl button.urow{display:grid;grid-template-columns:1fr auto;gap:3px 12px;padding:10px 4px;border-bottom:1px solid var(--line);text-align:left;width:100%;font:inherit;color:inherit}
  .unl .urow>span:first-child,.unl .urow small,.unl .urow em{min-width:0;overflow-wrap:anywhere}
  .unl .urow small{grid-column:1/-1;color:var(--mute);letter-spacing:.04em;line-height:1.4}
  .unl .urow small:empty{display:none}
  .unl em{font-style:normal;color:var(--mute);font-size:10px;letter-spacing:.12em;text-transform:uppercase;align-self:start}
  /* a locked row is the way in — the requirement reads in the same green everything unlockable wears (v14 3.1) */
  .unl .lock>span:first-child{color:var(--mute)}
  .unl .lock small{color:var(--ok)}
  .unl .done>span:first-child,.unl .done em{color:var(--ok)}
  .unl .urow:active{background:color-mix(in srgb,var(--ink) 6%,transparent)}
  #unl-lede{width:min(92vw,440px);margin:0 auto 4px;text-align:left;line-height:1.5}

/* ==================================================================
   achievements — the second tab of ui/screens/progress.js
   ================================================================== */
  /* v14 (8.2): no horizontal axis at all here — an accidental left-swipe left the rows off-centre with their text clipped,
     and nothing snapped them back. The board keeps its sideways axis, because a wide score table needs it */
  .ach{display:flex;flex-direction:column;width:min(92vw,440px);font-family:var(--mono);font-size:12px;overflow-x:hidden}
  /* and nothing inside a row may widen it: a grid child's default min-width is its content, which is what let it grow */
  .ach .a>span:first-child,.ach .a small,.ach em{min-width:0;overflow-wrap:anywhere}
  .ach h4{font-weight:500;font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--ink);padding:18px 4px 6px;display:flex;justify-content:space-between;align-items:baseline}
  .ach h4 span{color:var(--mute);letter-spacing:.06em;text-transform:none;font-size:11px}
  .ach h4.secret{color:var(--cue)}
  .ach button.a{display:grid;grid-template-columns:1fr auto;gap:3px 12px;padding:10px 4px;border-bottom:1px solid var(--line);text-align:left;width:100%;font:inherit;color:inherit}
  .ach button.a.jump:active{background:color-mix(in srgb,var(--ink) 6%,transparent)}
  .ach .a small{grid-column:1/-1;color:var(--mute);letter-spacing:.04em;line-height:1.4}
  .ach .lock>span:first-child{color:var(--mute)}
  /* v14 (8.3): the game name leads the title, so it sits before the name and not after it */
  .ach .a>span:first-child i{font-style:normal;color:var(--mute);font-size:9px;letter-spacing:.16em;margin-right:8px;text-transform:uppercase}
  .ach em{font-style:normal;color:var(--mute);font-size:10px;letter-spacing:.12em;text-transform:uppercase;align-self:start}
  .ach .lock em.u{color:var(--cue)}
  .ach .done>span:first-child,.ach .done em{color:var(--ok)}
  .ach .done .pbar i{background:var(--ok)}
  .ach .a small.go{color:var(--ink);opacity:.7}
  .pbar{grid-column:1/-1;height:3px;background:var(--line);margin-top:4px;position:relative}
  .pbar i{position:absolute;left:0;top:0;bottom:0;background:var(--ink)}
  .pbar.s i{background:var(--cue)}
  .ach .a.flash{animation:achflash 1.6s ease-out}
  /* earned rows slide in on every visit; ones earned since the last visit come in green (v8) */
  .ach .a.done{animation:achin .5s ease-out both}
  .ach .a.new{animation:achnew 1.6s ease-out both}
  @keyframes achin{0%{opacity:0;translate:-10px 0}100%{opacity:1;translate:0 0}}
  @keyframes achnew{0%{background:color-mix(in srgb,var(--ok) 40%,transparent);translate:-14px 0;opacity:0}25%{opacity:1;translate:0 0;background:color-mix(in srgb,var(--ok) 40%,transparent)}100%{background:none}}
  @keyframes achflash{0%{background:color-mix(in srgb,var(--ink) 18%,transparent)}100%{background:none}}
  .ach .a.done{animation:achin .5s ease-out both}

/* ==================================================================
   the key — ui/screens/key.js (build 22, v14 9.2-9.7)
   ================================================================== */
  /* Option A: the key dead centre, seven games on a ring, a root per game growing inward. The roots are drawn as one
     <line> per combination so a segment can light on its own (9.6) — the segment count is the game's own, not a constant */
  /* v15 (5.3, build 26): three keys across the top, then whichever one is selected below. #key-main is the ring and its
     panel; #key-shell is what keys 2 and 3 show instead, because #372 has not decided what is behind them */
  #key-main{display:flex;flex-direction:column;align-items:center;gap:12px;width:100%;min-height:0}
  .keyrow{display:flex;gap:10px;justify-content:center;align-items:flex-end}
  .kkey{display:flex;flex-direction:column;align-items:center;gap:5px;padding:9px 14px;border:1px solid var(--line);background:none;min-width:82px}
  .kkey.sel{border-color:var(--ink)}
  .kkey b{font-family:var(--mono);font-weight:700;font-size:9px;letter-spacing:.18em;text-transform:uppercase;color:var(--mute)}
  .kkey.sel b{color:var(--ink)}
  .kkey u{text-decoration:none;font-family:var(--mono);font-size:9px;letter-spacing:.14em;text-transform:uppercase;color:var(--mute);opacity:.8}
  .kkey.whole u{color:var(--ok);opacity:1}
  /* locked is the default state of a key: it is not turned yet. Whole is the only other one */
  /* v17 (B.31): the tier's theme. `--ktint` and `--kground` come off config/keys.js through ui/screens/key.js, so the
     three themes are data and this only spends them: the glyph, the hub and the halo wear the tint, and the screen sits
     on the theme's own ground. Roots green, Frost white-blue, Thorn black with white accents. A CLEARED SEGMENT STAYS
     GREEN — cleared is `--ok` everywhere in the app (L8) and a theme is dressing, not a new state. */
  #s-key{--ktint:var(--ink);--kground:transparent}
  #key-main{background:radial-gradient(circle at 50% 34%,var(--kground),transparent 68%)}
  #key-keys.one .kkey{pointer-events:none}
  .kkey i{display:block;font-family:var(--mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--ktint);opacity:.75;margin-top:1px}
  #s-key .kglyph{stroke:var(--ktint);opacity:.85}
  #s-key .khub{stroke:var(--ktint);opacity:.5}
  .kgl{width:34px;height:34px;overflow:visible}
  .kgl path{fill:none;stroke:var(--mute);stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round;transition:stroke .35s ease}
  .kkey.sel .kgl path{stroke:var(--ktint,var(--ink))}
  .kkey.whole .kgl path{stroke:var(--ok);filter:drop-shadow(0 0 6px color-mix(in srgb,var(--ok) 55%,transparent))}
  .kkey.shell .kgl{opacity:.45}
  .kgl.big{width:76px;height:76px}
  #key-shell{display:flex;flex-direction:column;align-items:center;gap:14px;max-width:34ch;text-align:center;padding-top:22px}
  #key-shell p{color:var(--mute);font-size:13px;line-height:1.6}
  #key-shell p.soon{font-family:var(--mono);font-size:11px;letter-spacing:.06em;color:var(--mute);opacity:.7;line-height:1.7}
  #key-ring{width:min(86vw,340px);max-height:46vh;overflow:visible;margin-top:2px}
  .khub{fill:none;stroke:var(--line);stroke-width:1}
  .kglyph{fill:none;stroke:var(--mute);stroke-width:2.4;stroke-linecap:round;transition:stroke .5s ease,filter .5s ease}
  .kglyph.whole{stroke:var(--ok);filter:drop-shadow(0 0 8px color-mix(in srgb,var(--ok) 60%,transparent))}
  .kroot{stroke:var(--line);stroke-width:3;stroke-linecap:round;transition:stroke .35s ease,opacity .35s ease;opacity:.7}
  .kroot.on{stroke:var(--ok);opacity:1}
  /* 9.5: the short advance. The segment that just cleared walks in from the ring end and settles — nothing else moves.
     v15 (5.1): and the whole root of the game that earned it lights behind, so the screen says which game moved before
     it says which combination did. The halo is a single line under the segments, invisible until it is asked for */
  /* v17 (B.33): BOTH RUN AT 1.5x. Aiden's note off the build-27 board was "too fast" and the board's own slider setting
     was 1.5, so that is the number: .62s -> .93s and 1.9s -> 2.85s. The interlude in ui/screens/key.js waits 1.5x as
     long for the same reason — it used to hand itself back over the top of the halo. The unlock TOAST is unchanged. */
  @keyframes krootgrow{0%{opacity:0;stroke-width:6;filter:drop-shadow(0 0 7px var(--ok))}45%{opacity:1;stroke-width:6}100%{opacity:1;stroke-width:3;filter:none}}
  .kroot.grow{animation:krootgrow .93s ease-out both}
  .khalo{stroke:var(--ok);stroke-width:11;stroke-linecap:round;opacity:0}
  @keyframes khaloglow{0%{opacity:0}22%{opacity:.3}70%{opacity:.22}100%{opacity:0}}
  .kr.glow .khalo{animation:khaloglow 2.85s ease-out both;filter:blur(5px)}
  .knode{cursor:pointer}
  .khit{fill:transparent}
  .kdot{fill:var(--ground);stroke:var(--line);stroke-width:1.5;transition:stroke .35s ease,fill .35s ease}
  .knode.lit .kdot{stroke:var(--ok)}
  .knode.home .kdot{fill:var(--ok);stroke:var(--ok)}
  .knode.sel .kdot{stroke:var(--ink);stroke-width:2.5}
  .klbl{fill:var(--mute);font-family:var(--display);font-weight:500;font-size:11px;letter-spacing:.06em;text-anchor:middle}
  .knode.sel .klbl{fill:var(--ink)}
  .knum{fill:var(--mute);font-family:var(--mono);font-size:9px;letter-spacing:.1em;text-anchor:middle;opacity:.75}
  .knode.home .knum{fill:var(--ok);opacity:1}
  .keycount{font-family:var(--mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--ink)}
  #key-warn.warn{color:var(--miss);opacity:1;text-transform:none;letter-spacing:.08em;max-width:34ch}
  #key-list{max-height:34vh}
  .keyblk h4{font:500 14px var(--display);letter-spacing:.1em;text-transform:uppercase;color:var(--ink);padding:2px 0 2px}
  .keyblk h4 span{font-family:var(--mono);font-size:10px;letter-spacing:.16em;color:var(--mute);margin-left:8px}
  .keyblk p{color:var(--mute);font-size:12px;line-height:1.5;margin:2px 0 8px}
  .krow{display:grid;grid-template-columns:1fr auto;gap:2px 10px;padding:8px 0;border-bottom:1px solid var(--line)}
  .krow b{grid-column:1;grid-row:1;font-weight:500;font-size:13px;letter-spacing:.04em;color:var(--ink)}
  .krow i{grid-column:1;grid-row:2;font-family:var(--mono);font-style:normal;font-size:11px;color:var(--mute)}
  .krow u{grid-column:2;grid-row:1/3;align-self:center;justify-self:end;text-decoration:none;font-family:var(--mono);font-size:9px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute)}
  .krow.done u{color:var(--ok)}
  .krow.done b{color:var(--ok)}
  .krow.nobar i{color:var(--miss)}
  .krow{cursor:pointer}
  /* v15 (5.4): the first open of the keys, once per profile. The keys land, then the ring builds itself out of nothing —
     hub, roots, nodes — rather than simply being there when the screen arrives. Every open after this one is instant */
  @keyframes keyin{0%{opacity:0;translate:0 -10px}100%{opacity:1;translate:0 0}}
  @keyframes hubin{0%{opacity:0;scale:.2}70%{opacity:1;scale:1.12}100%{opacity:1;scale:1}}
  #s-key.first .kkey{animation:keyin .5s ease-out both}
  #s-key.first .kkey:nth-child(2){animation-delay:.14s}
  #s-key.first .kkey:nth-child(3){animation-delay:.28s}
  #s-key.first .khub,#s-key.first .kglyph{animation:hubin .7s cubic-bezier(.2,.9,.3,1.2) .5s both;transform-box:fill-box;transform-origin:center}
  #s-key.first .kr{animation:keyin .6s ease-out 1s both}
  #s-key.first .knode{animation:keyin .5s ease-out 1.5s both}
  #s-key.first .keycount,#s-key.first #key-hint{animation:keyin .5s ease-out 1.9s both}
  /* 5.1: while the interlude plays, nothing on this screen invites a tap — the run is coming straight back */
  #s-key.auto .back,#s-key.auto .keyrow{opacity:.2;transition:opacity .3s ease}
  /* ---------- v18 (B.22, build 32): the ring in the tier's style — Lantern, Circuit, Thorn ----------
     config/keys.js names the style, the tint and the dim; ui/screens/key.js draws the shapes; this file only dresses them.
     A segment is a <path pathLength="1"> in every style, so krootgrow and khaloglow above apply unchanged. A LIT SEGMENT
     WEARS THE TIER'S TINT since B.22 — the page Aiden approved lights Lantern warm, Circuit white-blue and Thorn white;
     the build-30 "cleared stays green" line is withdrawn for the ring only (green is still L8's mark everywhere else). */
  .kroot{fill:none;stroke:var(--kdim,var(--line))}
  .kroot.on{stroke:var(--ktint)}
  #s-key[data-style="lantern"] .kroot.on{filter:drop-shadow(0 0 4px color-mix(in srgb,var(--ktint) 70%,transparent))}
  #s-key[data-style="lantern"] .kroot{stroke-width:2.6}
  .karc{fill:none;stroke:var(--ktint);stroke-width:1.4;opacity:.55;filter:drop-shadow(0 0 5px color-mix(in srgb,var(--ktint) 60%,transparent))}
  #s-key[data-style="circuit"] .kroot{stroke-width:2.2;stroke-linecap:square}
  #s-key[data-style="circuit"] .kroot.on{filter:drop-shadow(0 0 2px color-mix(in srgb,var(--ktint) 50%,transparent))}
  .kdot2{fill:var(--ground);stroke:var(--kdim,var(--line));stroke-width:1;transition:fill .35s ease,stroke .35s ease}
  .kdot2.on{fill:var(--ktint);stroke:var(--ktint)}
  #s-key[data-style="circuit"] .kdot{rx:0}
  #s-key[data-style="thorn"] .kroot{stroke-width:1.6}
  #s-key[data-style="thorn"] .kroot.on{filter:none}
  .kthorn{fill:var(--kdim,var(--line));stroke:none;transition:fill .35s ease}
  .kthorn.on{fill:var(--ktint)}
  #s-key[data-style="thorn"] .khub{stroke:var(--ktint);opacity:.9;stroke-width:.8}
  #s-key[data-style="thorn"] #key-main{background:#000}
  .khalo{fill:none;stroke:var(--ktint)}
  @keyframes krootgrow{0%{opacity:0;stroke-width:6;filter:drop-shadow(0 0 7px var(--ktint))}45%{opacity:1;stroke-width:6}100%{opacity:1;stroke-width:3;filter:none}}
  .kglyph path{fill:none;stroke:inherit}
  .knode.lit .kdot{stroke:var(--ktint)}
  .knode.home .kdot{fill:var(--ktint);stroke:var(--ktint)}
  .knode.home .knum{fill:var(--ktint)}
  .kglyph.whole{stroke:var(--ktint);filter:drop-shadow(0 0 8px color-mix(in srgb,var(--ktint) 60%,transparent))}
  /* B.23: the ring's own ground absorbs a tap (its act is silence), and each root has a wide invisible hit line */
  .kground{cursor:default}
  .khit2{fill:none;stroke:transparent;stroke-width:22;stroke-linecap:round;cursor:pointer;pointer-events:stroke}
  /* B.20: the whole-key moment. The hub glyph turns and flares, every root glows once, the count line lands */
  @keyframes kwholeglyph{0%{rotate:0deg;scale:1}35%{rotate:-18deg;scale:1.25;filter:drop-shadow(0 0 14px var(--ktint))}70%{rotate:8deg;scale:1.12}100%{rotate:0deg;scale:1;filter:drop-shadow(0 0 8px color-mix(in srgb,var(--ktint) 60%,transparent))}}
  #s-key.kwhole .kglyph{animation:kwholeglyph 2.2s cubic-bezier(.3,.9,.3,1.1) both;transform-box:fill-box;transform-origin:center}
  #s-key.kwhole .khalo{animation:khaloglow 2.85s ease-out both;filter:blur(5px)}
  #s-key.kwhole .keycount{animation:keyin .6s ease-out .9s both;color:var(--ktint)}
  #s-key.kwhole .khub{animation:hubin 1.2s cubic-bezier(.2,.9,.3,1.2) both;transform-box:fill-box;transform-origin:center}

/* ==================================================================
   customise — ui/screens/customise.js
   ================================================================== */
  /* customise */
  /* v18 (B.31, build 33): Customise is a TAB now, so the tab scrolls rather than the screen — the tab row above it has
     to stay put, which is what the whole screen scrolling would have taken away */
  #p-cus{gap:12px;flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding-bottom:18px}
  .pv{width:min(84vw,320px);aspect-ratio:16/10;background:var(--ground);border:1px solid var(--line);position:relative;overflow:hidden;pointer-events:none;flex:none}
  /* v9: every preview has the same finger as the pre-game demo, and shows the tap and what comes of it */
  #pvg{position:absolute;left:0;top:0;width:26px;height:26px;margin:-13px 0 0 -13px;border-radius:50%;border:2px solid var(--ink);outline:2px solid #0a0a0b;background:color-mix(in srgb,var(--ink) 18%,transparent);opacity:0;transition:left .4s cubic-bezier(.3,.7,.3,1),top .4s cubic-bezier(.3,.7,.3,1),opacity .2s;box-shadow:inset 0 0 0 2px #0a0a0b;z-index:5}
  #pvg.on{opacity:.95}
  #pvg.tap{animation:gtap .22s ease-out}
  #pvg.hold{scale:.86;background:color-mix(in srgb,var(--ink) 40%,transparent)}
  .pvtxt{position:absolute;left:0;right:0;text-align:center;font-family:var(--mono);font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--mute)}
  .pvtxt b{display:block;font-size:22px;color:var(--ink);letter-spacing:0;font-variant-numeric:tabular-nums}
  .pvtxt b.g{color:var(--ok)}
  .pvtxt b.r{color:var(--miss)}
  .pv>div{position:absolute;inset:0;display:none}
  .pv[data-g="quick-tap"] .pvqt,.pv[data-g="dots"] .pvdots,.pv[data-g="hold"] .pvhold,.pv[data-g="sequence"] .pvseq{display:block}
  .pvgen{display:none;align-items:center;justify-content:center;gap:8%}
  /* v8 previews drawn from the real games: a ball and wall, a flash pane, a field of shapes */
  .pvtm{display:none}
  .pv[data-g="timing"] .pvtm{display:block}
  .pvrx{display:none;place-items:center;font-family:var(--mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--mute);transition:background .05s}
  .pv[data-g="reaction"] .pvrx{display:grid}
  .pvrx b{display:block;font-size:24px;color:var(--ink);letter-spacing:0;text-align:center}
  .pvrx.lit{background:var(--sq-live);color:var(--ground)}
  .pvsp{display:none;flex-wrap:wrap;gap:5% 7%;padding:8% 10%;align-content:center;justify-content:center}
  .pv[data-g="spot"] .pvsp{display:flex}
  .pvsp i{width:11%;aspect-ratio:1;background:var(--sq-live);transition:opacity .3s}
  .pvsp i.c{border-radius:50%}
  .pvsp i.t{clip-path:polygon(50% 0,100% 100%,0 100%)}
  .pvsp i.odd{box-shadow:0 0 0 3px var(--ok)}
  .pvsp i.dim{opacity:.2}
  .pvgen i{width:16%;aspect-ratio:1;background:var(--sq-live)}
  .pvgen i.c{border-radius:50%}
  .pvgen i.t{clip-path:polygon(50% 0,100% 100%,0 100%)}
  .pvgen i.ld{background:none;border:2px dashed var(--cue);border-radius:50%}
  .pvdots{--dsz:22%}
  .pvdots b{position:absolute;left:0;top:0;width:var(--dsz);aspect-ratio:1;border-radius:50%;background:var(--sq-live);opacity:0;transition:opacity .08s}
  .pvdots b.on{opacity:1}
  .pvdots b.ld{background:color-mix(in srgb,var(--cue) 22%,transparent);border:2px solid var(--cue)}
  .pvhold svg{width:100%;height:100%}
  .pvhold #pvhcut{display:none}
  .pvhold.cut #pvhgrow{display:none}
  .pvhold.cut #pvhcut{display:block}
  .pvhold .t{fill:none;stroke:var(--sq-live);stroke-width:2}
  .pvhold .m{fill:color-mix(in srgb,var(--sq-live) 55%,var(--cue));transition:r .9s linear}
  /* v14 (8.8): the Cut pieces swatch had nothing in the preview to colour. Same pair as the run: the cut-off piece full, the
     rest of the shape at 40% — so picking a Cut colour shows what it does instead of changing nothing on screen */
  .pvhold #pvhcut .pa{fill:var(--cutp,var(--sq-live))}
  .pvhold #pvhcut .pb{fill:color-mix(in srgb,var(--cutp,var(--sq-live)) 40%,var(--ground))}
  .pvhold #pvhcut line{stroke:var(--ink);stroke-width:1.5;opacity:.7}
  .pvtm .clk{top:36%}
  .pvtm .tgt{top:8%}
  .pvtm .tgt b{font-size:14px}
  .pvseq{display:none;gap:4px;padding:16% 6% 6%}
  .pv[data-g="sequence"] .pvseq{display:flex}
  .pvseq i{flex:1;background:var(--sq);border-radius:0 0 4px 4px}
  .pvseq i.lit{background:var(--sq-live)}
  /* v18 (B.30): the locked line sits UNDER the row it is about, one per group, and takes no height until it has
     something to say — never over the options, and never a toast (the toast is pinned to the top of the screen, which
     is what made it unreadable). It is a button in everything but name: a tap goes to the achievement that opens it */
  .lockline{font-family:var(--mono);font-size:11px;letter-spacing:.14em;color:var(--cue);text-transform:uppercase;text-align:center;max-width:36ch;line-height:1.5;cursor:pointer}
  .lockline:empty{display:none}
  .lockline:not(:empty){padding:2px 10px 4px}
  .lockline u{color:var(--ink);text-decoration:underline;text-underline-offset:4px}
  .cgroup{display:flex;flex-direction:column;gap:8px;align-items:center}
  .clabel{font-family:var(--mono);font-size:13px;letter-spacing:.24em;text-transform:uppercase;color:var(--ink)}
  .sw{display:flex;gap:10px;flex-wrap:wrap;justify-content:center}
  .sw button{width:38px;height:38px;border:1px solid var(--line);border-radius:2px;position:relative;background-size:cover}
  .sw button.sel{outline:2px solid var(--ink);outline-offset:2px}
  .sw button.pvw{outline:2px solid var(--cue);outline-offset:2px}
  .sw button.locked{opacity:.32}
  .sw button.locked::after{content:"";position:absolute;inset:-1px;background:linear-gradient(135deg,transparent 46%,var(--ground) 46%,var(--ground) 54%,transparent 54%)}
  .sw button.wheel{background:conic-gradient(red,yellow,lime,cyan,blue,magenta,red);border-radius:50%}
  .sw button.wheel::before{content:"";position:absolute;inset:11px;border-radius:50%;background:var(--ground)}
  .sw button.wheel.locked::after{border-radius:50%}
  .sw button.bg-stars{background-image:radial-gradient(circle at 20% 30%,#fff 1px,transparent 1.5px),radial-gradient(circle at 70% 60%,#fff 1px,transparent 1.5px),radial-gradient(circle at 45% 80%,#fff .8px,transparent 1.3px)}
  .sw button.bg-grid{background-image:repeating-linear-gradient(0deg,rgba(255,255,255,.18) 0 1px,transparent 1px 9px),repeating-linear-gradient(90deg,rgba(255,255,255,.18) 0 1px,transparent 1px 9px)}
  .sw button.bg-rain{background-image:repeating-linear-gradient(90deg,transparent 0 6px,rgba(255,255,255,.22) 6px 7px,transparent 7px 13px)}
  .sw button.bg-orbs{background-image:radial-gradient(circle at 30% 35%,rgba(255,255,255,.25),transparent 55%),radial-gradient(circle at 75% 70%,rgba(255,255,255,.18),transparent 50%)}
  .opt{font-family:var(--mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;padding:9px 13px;border:1px solid var(--line);color:var(--mute)}
  .opt.sel{color:var(--ink);border-color:var(--ink)}
  .opt.locked{opacity:.35;text-decoration:line-through}
  /* v17 (B.32): a locked TRACK row is not a struck-out option — it is the track this game plays, readable, with a
     padlock drawn beside it and no text about what opens it (A.1). The padlock is two boxes, so nothing is asked of a
     font that may not have the glyph */
  .opt.locked.plain{opacity:.72;text-decoration:none;position:relative;padding-right:30px}
  .opt.locked.plain::after{content:"";position:absolute;right:10px;top:50%;width:10px;height:7px;margin-top:-2px;border:1px solid var(--mute);border-radius:1px}
  .opt.locked.plain::before{content:"";position:absolute;right:12.5px;top:50%;width:5px;height:4px;margin-top:-7px;border:1px solid var(--mute);border-bottom:0;border-radius:3px 3px 0 0}
  .earn{font-family:var(--mono);font-size:10px;letter-spacing:.12em;color:var(--mute);text-transform:uppercase;text-align:center;max-width:40ch;line-height:1.7}
  .earn b{color:var(--ink);font-weight:500}
  #wheelwrap{position:absolute;inset:0;display:none;place-items:center;background:color-mix(in srgb,var(--ground) 82%,transparent);z-index:8}
  #wheelwrap.on{display:grid}
  #wheelbox{display:flex;flex-direction:column;gap:14px;align-items:center;background:var(--panel);border:1px solid var(--line);padding:22px}
  #wheelpad{position:relative;width:240px;height:240px}
  #wheel{width:240px;height:240px;border-radius:50%;touch-action:none;cursor:crosshair;display:block}
  /* v14 (8.11): a small ring on the wheel at the colour currently chosen, so the wheel opens showing where you already are */
  #wheelmark{position:absolute;width:16px;height:16px;margin:-8px 0 0 -8px;border:2px solid var(--ink);border-radius:50%;box-shadow:0 0 0 1px var(--ground);pointer-events:none;transition:left .12s,top .12s}
  #wheelout{width:60px;height:24px;border:1px solid var(--line)}

/* ==================================================================
   about — ui/screens/about.js
   ================================================================== */
  .support{border:1px solid var(--ink);padding:14px 22px;display:flex;flex-direction:column;gap:6px;align-items:center;max-width:32ch}
  .support b{font-family:var(--mono);font-weight:700;font-size:12px;letter-spacing:.22em;text-transform:uppercase}
  .support small{color:var(--mute);font-size:13px;line-height:1.45;text-align:center}
  .tierbox{display:flex;flex-direction:column;gap:6px;width:min(92vw,380px);font-family:var(--mono);font-size:11px;letter-spacing:.1em;color:var(--mute);line-height:1.6}
  .tierbox div{display:grid;grid-template-columns:1fr 1fr;gap:10px;border-top:1px solid var(--line);padding:6px 0}
  .tierbox div:first-child{border:0;color:var(--ink);letter-spacing:.2em;text-transform:uppercase}
  .tierbox b{color:var(--ink);font-weight:500}
  .tierbox .y{color:var(--ok)}
  #s-about{overflow-y:auto;justify-content:flex-start;padding-top:max(18px,env(safe-area-inset-top))}
  /* 13.1: the supporter block is a thank-you line and three lines of what it includes — no comparison table */
  .tierbox div{grid-template-columns:1fr}
  .tierbox div:first-child{border-top:1px solid var(--line);color:var(--mute);letter-spacing:.1em;text-transform:none}

/* ==================================================================
   pass — ui/screens/pass.js
   ================================================================== */
  /* v14 (4.7): whose turn it is is the biggest thing on the screen and wears that player's colour; the game name is back at
     the top where it belongs; 4.9 puts what there is to beat under it, with the pace behind it as a bar */
  #s-pass{gap:14px}
  #s-pass .pwho{font-family:var(--title);font-size:clamp(28px,9.5vw,52px);letter-spacing:.14em;text-transform:uppercase;text-align:center;line-height:1.2}
  .pturnline{font-family:var(--mono);font-size:13px;letter-spacing:.3em;text-transform:uppercase;color:var(--ink)}
  .pbeat{display:flex;flex-direction:column;align-items:center;gap:8px;font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--mute);border:1px solid var(--line);padding:14px 24px;min-width:min(80vw,260px)}
  .pbeat b{font-size:clamp(34px,11vw,56px);color:var(--ink);letter-spacing:0;font-variant-numeric:tabular-nums;font-weight:700;line-height:1}
  .pbeat .pbar{width:100%;height:4px;background:var(--line);position:relative;margin:0}
  .pbeat .pbar i{position:absolute;left:0;top:0;bottom:0;background:var(--sq-live)}

/* ==================================================================
   result — ui/screens/result.js
   ================================================================== */
  /* result */
  #s-over{gap:14px}
  #s-over.on .big{animation:scorein .5s cubic-bezier(.2,.9,.3,1.2) both}
  @keyframes scorein{0%{opacity:0;scale:.35}60%{opacity:1;scale:1.12}100%{scale:1}}
  .change{display:flex;flex-direction:column;gap:8px;align-items:center;margin-top:4px}
  .change .chips+.chips{margin-top:2px}
  /* v9: everything sits lower so an achievement toast has the top of the screen to itself, and each block arrives in turn */
  #s-over{justify-content:flex-start;overflow-y:auto;padding-top:max(96px,calc(env(safe-area-inset-top) + 84px));gap:10px;padding-bottom:40px}
  #s-over.on>*{animation:rvin .5s ease-out both}
  #s-over.on>:nth-child(1){animation-delay:0s}
  #s-over.on>:nth-child(2){animation-delay:.1s}
  #s-over.on>:nth-child(3){animation-delay:.6s}
  #s-over.on>:nth-child(4){animation-delay:1s}
  #s-over.on>:nth-child(5){animation-delay:1.35s}
  #s-over.on>:nth-child(6){animation-delay:1.65s}
  #s-over.on>:nth-child(7){animation-delay:1.95s}
  #s-over.on>:nth-child(8){animation-delay:2.2s}
  #s-over.on>:nth-child(9){animation-delay:2.4s}
  #s-over.on>:nth-child(10){animation-delay:2.6s}
  #s-over.on>:nth-child(11){animation-delay:2.7s}
  @keyframes rvin{0%{opacity:0;translate:0 10px}100%{opacity:1;translate:0 0}}
  #s-over.on .stats span{animation:rvin .4s ease-out both}
  #s-over.on .stats span:nth-child(2){animation-delay:.25s}
  #s-over.on .stats span:nth-child(3){animation-delay:.5s}
  #s-over.on .stats span:nth-child(4){animation-delay:.75s}
  #to-games{margin-top:22px;font-size:20px;padding:16px 44px}
  .mchips{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}
  .mch{display:flex;flex-direction:column;align-items:center;gap:6px;padding:8px 10px;border:1px solid var(--line);opacity:.55;min-width:84px}
  .mch .pic{width:64px;height:64px;border:1px solid var(--line);background:var(--ground);display:grid;place-items:center}
  .mch b{font-family:var(--mono);font-weight:700;font-size:10px;letter-spacing:.18em;text-transform:uppercase}
  .mch.sel{border-color:var(--ink);opacity:1}
  #vsbox{display:none;width:min(92vw,360px);grid-template-columns:1fr auto 1fr;gap:10px;align-items:center;text-align:center;font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute)}
  #vsbox.on{display:grid}
  #vsbox b{display:block;font-size:clamp(30px,9vw,54px);color:var(--ink);letter-spacing:0;font-variant-numeric:tabular-nums}
  #vsbox .win b{color:var(--ok)}
  #vsbox .win{color:var(--ok)}
  #vsbox em{font-style:normal;font-family:var(--title);font-size:14px;color:var(--mute)}
  #over-top{width:min(92vw,360px);font-size:11px;flex:none}
  #over-top td,#over-top th{padding:5px 6px}
  #over-top .eyebrow{text-align:center;margin-bottom:4px}
  /* v15 (6.4): the panel is a fixed box that scrolls inside itself. It used to grow a row at a time as the top 10
     filled, and every row it gained pushed Game select further down the screen — the one button that has to be in the
     same place every time. Empty and full are now the same height, so nothing under it ever moves */
  .otwrap{height:min(30vh,188px);overflow-y:auto;overscroll-behavior:contain}
  /* v15 (5.1): the result stands aside while a key unlock plays. It cannot be tapped through — ui/actions.js holds the
     lock — and this is only what that looks like */
  #s-over.fadeout{opacity:0;transition:opacity .38s ease-out;pointer-events:none}
  .stats{font-family:var(--mono);color:var(--mute);font-size:13px;letter-spacing:.14em;text-transform:uppercase;display:flex;gap:22px}
  .stats b{color:var(--ink);font-weight:500}
  .verdict{font-family:var(--title);font-size:11px;letter-spacing:.16em;text-transform:uppercase;text-align:center;max-width:32ch;line-height:1.8}
  /* v17 (B.25, build 29): the tier's own colour arrives inline from config/verdicts.js — the one table the review page
     prints from too — so this only says what wearing a tier LOOKS like. Solo only (L4): ui/screens/result.js leaves the
     class and the colour off a two-player result, and light blue and red stay the players'. */
  .verdict.v-ace,.verdict.v-good,.verdict.v-ok,.verdict.v-bad{font-size:12px;text-shadow:0 0 14px color-mix(in srgb,currentColor 45%,transparent);animation:rise .4s ease-out both}
  .rank{font-family:var(--mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--mute)}
  .rank b{color:var(--sq-live);font-weight:700}
  .change{display:flex;flex-direction:column;gap:8px;align-items:center;margin-top:4px}
  /* result screen (v10): options first, then Go, then the board. An ad slot sits under the board for non-supporters */
  #s-over .change{margin-top:0}
  /* v14 (7.1): Go is on screen the moment the result is — a player going straight back in never waits for it to animate in */
  #s-over.on>#again{animation-delay:0s}
  #again{margin-top:6px;font-size:22px;padding:16px 60px}
  .adslot{width:min(92vw,360px);height:64px;border:1px dashed var(--line);display:grid;place-items:center;font-family:var(--mono);font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--mute);flex:none}
  .adslot.off{display:none}
  .mch.locked{opacity:.35}
  /* result screen (v11): the eyebrow carries only a status; the score marks lower-is-better; a full stop hides under the top 10 */
  #over-eyebrow:empty{display:none}
  #egg{width:6px;height:6px;border-radius:50%;background:var(--mute);opacity:.35;padding:0;margin:0 auto;flex:none;border:0;position:relative}
  #egg::before{content:"";position:absolute;inset:-14px}
  #over-vs .lbl{padding:0 2px}
  /* 3.4: Back sits at the top left of the result screen, not beside the share button */
  #over-back{position:absolute;left:6px;top:max(10px,env(safe-area-inset-top));margin:0;padding:12px 14px;font-family:var(--mono);font-size:12px;letter-spacing:.18em;color:var(--mute);z-index:5;animation:none!important}
  #over-back:hover{color:var(--ink)}
  /* 3.6: Challenge a friend, with a share icon */
  .shicon{width:15px;height:15px;vertical-align:-3px;margin-right:9px;overflow:visible}
  .shicon *{stroke:currentColor;fill:none;stroke-width:1.5;stroke-linecap:round}

/* ==================================================================
   lockbox — ui/screens/lockbox.js
   ================================================================== */
  /* a locked game or mode offers the way there (v10) — a sheet, not a toast */
  #lockwrap{position:absolute;inset:0;display:none;place-items:center;background:color-mix(in srgb,var(--ground) 84%,transparent);z-index:8;padding:24px}
  #lockwrap.on{display:grid}
  #lockbox{display:flex;flex-direction:column;gap:14px;align-items:center;background:var(--panel);border:1px solid var(--cue);padding:22px 20px;max-width:34ch;text-align:center}
  #lockbox h3{font-family:var(--title);font-size:14px;letter-spacing:.2em;text-transform:uppercase;color:var(--cue)}
  #lockbox p{font-size:15px;line-height:1.55;color:var(--ink)}
  #lockbox p b{font-family:var(--mono);font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);display:block;margin-top:8px}
  #lockbox .row{gap:10px}
  /* 3.7: the underline sits under "unlock" only */
  #lock-go,#lock-go:hover,#lock-go:focus-visible{text-decoration:none}
  #lock-go u{text-decoration:underline;text-underline-offset:7px}

/* ==================================================================
   toast — ui/toast.js
   ================================================================== */
  .ach em i.sw,#toast i.sw{display:inline-block;width:10px;height:10px;vertical-align:-1px;margin-left:6px;border:1px solid var(--line)}
  #toast{position:absolute;left:50%;top:max(20px,env(safe-area-inset-top));transform:translate(-50%,-30px);background:var(--panel);border:1px solid var(--ink);padding:10px 16px;font-family:var(--mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;opacity:0;transition:.22s;pointer-events:none;max-width:88vw;text-align:center;line-height:1.5;z-index:9}
  #toast.on{opacity:1;transform:translate(-50%,0)}
  /* on the result screen the toast sits low so it never covers the score and try-again (v8); an achievement toast is tappable */
  #toast.tap{pointer-events:auto;cursor:pointer;border-color:var(--ok)}
  #toast.tap::after{content:' · tap';color:var(--mute)}
  /* a green toast for an unlock the moment it lands (v10) */
  #toast.ok{border-color:var(--ok);color:var(--ok)}

/* ==================================================================
   ads — ui/ads.js
   ================================================================== */
  #adbreak{position:absolute;inset:0;display:none;place-items:center;background:var(--ground);z-index:40;flex-direction:column;gap:12px;text-align:center;font-family:var(--mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--mute)}
  #adbreak.on{display:grid}
  #adbreak .box{width:min(80vw,320px);aspect-ratio:1;border:1px dashed var(--line);display:grid;place-items:center;margin:0 auto 14px}
  #adbreak button{font-family:var(--mono);font-size:12px;letter-spacing:.2em;text-transform:uppercase;color:var(--ink);border:1px solid var(--line);padding:10px 18px}
  #adbreak button[disabled]{opacity:.35}

/* ==================================================================
   run — run/run.js, games/_shared/hud.js — the game layer, HUD, countdown, goal line, ghost finger
   ================================================================== */
  /* game */
  /* v14 (4.10): the game layer claims every touch itself. body's `touch-action:manipulation` let the browser treat a second
     simultaneous finger as the start of a pinch and cancel BOTH pointers — which is exactly the versus bug: two players
     tapping at the same time cancelled each other out. `none` means no gesture is ever a candidate, so every finger lands */
  #game{display:none;position:absolute;inset:0;touch-action:none}
  #game.on{display:block}
  /* v14 (4.8): a pass & play run is outlined in the colour of whoever is playing it */
  /* v15 (3.12): a glow, not a solid line. The 3px rule read as a border on the screen rather than as whose turn it is.
     Colours are untouched — --pc is still P1 red / P2 light blue (L4) */
  #game.pturn::after{content:"";position:absolute;inset:0;border:0;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--pc,var(--p1)) 50%,transparent),inset 0 0 30px 7px color-mix(in srgb,var(--pc,var(--p1)) 38%,transparent);pointer-events:none;z-index:6}
  /* v17 (B.27): FLOW STATE. Above FLOW_AT taps a second the edges glow and the hum comes in over the music. `--flow` is
     0..1, written every frame by run/run.js — the same smoothed number audio.js swells the hum with, so the light and
     the sound arrive together by construction rather than by two timers agreeing. Light blue is P2 (L4), which is
     exactly why `flowOn` is solo only and this can never be up at the same time as `.pturn`. */
  #game.flowon::before{content:"";position:absolute;inset:0;pointer-events:none;z-index:5;opacity:var(--flow,0);
    box-shadow:inset 0 0 0 1px rgba(110,198,255,.45),inset 0 0 42px 10px rgba(110,198,255,.34)}
  /* v10: Spot was still listed here as count/find — the v8 rename never reached this line, so #gen stayed hidden and neither mode could start */
  #game[data-g="timing"] #gen,#game[data-g="reaction"] #gen,#game[data-g="spot"] #gen{display:block}
  .pop{animation:pop .22s ease-out}
  @keyframes pop{0%{scale:1}40%{scale:1.05}100%{scale:1}}
  .go{animation:ring .28s ease-out}
  @keyframes ring{0%{opacity:.9;scale:1}100%{opacity:0;scale:1.06}}
  /* the four v7 games share one layer, #gen, and draw their own screen into it */
  #gen{overflow:hidden}
  .glbl{position:absolute;left:0;right:0;text-align:center;font-family:var(--mono);font-size:12px;letter-spacing:.2em;text-transform:uppercase;color:var(--mute);pointer-events:none;line-height:1.7}
  .glbl.top{top:0}
  .glbl.bot{bottom:4%}
  .glbl b{color:var(--ink);font-weight:700;font-size:26px;letter-spacing:0;display:block}
  .glbl b.g{color:var(--ok)}
  .glbl b.r{color:var(--miss)}
  /* v15 (3.9 answer, build 25): Spot · Count flashes the right answer instead of holding the card. 0.6s, then the round goes */
  .glbl b.cflash{animation:cflash .6s cubic-bezier(.2,.9,.3,1.2) both}
  @keyframes cflash{0%{transform:scale(1.6);opacity:0}22%{transform:scale(1);opacity:1}100%{transform:scale(1);opacity:1}}
  .fs{position:absolute;width:var(--fsz);height:var(--fsz);background:var(--sq-live);pointer-events:none;transition:opacity .2s}
  .fs.circle{border-radius:50%}
  .fs.tri{clip-path:polygon(50% 0,100% 100%,0 100%)}
  .fs.bad{background:var(--miss)}
  .fs.dim{opacity:.18}
  .fs.odd{box-shadow:0 0 0 4px var(--ok)}
  /* v16 (§4): the round's shape lights in the OWNER's colour, never green, so the two players can read a shared field (L4) */
  .fs.odd.p1{box-shadow:0 0 0 4px var(--p1)}
  .fs.odd.p2{box-shadow:0 0 0 4px var(--p2)}
  /* v16 (§4): the crowd starts static and gains a pulse as the match goes on. `scale` is its own transform property, so
     this never fights the rotation the drift loop writes to `rotate` */
  .fs.puls{animation:fspuls 1.35s ease-in-out infinite}
  @keyframes fspuls{0%,100%{scale:1}50%{scale:1.16}}
  @media (prefers-reduced-motion:reduce){.fs.puls{animation:none}}
  #hud-time.you{color:var(--sq-live);font-weight:700}
  /* v16 (§4): Find versus keeps its score in the HUD. It used to be two 42%-tall bands with a border between them —
     that border WAS the mid line Aiden asked us to remove, and their big centred numbers sat on top of the crowd.
     Its own class, not a rule over every #hud-time child: Reaction versus and the pass & play hand-over share that slot */
  #hud-time .spvs b{font-weight:700;font-size:19px;letter-spacing:.04em}
  #hud-time .spvs small{display:block;font-size:9px;color:var(--mute);letter-spacing:.18em;margin-top:1px}
  /* v17 (B.13): FIXED-WIDTH SLOTS. The row was space-between over three items, so the centre one moved every time the
     right one changed width — and Sequence changes it every few seconds ("round 4 · watch" → "your turn"), which is the
     round marker sliding left and right that Aiden reported. The two outer slots take equal share and hold their own
     alignment, so the score sits still whatever either of them says. */
  .hud{position:absolute;left:0;right:0;top:max(14px,env(safe-area-inset-top));padding:0 16px 0 60px;display:flex;justify-content:space-between;align-items:baseline;font-family:var(--mono);font-variant-numeric:tabular-nums;color:var(--mute);font-size:13px;letter-spacing:.14em;pointer-events:none}
  .hud>#hud-mode,.hud>#hud-time{flex:1 1 0;min-width:0}
  .hud>#hud-mode{text-align:left}
  .hud>#hud-time{text-align:right}
  .hud>#score{flex:0 0 auto;padding:0 6px}
  #score{font-weight:700;font-size:clamp(32px,8vw,60px);color:var(--ink);letter-spacing:0}
  #quit{position:absolute;left:8px;top:max(8px,env(safe-area-inset-top));width:44px;height:44px;display:grid;place-items:center;font-family:var(--mono);font-size:22px;color:var(--mute);z-index:6}
  #quit:hover{color:var(--ink)}
  #count{position:absolute;inset:0;display:none;place-items:center;font-family:var(--mono);font-weight:700;font-size:clamp(80px,26vw,180px);color:var(--ink);pointer-events:none;z-index:5;font-variant-numeric:tabular-nums}
  #count.on{display:grid}
  #count span{animation:cpop .3s ease-out}
  @keyframes cpop{0%{scale:1.25;opacity:.4}100%{scale:1;opacity:1}}
  #bar{position:absolute;left:0;bottom:0;height:3px;background:var(--ink);width:100%;transform-origin:left;pointer-events:none}
  /* rate meter (v6): a thin bar down the right edge, fills with hits per second between the mode's floor and ceiling */
  #rate{position:absolute;right:6px;top:calc(22% + env(safe-area-inset-top));bottom:calc(30% + env(safe-area-inset-bottom));width:4px;background:var(--line);pointer-events:none;display:none;z-index:4}
  #game.timed #rate{display:block}
  #rate i{position:absolute;left:0;right:0;bottom:0;height:0;background:var(--sq-live);transition:height .18s ease-out}
  #rate b{position:absolute;right:0;bottom:calc(100% + 8px);font-family:var(--mono);font-weight:500;font-size:11px;letter-spacing:.1em;color:var(--mute);white-space:nowrap;font-variant-numeric:tabular-nums}
  #edge{position:absolute;inset:0;pointer-events:none;opacity:0;box-shadow:inset 0 0 40px 6px var(--sq-live);transition:opacity .25s;z-index:1}
  /* v14 (6.3): a round's result stays up until it is tapped. The cue sits under the card, breathing, and never takes the tap
     itself — the whole game layer is the target, so a tap anywhere advances */
  #tapon{position:absolute;left:0;right:0;bottom:calc(3.5% + env(safe-area-inset-bottom) + 12px);text-align:center;font-family:var(--mono);font-size:10px;letter-spacing:.24em;text-transform:uppercase;color:var(--mute);pointer-events:none;opacity:0;z-index:6}
  #tapon.on{animation:tapcue 1.6s ease-in-out .25s infinite both}
  @keyframes tapcue{0%,100%{opacity:.35}50%{opacity:.85}}
  /* ghost finger and one-liner for a mode's first play (v6) */
  #intro{position:absolute;inset:0;display:none;pointer-events:none;z-index:7}
  #intro.on{display:block}
  #intro p{position:absolute;left:16px;right:16px;top:calc(27% + env(safe-area-inset-top));text-align:center;font-family:var(--mono);font-size:13px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);line-height:1.6}
  /* v16 (§5 / A.3): the one-liner arrives AS A LINE. It came in a word at a time from v8 to build 26, and that reveal is
     most of what made a first-play screen feel heavy. The sub-line under it is gone from the copy table entirely.
     `.w` stays defined because #rxbar (the Go / No-go rule bar) still uses it. */
  #intro p{opacity:0;animation:wordin .45s ease-out .1s both}
  #intro p .w{display:inline-block;opacity:0;animation:wordin .4s ease-out both;margin-right:.4em}
  @keyframes wordin{0%{opacity:0;translate:0 6px}100%{opacity:1;translate:0 0}}
  /* the "Ready?" — hidden until the demo has finished, and only on a player's first run of that game (A.3) */
  #intro p .rdy{display:none}
  #intro.ready p .rdy{display:block;margin-top:14px;font-size:17px;font-weight:600;letter-spacing:.12em;color:var(--ok);animation:wordin .35s ease-out both}
  #intro.ready p .rdy small{display:block;font-size:10px;font-weight:400;color:var(--mute);margin-top:5px;letter-spacing:.18em;animation:tapcue 1.6s ease-in-out .4s infinite both}
  /* ghost finger (v8): a dark ring inside and outside the white one, so it reads on a white pad and on the black ground alike */
  #ghost{position:absolute;left:0;top:0;width:44px;height:44px;margin:-22px 0 0 -22px;border-radius:50%;border:2px solid var(--ink);outline:2px solid #0a0a0b;background:color-mix(in srgb,var(--ink) 18%,transparent);opacity:0;transition:transform .45s cubic-bezier(.3,.7,.3,1),opacity .2s;box-shadow:inset 0 0 0 3px #0a0a0b,0 0 18px rgba(232,230,225,.25)}
  #ghost.on{opacity:.9}
  #ghost.tap{animation:gtap .22s ease-out}
  #ghost.hold{scale:.86;background:color-mix(in srgb,var(--ink) 40%,transparent)}
  /* the tap (v9) rings outward instead of shrinking */
  @keyframes gtap{0%{scale:1;box-shadow:inset 0 0 0 3px #0a0a0b,0 0 0 0 rgba(232,230,225,.6)}60%{scale:1.25;box-shadow:inset 0 0 0 3px #0a0a0b,0 0 0 14px rgba(232,230,225,0)}100%{scale:1;box-shadow:inset 0 0 0 3px #0a0a0b,0 0 0 0 rgba(232,230,225,0)}}
  #flash{position:absolute;inset:0;background:var(--miss);opacity:0;pointer-events:none}
  #game.shake{animation:shake .32s linear}
  @keyframes shake{0%,100%{transform:translate(0)}20%{transform:translate(-8px,3px)}40%{transform:translate(7px,-4px)}60%{transform:translate(-5px,2px)}80%{transform:translate(4px,-2px)}}
  @media (prefers-reduced-motion:reduce){#game.shake,.pop,.go,.wordmark,.glow,#lead.on b,#count span{animation:none}}
  /* the next unlock, in view while you play (v8). v15 (2.2): it sits at the TOP of the screen now — above the HUD, not
     under it — because the thing being chased has to be visible while playing, achievements included. The HUD steps down
     to make room only when there is something to chase, so a run with no goal looks exactly as it did */
  #goal{position:absolute;left:50%;top:max(11px,env(safe-area-inset-top));transform:translateX(-50%);font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute);border:1px solid var(--line);padding:5px 10px;white-space:nowrap;pointer-events:none;display:none;z-index:4;max-width:min(92vw,calc(100vw - 116px));overflow:hidden;text-overflow:ellipsis}
  #goal.on{display:block}
  #goal b{color:var(--ink);font-weight:500}
  #goal.hit{border-color:var(--ok);color:var(--ok);animation:pop .3s ease-out}
  #goal.hit b{color:var(--ok)}
  /* v14 (3.3): one line high, showing the requirement, then what it unlocks — it scrolls between the two rather than
     cramming both in. run/run.js takes the game's own name back out of the requirement while you are inside that game */
  #goal{height:22px;padding:0 10px;text-overflow:clip}
  #goal>i,#goal>u{display:block;font-style:normal;text-decoration:none;line-height:22px;white-space:nowrap;text-align:center}
  #goal>u{color:var(--mute)}
  #goal.roll>i,#goal.roll>u{animation:goalroll 9s ease-in-out infinite}
  @keyframes goalroll{0%,40%{translate:0 0}50%,90%{translate:0 -22px}100%{translate:0 0}}
  #goal.hit>i::before{content:"\2713 "}
  /* v15 (2.2): the run's own furniture moves down under the goal line rather than the goal line hiding behind it */
  #game.goalon .hud{top:calc(max(14px,env(safe-area-inset-top)) + 30px)}
  #game.goalon #rate{top:calc(28% + env(safe-area-inset-top))}
  #game.bigc #score{visibility:hidden}
  #intro p{background:color-mix(in srgb,var(--ground) 88%,#fff);border:1px solid var(--line);padding:12px 14px;left:24px;right:24px}
  #game.timed #rate{display:none}
  #game.timed.live #rate{display:block}
  /* PB (v11): a thin marker on the rate bar at your best pace; a "best" ghost beside the running figure on the others */
  #rate u{position:absolute;left:-4px;right:-4px;height:2px;background:var(--cue);bottom:0;display:none;text-decoration:none}
  #rate u.on{display:block}
  /* v17 (B.13): "best" sat at +52px, which is inside the score's own box — #score runs to 60px of type — so on Sequence,
     where the score is the round number, the best-ghost was drawn behind it. It clears the score now. */
  #pbghost{position:absolute;left:50%;top:calc(max(14px,env(safe-area-inset-top)) + 78px);translate:-50% 0;font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute);display:none;pointer-events:none}
  #pbghost.on{display:block}
  #game.versus #pbghost,#game.bigc #pbghost{display:none}
  #hud-mode .p1,#hud-mode .p2{font-weight:700}

/* ==================================================================
   quick-tap — games/quick-tap/
   ================================================================== */
  /* pads sit low and short (v7): no taller than 26% of the screen, so the cue has a lane to fall down */
  .pad{position:absolute;bottom:calc(3% + env(safe-area-inset-bottom));width:48%;height:min(36vw,22vh)}
  .pad.l{left:0}
  .pad.r{right:0}
  /* Four (v9): a second row of pads above the first. Lead is gone from Quick Tap */
  .pad.up{display:none}
  #game[data-d="four"] .pad.up{display:block;bottom:calc(3% + env(safe-area-inset-bottom) + min(36vw,22vh) + 8px)}
  #game[data-d="four"] .cue{display:none}
  .sq{position:absolute;inset:0;--v:0;background:color-mix(in srgb,var(--sq-live) calc(var(--v)*100%),var(--sq));transition:transform .12s}
  .pad.l .sq{right:6px;border-radius:0 4px 4px 0}
  .pad.r .sq{left:6px;border-radius:4px 0 0 4px}
  /* lead cue: a square that drops from above and comes to rest just over the next pad — it never covers the white */
  .cue{position:absolute;left:50%;bottom:calc(100% + 10px);width:34%;aspect-ratio:1;margin-left:-17%;background:var(--cue);opacity:0;border-radius:4px;z-index:2;pointer-events:none}
  .pad.l .cue{margin-left:calc(-17% - 3px)}
  .pad.r .cue{margin-left:calc(-17% + 3px)}
  #game[data-g="quick-tap"] .cue{display:none}
  .cue.on{opacity:1;animation:fall 1.1s cubic-bezier(.3,0,.7,1) forwards}
  @keyframes fall{0%{transform:translateY(-560%);background:var(--cue);opacity:.7}70%{background:color-mix(in srgb,var(--cue) 55%,var(--sq-live))}100%{transform:translateY(0);background:var(--sq-live);opacity:.55}}
  .ring{position:absolute;inset:0;border:2px solid var(--sq-live);opacity:0;pointer-events:none}
  .pad.l .ring{right:6px;border-radius:0 4px 4px 0}
  .pad.r .ring{left:6px;border-radius:4px 0 0 4px}
  .pvqt .pad{height:38%}
  .pvqt .pad.up{display:none}
  .pv[data-d="four"] .pvqt .pad.up{display:block;bottom:calc(4% + 38% + 4px)}
  .pvqt .pad{bottom:4%;height:32%}
  /* big live count for Quick Tap solo (v10): the number sits in the middle of the screen, pops on every hit */
  #bigcount{position:absolute;left:0;right:0;top:34%;text-align:center;font-family:var(--mono);font-weight:700;font-size:clamp(80px,26vw,180px);color:var(--ink);pointer-events:none;display:none;font-variant-numeric:tabular-nums;line-height:1;z-index:2;opacity:.92}
  #game.bigc #bigcount{display:block}
  #bigcount.pop{animation:cpop .18s ease-out}
  /* the live count and the rate bar (v11) wait for the countdown to finish */
  #game.bigc #bigcount{display:none}
  #game.bigc.live #bigcount{display:block}

/* ==================================================================
   dots — games/dots/
   ================================================================== */
  #game[data-g="quick-tap"] #qt,#game[data-g="dots"] #field{display:block}
  #field{--dsz:clamp(64px,18vmin,110px)}
  #dot,#lead,#dring{position:absolute;left:0;top:0;width:var(--dsz);height:var(--dsz);border-radius:50%;will-change:transform}
  #dot{background:var(--sq-live);opacity:0}
  #dot.on{opacity:1}
  /* lead ring: just fades in, no effects (v5) */
  #lead{border:2px solid var(--cue);background:color-mix(in srgb,var(--cue) 22%,transparent);opacity:0;pointer-events:none;transition:opacity .1s}
  #lead.on{opacity:1}
  /* landing ring: starts pushed along the line of travel and settles onto the dot as it fades (v5) */
  #dring{border:2px solid var(--sq-live);opacity:0;pointer-events:none}
  #dring.go{animation:none;transition:transform .3s ease-out,opacity .3s ease-out}
  #game.versus #qt,#game.versus #field,#game.versus .hud,#game.versus #rate,#game.versus #goal,#game.versus #bar{display:none}

/* ==================================================================
   estimate — games/estimate/
   ================================================================== */
  /* hold: three svg layers — target (solid while growing), ghost (dotted outline), mine (filled) */
  #game[data-g="hold"] #hfield{display:block}
  #hfield svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
  #hfield path{fill:var(--sq-live);transition:opacity .15s}
  #hfield #hg path{fill:none;stroke:var(--cue);stroke-width:2;stroke-dasharray:5 6}
  /* v15 (3.3): at the round result the dashed target outline sits OVER your shape, so the difference is readable without
     motion and without a dismiss — that is why outline-over-shape beat flashing between the two. During the hold it stays
     underneath, where it has always been; only .rev lifts it */
  #hfield.rev #hg{z-index:3}
  #hfield #hm path{fill:color-mix(in srgb,var(--sq-live) 55%,var(--cue))}
  #hfield #ht path{fill:none;stroke:var(--sq-live);stroke-width:2}
  #hfield.show #ht path{fill:var(--sq-live);stroke:none}
  #hfield #hcut path{fill:var(--cutp,var(--sq-live))}
  #hfield #hcut path.b{fill:color-mix(in srgb,var(--cutp,var(--sq-live)) 40%,var(--ground))}
  #hfield line{stroke:var(--cue);stroke-width:3;stroke-dasharray:8 8;stroke-linecap:round;pointer-events:none}
  /* what to do, written large and faint behind the shape (v9) — "tap and hold" / "tap and draw a line" */
  #hbg{position:absolute;left:0;right:0;top:40%;text-align:center;font-family:var(--title);font-size:clamp(14px,4.2vw,28px);letter-spacing:.16em;text-transform:uppercase;color:var(--ink);opacity:0;pointer-events:none;transition:opacity .3s;line-height:1.5;padding:0 12px}
  #hbg.on{opacity:.13}
  /* the reveal (v9): a centred panel with two bars that fill while the px² count, then the difference, then the % */
  #hcalc{position:absolute;left:8%;right:8%;top:50%;transform:translateY(-50%);display:none;flex-direction:column;gap:12px;background:color-mix(in srgb,var(--ground) 82%,transparent);border:1px solid var(--line);padding:18px 16px;font-family:var(--mono);font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--mute);pointer-events:none;z-index:3}
  #hcalc.on{display:flex}
  .hrow{display:grid;grid-template-columns:56px 1fr 84px;gap:10px;align-items:center}
  .hrow .bar{height:14px;background:var(--line);position:relative;overflow:hidden}
  .hrow .bar i{position:absolute;left:0;top:0;bottom:0;width:0;background:var(--sq-live)}
  .hrow.m .bar i{background:color-mix(in srgb,var(--sq-live) 55%,var(--cue))}
  .hrow.b .bar i{background:color-mix(in srgb,var(--sq-live) 30%,var(--ground))}
  /* v13 (6.5): the Cut reveal is ONE bar — the whole shape — with the piece filling it from the left in the same two colours as the shape */
  .hrow.cutbar .bar{background:color-mix(in srgb,var(--cutp,var(--sq-live)) 40%,var(--ground))}
  .hrow.cutbar .bar i{background:var(--cutp,var(--sq-live))}
  .hrow .bar u{position:absolute;top:-4px;bottom:-4px;width:2px;background:var(--cue);left:0;text-decoration:none}
  .hrow b{color:var(--ink);text-align:right;font-variant-numeric:tabular-nums;letter-spacing:0;font-size:13px}
  #hres{text-align:center;opacity:0;transition:opacity .25s;line-height:1.9}
  #hres.on{opacity:1}
  #hres b{display:block;font-size:30px;color:var(--ink);letter-spacing:0}
  #hres b.g{color:var(--ok)}
  #hres b.r{color:var(--miss)}
  #hres i{font-style:normal;color:var(--ink)}
  #hlbl{position:absolute;left:0;right:0;bottom:6%;text-align:center;font-family:var(--mono);font-size:12px;letter-spacing:.2em;text-transform:uppercase;color:var(--mute);pointer-events:none}
  #hlbl b{color:var(--ink);font-weight:700;font-size:26px;letter-spacing:0;display:block;margin-bottom:6px}
  #hlbl b.g{color:var(--ok)}
  #hlbl b.r{color:var(--miss)}
  #hdiff.pop{animation:pop .3s ease-out}
  /* estimate: the shape you'll hold, shown as a picture top-right so nobody has to read it (v6) */
  #hicon{position:absolute;left:50%;top:8px;width:13vmin;height:13vmin;margin-left:-6.5vmin;overflow:visible;opacity:0;transition:opacity .2s}
  #hicon.on{opacity:1}
  #hicon path{fill:none;stroke:var(--sq-live);stroke-width:2}
  #hicon text{fill:var(--mute);font-family:var(--mono);font-size:9px;letter-spacing:.2em;text-transform:uppercase}
  /* the cut hint (v9): a finger drags a dotted line through a small shape, top right, before every Cut shape */
  #hicon line{stroke:var(--cue);stroke-width:2.5;stroke-dasharray:5 5;stroke-dashoffset:120;animation:cutline 2.2s ease-in-out infinite}
  #hicon circle.fg{fill:color-mix(in srgb,var(--ink) 25%,transparent);stroke:var(--ink);stroke-width:2;animation:cutfg 2.2s ease-in-out infinite}
  @keyframes cutline{0%,15%{stroke-dashoffset:120;opacity:0}20%{opacity:1}70%,100%{stroke-dashoffset:0;opacity:1}}
  @keyframes cutfg{0%,15%{cx:6px;cy:70px;opacity:0}20%{opacity:1}70%{cx:94px;cy:30px;opacity:1}85%,100%{cx:94px;cy:30px;opacity:0}}
  #hicon.big{width:26vmin;height:26vmin;right:auto;left:50%;top:14%;margin-left:-13vmin}
  /* Estimate (v10): what to do sits top-middle in ink, never behind the shape; the Cut share counts up from 0 so the eye lands on it; the shape sits lower */
  #hbg{top:11%;opacity:0;font-size:clamp(13px,3.8vw,22px);letter-spacing:.2em}
  #hbg.on{opacity:.85}
  #hshare{position:absolute;left:0;right:0;top:19%;text-align:center;font-family:var(--mono);font-weight:700;font-size:clamp(44px,15vw,96px);color:var(--sq-live);letter-spacing:0;font-variant-numeric:tabular-nums;pointer-events:none;opacity:0;transition:opacity .2s;line-height:1;z-index:2}
  #hshare.on{opacity:1}
  #hshare small{display:block;font-family:var(--mono);font-weight:500;font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--mute);margin-top:6px}
  /* v10: `#hfield svg` (inset 0, 100%) outranked `#hicon` on specificity, so the parked icon was drawn across the whole field — that was the big white outline in Grow and the clutter in Cut */
  /* v14 (6.11): the shape you grow is centre-top, always — top right was confusing, and it is shown even when it is the
     same shape as the target */
  #hfield #hicon{position:absolute;left:50%;right:auto;bottom:auto;top:4px;width:13vmin;height:13vmin;margin-left:-6.5vmin}
  #hfield #hicon.big{width:22vmin;height:22vmin;right:auto;left:50%;top:1%;margin-left:-11vmin}
  .sheet.len #lenwrap,.choice,#vsart.on,#vs-sub:not([hidden]),#lockwrap.on #lockbox,#wheelwrap.on #wheelbox,.mch,#over-vs .chip,#over-chips2 .chip,#over-chips3 .chip,.ach .a.lock,.ach h4,.lockline:not(:empty),#hcalc.on,#adbreak.on>div,.tbtn,#s-pass.on>*,#nextup:not([hidden]){animation:rise .18s ease-out both}
  /* Estimate (v11): the target's outline stays up while you grow */
  #hfield #hg path{stroke:var(--cue)}

/* ==================================================================
   sequence — games/sequence/
   ================================================================== */
  #qt,#field,#hfield,#seq,#gen{display:none}
  #field,#hfield,#seq,#gen{position:absolute;top:calc(16% + env(safe-area-inset-top));bottom:calc(3% + env(safe-area-inset-bottom));left:0;right:0}
  /* sequence: your turn is unmissable (v7) — keys dim while it plays, light up with a lit top edge when it's you */
  #seq.watch .key{opacity:.5}
  #seq.input .key{border-top:4px solid var(--sq-live);box-shadow:inset 0 6px 18px color-mix(in srgb,var(--sq-live) 22%,transparent)}
  #seq.input{background:color-mix(in srgb,var(--sq-live) 4%,transparent)}
  #turn{position:absolute;left:0;right:0;top:36%;text-align:center;font-family:var(--title);font-size:clamp(22px,7vw,40px);letter-spacing:.2em;text-transform:uppercase;color:var(--sq-live);pointer-events:none;opacity:0;z-index:4;text-shadow:0 0 18px var(--glow)}
  #turn.on{animation:turnin 1.1s ease-out both}
  @keyframes turnin{0%{opacity:0;scale:1.6}25%{opacity:1;scale:1}70%{opacity:1}100%{opacity:0;scale:.9}}
  /* sequence */
  #game[data-g="sequence"] #seq{display:flex;flex-direction:row;gap:6px;padding:0 6px}
  /* keys (v9): a mid grey, not the pad grey — only the white ones could be seen */
  .key{flex:1;background:color-mix(in srgb,var(--ground) 62%,#fff);border-radius:0 0 6px 6px;position:relative;transition:background .08s;border-top:2px solid color-mix(in srgb,var(--ground) 40%,#fff)}
  .key.lit{background:var(--sq-live)}
  .key.bad{background:var(--miss)}
  .key::after{content:attr(data-n);position:absolute;bottom:10px;left:0;right:0;text-align:center;font-family:var(--mono);font-size:10px;color:var(--ground);opacity:.7;letter-spacing:.2em}
  .key.lit::after{color:var(--ground)}
  .tbtn .keys{display:flex;gap:2px;height:22px;width:66px;margin-bottom:2px}
  .tbtn .keys i{flex:1;background:var(--sq);border-radius:0 0 3px 3px;border-top:1px solid var(--line)}
  .tbtn .keys i:first-child{background:var(--sq-live)}
  /* Sequence keys (v11): idle keys are white at reduced opacity, clearly dimmer than a lit key */
  .key{background:var(--ink);opacity:.3;border-top:2px solid var(--ink)}
  .key.lit{background:var(--sq-live);opacity:1}
  .key.bad{background:var(--miss);opacity:1}
  #seq.watch .key{opacity:.18}
  #seq.watch .key.lit{opacity:1}
  #seq.input .key{border-top:4px solid var(--sq-live);opacity:.42}
  #seq.input .key.lit{opacity:1}
  .key::after{color:var(--ground);opacity:.9}
  /* the two cues (v11) sit on a contrasting backing, so they read on any key colour */
  #turn{top:30%;font-size:clamp(18px,5.6vw,32px);left:50%;right:auto;translate:-50% 0;background:color-mix(in srgb,var(--ground) 90%,#fff);border:1px solid var(--sq-live);padding:14px 22px;white-space:nowrap}
  #turn.on{animation:turnin 1.3s ease-out both}
  #turn.p1{color:var(--p1);border-color:var(--p1)}
  #turn.p2{color:var(--p2);border-color:var(--p2)}
  #turn.stay{animation:none;opacity:1}
  #seqdone{position:absolute;left:50%;bottom:calc(6% + env(safe-area-inset-bottom));translate:-50% 0;z-index:6;font-family:var(--mono);font-size:12px;letter-spacing:.2em;text-transform:uppercase;border:1px solid var(--ink);padding:10px 18px;background:var(--ground);display:none}
  #seqdone.on{display:block}

/* ==================================================================
   timing — games/timing/
   ================================================================== */
  /* timing */
  .tmclock{position:absolute;left:0;right:0;top:34%;text-align:center;font-family:var(--mono);font-weight:700;font-size:clamp(56px,18vw,120px);color:var(--ink);font-variant-numeric:tabular-nums;letter-spacing:0;pointer-events:none}
  .tmtarget{position:absolute;left:0;right:0;top:10%;text-align:center;font-family:var(--mono);font-size:12px;letter-spacing:.2em;text-transform:uppercase;color:var(--mute);pointer-events:none}
  .tmtarget b{display:block;font-size:34px;color:var(--sq-live);letter-spacing:0;font-variant-numeric:tabular-nums}
  /* v14 (6.18): the running total of the time the game has asked for, under the round's target */
  .tmtarget u{display:block;text-decoration:none;font-size:10px;letter-spacing:.22em;color:var(--mute);font-variant-numeric:tabular-nums;margin-top:2px;min-height:1.2em}
  #tmwall{position:absolute;top:9%;bottom:0;background:color-mix(in srgb,var(--ground) 88%,#fff);border-left:1px solid var(--line);transition:opacity .3s;pointer-events:none}
  #tmball{position:absolute;left:0;top:0;width:var(--fsz);height:var(--fsz);border-radius:50%;background:var(--sq-live);pointer-events:none}
  #tmmark{position:absolute;width:var(--fsz);height:var(--fsz);border-radius:50%;border:2px dashed var(--cue);pointer-events:none;z-index:2}
  #tmghost{position:absolute;width:var(--fsz);height:var(--fsz);border-radius:50%;border:2px solid var(--ok);opacity:0;pointer-events:none;z-index:2}

/* ==================================================================
   reaction — games/reaction/
   ================================================================== */
  /* reaction */
  .rxpane{position:absolute;inset:0;display:grid;place-items:center;transition:background .05s}
  .rxpane.lit{background:var(--sq-live)}
  .rxpane.bad{background:color-mix(in srgb,var(--miss) 55%,var(--ground))}
  .rxpane .rxshape{width:34vmin;height:34vmin;background:var(--sq-live)}
  .rxpane .rxshape.circle{border-radius:50%}
  .rxpane .rxshape.tri{clip-path:polygon(50% 0,100% 100%,0 100%)}
  /* v19 (C.3, build 32): two more shapes so a Set's five rounds draw on five. A diamond is a square on its point and is
     never rotated (it would become the square); a hexagon is drawn flat-topped and may turn 30° */
  .rxpane .rxshape.diamond{clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%)}
  .rxpane .rxshape.hex{clip-path:polygon(25% 3%,75% 3%,100% 50%,75% 97%,25% 97%,0 50%)}
  .rxmsg{position:absolute;left:0;right:0;top:16%;text-align:center;font-family:var(--mono);font-size:12px;letter-spacing:.2em;text-transform:uppercase;color:var(--mute);line-height:1.8}
  .rxmsg b{display:block;font-size:clamp(48px,16vw,110px);color:var(--ink);letter-spacing:0;font-variant-numeric:tabular-nums;font-weight:700}
  .rxmsg b.fb{font-size:clamp(30px,9vw,60px);font-family:var(--title);letter-spacing:.08em;margin-bottom:10px;line-height:1.2}
  .rxmsg .sub{display:block;font-size:14px;letter-spacing:.2em;color:var(--ink);margin-top:8px}
  .rxpane.lit .rxmsg,.rxpane.lit .rxmsg b{color:var(--ground)}
  .rxrule{display:grid;grid-auto-flow:column;justify-content:center;gap:10px;align-items:center;margin-top:18px;color:var(--ink)}
  .rxpane .rxshape{transition:none}
  .rxrule i{display:inline-block;width:18px;height:18px;background:var(--sq-live)}
  .rxrule i.square{border-radius:1px}
  .rxrule i.circle{border-radius:50%}
  .rxrule i.tri{clip-path:polygon(50% 0,100% 100%,0 100%)}
  .rxrule i.diamond{clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%)}
  .rxrule i.hex{clip-path:polygon(25% 3%,75% 3%,100% 50%,75% 97%,25% 97%,0 50%)}
  /* v14 (6.22): white is the WHOLE screen again. The v10 square with burst lines was the preview screen's picture of Flash,
     not the game — the lines are gone and .rxbox with them, so the lit pane is the plain fill defined above */
  .rxpane.hit .rxmsg{z-index:2}
  /* the rule (v10) lives at the top-middle for the whole attempt, and arrives a word at a time */
  #rxbar{position:absolute;left:0;right:0;top:calc(16.5% + env(safe-area-inset-top));text-align:center;font-family:var(--mono);font-size:13px;letter-spacing:.2em;text-transform:uppercase;color:var(--ink);line-height:1.8;z-index:3;pointer-events:none}
  #rxbar .w{display:inline-block;opacity:0;animation:wordin .4s ease-out both;margin-right:.4em}
  #rxbar i{display:inline-block;width:20px;height:20px;background:var(--sq-live);vertical-align:-4px;margin:0 4px}
  #rxbar i.circle{border-radius:50%}
  #rxbar i.tri{clip-path:polygon(50% 0,100% 100%,0 100%)}
  #rxbar i.diamond{clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%)}
  #rxbar i.hex{clip-path:polygon(25% 3%,75% 3%,100% 50%,75% 97%,25% 97%,0 50%)}
  #rxbar b{color:var(--ok);font-weight:700}
  .rxpane .rxmsg{top:22%}

/* ==================================================================
   spot — games/spot/
   ================================================================== */
  /* v14 (6.31): Spot · Find runs its own clock in large grey type BEHIND the shapes — the cost of staring, while you stare */
  .spclock{position:absolute;left:0;right:0;top:34%;text-align:center;font-family:var(--mono);font-weight:700;font-size:clamp(52px,17vw,110px);color:var(--ink);opacity:.09;font-variant-numeric:tabular-nums;pointer-events:none;z-index:0}
  /* count keypad */
  .pad-num{position:absolute;left:8%;right:8%;bottom:6%;display:grid;grid-template-columns:repeat(5,1fr);gap:8px}
  .pad-num button{font-family:var(--mono);font-weight:700;font-size:22px;padding:14px 0;border:1px solid var(--line);color:var(--ink);pointer-events:auto}
  .pad-num button.sel{border-color:var(--ink);background:color-mix(in srgb,var(--ink) 10%,transparent)}
  /* versus for Reaction and two-player Count (v11): two zones, the top one turned to face the other player */
  .vz{position:absolute;left:0;right:0;height:42%;display:grid;place-items:center;font-family:var(--mono);font-size:12px;letter-spacing:.2em;text-transform:uppercase;pointer-events:none}
  .vz.top{top:0;rotate:180deg;border-bottom:1px solid var(--line)}
  .vz.bot{bottom:0;border-top:1px solid var(--line)}
  .vz b{display:block;font-size:34px;letter-spacing:0;text-align:center}
  .vz.p1{color:var(--p1)}
  .vz.p2{color:var(--p2)}
  .vz .pad-num{position:static;width:84%;pointer-events:auto;grid-template-columns:repeat(5,1fr)}
  .vz .pad-num button{padding:8px 0;font-size:18px}
  .vz .pad-num button.sel{border-color:currentColor}
  .vz.done .pad-num{opacity:.3;pointer-events:none}
  .vmid{position:absolute;left:0;right:0;top:42%;height:16%;display:grid;place-items:center;font-family:var(--mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--mute);text-align:center;line-height:1.7;pointer-events:none}
  .vmid b{color:var(--ink);font-size:16px;letter-spacing:.1em}

/* ==================================================================
   versus — games/_shared/versus.js
   ================================================================== */
  /* versus (v10): one phone, two ends. The top half is turned to face the second player; the middle carries both counts and the lead */
  #vs{display:none;position:absolute;inset:0}
  #game.versus #vs{display:block}
  .vhalf{position:absolute;left:0;right:0;height:38%}
  .vhalf.bot{bottom:calc(2% + env(safe-area-inset-bottom))}
  .vhalf.top{top:calc(2% + env(safe-area-inset-top));rotate:180deg}
  .vhalf .pad{bottom:0;height:min(34vw,20vh)}
  .vhalf .vwho{position:absolute;left:0;right:0;bottom:calc(min(34vw,20vh) + 10px);text-align:center;font-family:var(--mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--mute);pointer-events:none}
  .vhalf .vwho b{display:block;font-size:34px;color:var(--ink);letter-spacing:0;font-variant-numeric:tabular-nums;font-weight:700}
  #vsmid{position:absolute;left:8%;right:8%;top:50%;transform:translateY(-50%);display:flex;flex-direction:column;gap:8px;align-items:center;font-family:var(--mono);font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--mute);pointer-events:none;z-index:3}
  #vsmid .lead{height:8px;width:100%;background:var(--line);position:relative;overflow:hidden}
  #vsmid .lead i{position:absolute;top:0;bottom:0;left:50%;width:0;background:var(--sq-live);transition:left .12s,width .12s}
  #vsmid b{color:var(--ink);font-size:13px;letter-spacing:.14em}
  #vsmid .lead u{position:absolute;top:-2px;bottom:-2px;width:2px;background:var(--cue);left:50%;margin-left:-1px}
  /* versus dots: the field is the whole screen; squares belong to the bottom player, circles to the top one */
  #vfield{position:absolute;inset:0;display:none}
  #game.versus[data-g="dots"] #vfield{display:block}
  #game.versus[data-g="dots"] .vhalf .pad{display:none}
  #game.versus[data-g="dots"] .vhalf{height:14%;pointer-events:none}
  #game.versus[data-g="dots"] .vhalf .vwho{bottom:0}
  .vshape{position:absolute;left:0;top:0;width:var(--dsz,80px);height:var(--dsz,80px);background:var(--sq-live);opacity:0;will-change:transform;transition:opacity .08s}
  .vshape.on{opacity:1}
  .vshape.c{border-radius:50%}
  .vlead{position:absolute;left:0;top:0;width:var(--dsz,80px);height:var(--dsz,80px);border:2px solid var(--cue);background:color-mix(in srgb,var(--cue) 22%,transparent);opacity:0;pointer-events:none;will-change:transform}
  .vlead.c{border-radius:50%}
  .vlead.on{opacity:1}
  #vwin{position:absolute;inset:0;display:none;place-items:center;z-index:6;pointer-events:none}
  #vwin.on{display:grid}
  #vwin div{font-family:var(--title);font-size:clamp(22px,7vw,40px);letter-spacing:.2em;text-transform:uppercase;color:var(--ok);text-align:center;text-shadow:0 0 18px var(--glow);animation:turnin 1.6s ease-out both}
  #vwin div.top{rotate:180deg}
  /* v14 (4.11): Four is four pads a player in versus too — the half makes room for the second row and the count moves up */
  .vhalf .pad.up{display:none}
  #game.versus[data-d="four"] .vhalf .pad{height:min(28vw,15vh)}
  #game.versus[data-d="four"] .vhalf .pad.up{display:block;bottom:calc(min(28vw,15vh) + 8px)}
  #game.versus[data-d="four"] .vhalf .vwho{bottom:calc(2 * min(28vw,15vh) + 18px)}
  /* v14 (4.12): a wrong tap reads the way it does in solo — red over that player's half, not a blank screen */
  .vhalf.miss::after{content:"";position:absolute;inset:0;background:var(--miss);pointer-events:none;animation:vsmiss .5s ease-out both}
  /* v15 (3.11, L4): the lit square in a Quick Tap versus run is the TAPPING player's colour — P1 red at the bottom,
     P2 light blue at the top. Both halves lit the same colour before, which is the one thing L4 exists to prevent.
     This enforces L4, it does not change it: the two values come straight from --p1 / --p2 */
  #game.versus .vhalf.p1 .sq{--sq-live:var(--p1)}
  #game.versus .vhalf.p2 .sq{--sq-live:var(--p2)}
  @keyframes vsmiss{0%{opacity:.34}100%{opacity:0}}
  /* v14 (4.15): the screen leans towards whoever is ahead, and shakes gently once the end is in sight */
  #vslean{position:absolute;inset:0;pointer-events:none;opacity:calc(var(--lk,0) * .55);transition:opacity .3s}
  #vslean.p1{background:radial-gradient(140% 62% at 50% 100%,var(--p1),transparent 72%)}
  #vslean.p2{background:radial-gradient(140% 62% at 50% 0%,var(--p2),transparent 72%)}
  #vs.close{animation:vsclose 1.5s ease-in-out infinite}
  @keyframes vsclose{0%,100%{transform:translate(0)}25%{transform:translate(-2px,1px)}75%{transform:translate(2px,-1px)}}
  @media (prefers-reduced-motion:reduce){#vs.close{animation:none}}

  /* ---------- v20 / v21 (batch 15, build 35): the new-mode green, selection over it, the versus pad ---------- */
  /* D.1: a newly unlocked MODE stays green until a run of it is on record — the same green line L8's first-seen mark draws
     on game select, no new treatment. On its row in the sheet and on its game's tile. */
  .choice.newplay{border-color:var(--ok)}
  .tile.newplay .pic{border-color:var(--ok)}
  /* D.2: SELECTED BEATS GREEN. `.newthing` carries `border-color:…!important`, so the selected line lost to it however the
     classes in fillSheet were ordered — the class list was never the problem, the cascade was. A selected mode wears its
     ordinary selected line; a pressed tile its amber outline with no green border under it. The 170ms green of a mode being
     PICKED (v14 4.6) is the tap's own confirmation and still wins for its 170ms, on every mode alike. */
  /* AMENDED at build 37 (v22 §K): the selected line is --press now, the same amber as the pressed tile — see §K below */
  .choice.sel.newthing,.choice.sel.newplay{border-color:var(--press)!important}
  .choice.sel.picked{border-color:var(--ok)!important}
  .tile.keep.newthing .pic,.tile.keep.newplay .pic{border-color:var(--press)}
  /* F.3: a versus pad answers the finger that hit it — the lit square often moves on to the SAME pad, which changed nothing
     on screen and read as a miss. (guess) .14s. Presentation only (L10) */
  .pad.tapped{animation:padtap .14s ease-out}
  @keyframes padtap{0%{scale:.93;filter:brightness(1.6)}100%{scale:1;filter:none}}
  @media (prefers-reduced-motion:reduce){.pad.tapped{animation:none}}

  /* ---------- v22 §K (build 37): ONE colour for "this is what you chose" ----------
     A pressed game tile wore --press (amber) and the mode chosen under it wore --ink (white) — two colours for one idea.
     The rule: exactly one --press thing on screen at a time, and it is whatever the player is choosing right now. The
     three checks §K asked for, answered: (1) `.grid.dim` IS on for the mode sheet as well as the length sheet —
     setStage() in ui/screens/pick.js toggles it for every stage but the grid (the gate asserts it on a two-mode game and on
     Sequence, which goes straight to its length row); (2) the two !important overrides were decided one by one — a
     first-seen or unplayed mode that is selected takes --press like any other selected mode (above), and `.picked` KEEPS
     --ok: it is the 170ms confirmation of the tap itself (v14 4.6), not a selected state; (3) --press is checked against
     the SHEET's own ground (--panel, and the pass & play sheet's green-tinted one), not the grid's — the gate measures
     the contrast. Everywhere else `.sel` appears is unchanged: this is the game-then-mode pick only. */
  .choice.sel{border-color:var(--press)}
  /* AMENDED at build 38 (Aiden): the tile demotes once a mode is CHOSEN (`.grid.chosen`, set in setStage), not the moment the
     sheet is up — until a mode is tapped the tile is still the live choice and keeps its amber */
  .grid.chosen .tile.keep .pic{outline-color:var(--line)}
  .grid.chosen .tile.keep .name{color:var(--mute)}
  .grid.chosen .tile.keep.newthing .pic,.grid.chosen .tile.keep.newplay .pic{border-color:var(--line)}

  /* ---------- v21 §G / v20 §D (build 37): keys and chests ---------- */
  /* G.2 / D.7: a locked key is CROSSED OUT, not greyed — the locked-mode pattern — with what opens it underneath and no number */
  .kkey b.x{position:relative;color:var(--mute)}
  .kkey u.need{color:var(--ok);opacity:1;text-transform:none;letter-spacing:.04em;max-width:13ch;text-align:center;line-height:1.35}
  /* G.3: a gate on the connector into chest 1 while any mode is still locked, and the one time it animates away */
  .gridlines .glgate rect{fill:var(--ground);stroke:none}
  .gridlines .glgate path{fill:none;stroke:var(--cue);stroke-width:1.6;stroke-linecap:round}
  .gridlines .glgate{transform-box:fill-box;transform-origin:center;animation:gatein .4s ease-out var(--gd,0s) both}
  .gridlines .glgate.off{animation:gateoff .9s ease-in .5s both}
  @keyframes gatein{from{opacity:0}to{opacity:1}}
  @keyframes gateoff{0%{opacity:1;scale:1}40%{opacity:1;scale:1.25}100%{opacity:0;scale:.2}}
  /* D.4: the front number went up since it was last shown — a pulse while it counts */
  .menu-key.up{animation:pctup .9s ease-out}
  @keyframes pctup{0%{scale:1}25%{scale:1.16;color:var(--ok)}100%{scale:1}}
  @media (prefers-reduced-motion:reduce){.gridlines .glgate,.gridlines .glgate.off,.menu-key.up{animation:none}}
