/* ══════════ TEST YOUR MIGHT — the game's own page ══════════
   A fixed 960x540 stage on a dark page, like an arcade cabinet.
   Everything is px, deliberately: the old overlay sized its HUD in vw
   because it WAS the viewport; a fixed box on a wide monitor turns 4vw
   into a 100px glyph in a 540px frame. The box is a constant, so its
   type is too. */

/* ══════════ THE FONTS ARE SELF-HOSTED, AND THAT IS A BUG FIX ══════════

   The page used to pull Fraunces and JetBrains Mono from Google Fonts.
   That link cost a player in Iran the entire game (reported 2026-08-04:
   "stuck at 0%, never fills"), and the mechanism is worth writing down
   because it looks nothing like a font problem:

     · fonts.googleapis.com is filtered there by BLACKHOLING — the
       connection is accepted and then never answered, so the request
       hangs for the browser's full timeout instead of failing fast.
     · a render-blocking stylesheet that has not settled blocks any
       classic <script> from EXECUTING, and ring.html loads sound.js as
       one. The module script that calls boot() runs after it.
     · so boot() never ran, and the gate sat at the literal `0` that is
       hardcoded in the markup. Not a stalled loader — a loader that had
       never been started. Nothing in the game had failed; nothing in the
       game had run.

   Reproduced exactly against a server that accepts and never responds:
   bootRan false, percent "0", caption "lacing up", readyState "loading".
   A fast failure (connection refused) does NOT reproduce it — the
   stylesheet errors immediately and the scripts proceed — which is why
   this never showed up anywhere else.

   Latin subsets only, since the interface is English; JetBrains Mono v24
   is variable, so weights 300 and 400 are the same file. 98 KB for both,
   against a 5.9 MB bundle, and now nothing outside this origin is needed
   to start the game. */
@font-face{
  font-family:"Fraunces";
  font-style:normal;
  font-weight:300 600;
  font-display:swap;
  src:url(../fonts/fraunces-latin.woff2) format("woff2");
}
@font-face{
  font-family:"JetBrains Mono";
  font-style:normal;
  font-weight:300 400;
  font-display:swap;
  src:url(../fonts/jetbrainsmono-latin.woff2) format("woff2");
}

:root{
  --ink:#EDF2F4; --ink-dim:#8D99AE; --hair:#2B2D42;
  --crimson:#FF4D6D; --base:#0B0D10;
  --serif:"Fraunces",serif; --mono:"JetBrains Mono",monospace;
  --fit:1;
}
*{margin:0;padding:0;box-sizing:border-box}
/* THE hidden ATTRIBUTE MUST ALWAYS WIN.

   `hidden` works through the UA stylesheet's `display:none` — which any
   author `display:flex` on the same element silently overrides. That is
   exactly what happened here: `.ring__panel{display:flex}` kept the
   EMPTY leaderboard panel rendered over the whole stage from the first
   frame — its dark tint and backdrop-filter blur made the game look
   frozen and out of focus, and it swallowed every click so the guard
   could never be raised. JS said `panel.hidden === true` the whole time,
   and canvas-only captures never showed the DOM overlay: only the
   user's real screenshot caught it. */
[hidden]{display:none!important}
html,body{height:100%}
body{background:#07090C;color:var(--ink);font-family:var(--mono);
  display:flex;align-items:center;justify-content:center;overflow:hidden}

.back{position:fixed;top:16px;left:18px;font-size:10px;letter-spacing:.18em;
  text-transform:uppercase;color:var(--ink-dim);text-decoration:none;z-index:10}
.back:hover{color:var(--ink)}
.pill{font-family:var(--mono);font-size:9px;letter-spacing:.18em;text-transform:uppercase;
  color:var(--ink-dim);background:rgba(10,5,7,.6);border:1px solid var(--hair);
  border-radius:100px;padding:8px 14px;cursor:pointer;transition:all .3s ease}
.pill:hover{color:var(--ink);border-color:var(--crimson)}
.pill[aria-pressed="true"]{color:var(--crimson);border-color:var(--crimson)}
.snd{position:fixed;top:12px;right:18px;z-index:10}

/* ── the stage ──
   Two nested transforms, two elements, on purpose: the outer .ring
   carries the fit-to-window scale, the inner .ring__shell carries
   sway/shake/kick. One transform per element — sharing one would mean
   the effects and the fit clobbering each other's matrix. */
.stagewrap{display:flex;flex-direction:column;align-items:center;gap:14px}
.ring{position:relative;width:960px;height:540px;background:#000;
  transform:scale(var(--fit));transform-origin:center center;
  border:1px solid #1a1215;border-radius:4px;overflow:hidden}
.ring__shell{position:absolute;inset:0;
  transform:translate(var(--shx,0px),var(--shy,0px)) scale(var(--kick,1))}
.ring__gl{display:block;width:960px;height:540px}
.hint{font-size:9px;letter-spacing:.2em;text-transform:uppercase;color:var(--ink-dim)}

/* ── film grain + vignette ──
   Above the canvas, BELOW the HUD: grain over 9px mono type costs
   legibility. The grain is a tiled noise image stepped at ~12Hz — at
   60Hz it reads as electronic shimmer, at film rates it reads as film.
   It also dithers the JPEG banding a dark backdrop is prone to. */
.ring__grain{position:absolute;inset:-64px;pointer-events:none;z-index:2;opacity:.05;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='128' height='128' filter='url(%23n)' opacity='0.9'/%3E%3C/svg%3E");
  animation:grain .8s steps(8) infinite}
@keyframes grain{
  0%{transform:translate(0,0)}25%{transform:translate(-32px,18px)}
  50%{transform:translate(24px,-28px)}75%{transform:translate(-18px,-12px)}
  100%{transform:translate(0,0)}}
/* THE VIGNETTE CLOSES IN. Both the inner stop and the outer darkness are
   variables so a draining clock can strangle the frame — 58%/.55 at rest
   is exactly v1's look, and the game walks them to 20%/.94 as the last
   seconds go. Cheaper than any post-process and it darkens the HUD's
   corners too, which a shader on the canvas could not reach. */
.ring__vig{position:absolute;inset:0;pointer-events:none;z-index:2;
  background:radial-gradient(115% 100% at 50% 48%,
    transparent var(--vigstop,58%),rgba(4,2,3,var(--vigdark,.55)) 100%)}

/* ── the gate ── */
.ring__gate{position:absolute;inset:0;z-index:5;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:10px;background:#07090C;
  transition:opacity .7s ease}
.ring__gate.gone{opacity:0;pointer-events:none}
.ring__pct{font-family:var(--serif);font-size:34px;line-height:1}
.ring__pct i{font-family:var(--mono);font-size:10px;color:var(--ink-dim);font-style:normal}
.ring__bar{width:220px;height:2px;background:var(--hair);overflow:hidden}
.ring__bar span{display:block;height:100%;width:0;background:var(--crimson);
  transition:width .25s linear}
.ring__cap{font-size:8px;letter-spacing:.2em;text-transform:uppercase;color:var(--ink-dim)}

/* ── HUD ── */
.ring__hud{position:absolute;inset:0;z-index:3;pointer-events:none}
.ring__strikes{position:absolute;top:14px;left:16px;display:flex;gap:6px}
.ring__strikes i{width:10px;height:10px;border-radius:50%;border:1px solid var(--hair);
  transition:all .3s ease}
.ring__strikes i.on{background:var(--crimson);border-color:var(--crimson);
  box-shadow:0 0 14px rgba(255,77,109,.6)}
/* same reasoning as the sequence chip — a 9px tracked label over brick is
   unreadable without its own substrate */
.ring__level{position:absolute;top:12px;left:50%;transform:translateX(-50%);
  font-size:9px;letter-spacing:.2em;text-transform:uppercase;color:var(--ink);
  padding:6px 14px;border-radius:999px;
  background:rgba(6,4,6,.6);
  -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
  border:1px solid rgba(237,242,244,.10);white-space:nowrap}
.ring__pause{pointer-events:auto;position:absolute;top:10px;right:12px}
/* ── the sequence ──
   THE PROBLEM: thin light glyphs sitting directly on a photograph of a
   brick gym. The backdrop has detail and contrast at exactly the scale
   of the type, so the code disappears into the wall behind it — and the
   code is the one thing the player MUST read.

   The fix is a backing plate, not a brighter colour: raising contrast
   against an unpredictable background is a losing game, because the
   background changes as the bag swings. A dark, blurred, shrink-wrapped
   chip gives every glyph the SAME substrate no matter what is behind it,
   which is why every game HUD that has to survive arbitrary scenery ends
   up here. :empty keeps it from showing as a bare lozenge between
   sequences. */
/* 92px, not the old 64: the chip is OPAQUE now, and the clock arc's box
   reaches 84px up from the bottom. At 64 the plate sat on top of the
   arc's apex and hid the very part of it that shows time running out. */
.ring__code{position:absolute;left:50%;transform:translateX(-50%);bottom:92px;
  display:flex;align-items:center;gap:14px;
  padding:9px 20px;border-radius:999px;
  background:rgba(6,4,6,.66);
  -webkit-backdrop-filter:blur(12px) saturate(.75);
  backdrop-filter:blur(12px) saturate(.75);
  border:1px solid rgba(237,242,244,.12);
  box-shadow:0 10px 34px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05)}
.ring__code:empty{display:none}
.ring__code b{font-size:30px;line-height:1;color:var(--ink-dim);opacity:.4;
  transition:all .22s ease;text-shadow:0 2px 6px rgba(0,0,0,.8)}
/* the punch you are ON: full white, larger, and lit from behind so it
   separates from its own neighbours as well as from the room */
.ring__code b.now{opacity:1;color:#fff;transform:scale(1.18);
  text-shadow:0 0 16px rgba(255,255,255,.55),0 2px 6px rgba(0,0,0,.9)}
.ring__code b.done{opacity:1;color:var(--crimson);
  text-shadow:0 0 18px rgba(255,77,109,.6),0 2px 6px rgba(0,0,0,.8)}
/* ── THE COMBO STATE ──
   The finisher sequence is running. The user chose this over a hit
   counter or a title card: no new element, no number to read, the strip
   the player is ALREADY looking at simply changes register the moment the
   first combo punch lands.

   The plate goes from smoked glass to a hot crimson-rimmed slab and the
   glyphs come up out of dim — it stops looking like a HUD chip and starts
   looking like part of the fight. Transitioned rather than switched so it
   arrives with the beat's freeze instead of popping a frame before it. */
.ring__code{transition:background .18s ease,border-color .18s ease,box-shadow .18s ease}
.ring__code.combo{background:rgba(30,2,8,.78);
  border-color:rgba(255,77,109,.55);
  box-shadow:0 10px 34px rgba(0,0,0,.7),0 0 26px rgba(255,77,109,.28),
    inset 0 1px 0 rgba(255,255,255,.06)}
.ring__code.combo b{opacity:.72;color:#f3d9de}
.ring__code.combo b.now{opacity:1;color:#fff;transform:scale(1.22);
  text-shadow:0 0 20px rgba(255,120,145,.75),0 2px 6px rgba(0,0,0,.9)}
/* ── the clock, as a draining arc ──
   A straight rule reads as a progress bar; a curve emptying toward its
   end reads as time. Same footprint as v1's bar so nothing else moved. */
.ring__arc{position:absolute;left:50%;transform:translateX(-50%);bottom:38px;
  width:320px;height:46px;overflow:visible}
.ring__arctrack{fill:none;stroke:var(--hair);stroke-width:2;stroke-linecap:round}
.ring__arcfill{fill:none;stroke:var(--crimson);stroke-width:2.5;stroke-linecap:round;
  transition:stroke .3s ease;
  filter:drop-shadow(0 0 5px rgba(255,77,109,.45))}
/* the last three seconds: hotter, thicker, and pulsing on the beat of
   the corner bell */
.ring__arc.low .ring__arcfill{stroke:#FF2740;stroke-width:3.4;
  filter:drop-shadow(0 0 12px rgba(255,39,64,.9));
  animation:arcpulse .5s ease-in-out infinite}
@keyframes arcpulse{0%,100%{opacity:1}50%{opacity:.45}}
/* the caption carries the countdown and the round announcements, so it
   gets a hard shadow rather than a plate — it has to stay legible while
   sitting over the bag itself, and a chip there would box in the frame */
.ring__say{position:absolute;left:0;right:0;bottom:22px;text-align:center;
  font-size:10px;letter-spacing:.22em;text-transform:uppercase;color:#fff;
  text-shadow:0 0 10px rgba(0,0,0,.95),0 2px 4px rgba(0,0,0,1),0 0 26px rgba(0,0,0,.8)}

/* ── the finisher's grade and matte ──
   THE SCREEN-BLENDED HOT SPOT IS GONE (user, 2026-08-04, pass 3).

   It was a radial bloom pinned to the impact point, and its problem was
   its LIFETIME rather than its look: --gamt rose over the first tenth of
   a second and then sat at full for the entire six-and-a-half-second
   hold, so what the player actually saw was a permanent orange smear
   parked on the glove. The user's note was that the light burst was not
   good, and the reason is that a wash which outlives its cause stops
   reading as light and starts reading as a decal on the lens.

   What replaced it is a real lamp inside the crater at more than double
   the intensity, alive for 0.12 s — "just a short sudden burst, impact
   light". That one lights the leather, the ripple crests and all three
   dust layers, which a CSS overlay could never do.

   The overlay wash stays: it is a GRADE, it is meant to sit under the
   whole shot, and it never claimed to be an event. Driven by --gamt as
   before, which now carries only this. */
/* ── THE FRACTURE ──
   The last beat of a combo, and nothing else. Fracture lines radiating
   from just off-centre, drawn as a handful of hard-edged conic gradients
   — no image, no SVG file, no extra request, and the compositor treats it
   as one layer. `--crack` is 0 or 1; the fade-in is the transition, so
   the break arrives WITH the beat's freeze rather than a frame before it.

   Deliberately faint. It is a crack in the lens, not a broken monitor:
   at full strength this reads as a hairline over the picture, and the
   moment it stops being subtle it stops being cinema and starts being a
   glitch effect. */
/* THE FRACTURE AND THE GRADE ARE NOT DOM ANY MORE.

   Both were full-frame divs with a mix-blend-mode — screen for the
   fracture, overlay for the grade — stacked over the WebGL canvas beside
   the HUD's backdrop-filter. On iOS that stack rendered a solid green
   square at the moment the fracture switched on, which is the only thing
   in this game that is fixed on screen, square, and green.

   They live in the post-composite fragment shader now (see ensurePost),
   which already runs during exactly the moments they are visible. Two
   fewer compositor layers, and both effects are now INSIDE the image:
   tone-mapped with everything else and closed over by the vignette,
   which a sibling element at the same z-index never could be. */

/* THE LETTERBOX MATTE IS GONE. It was two bars driven by `--bar`, and
   `bars` has been hard-false on every fatality shot since the user's call
   that the frame stays 16:9 — so the markup, the CSS and a per-frame
   setProperty were all running to move something that could never appear.
   Removed rather than left dormant. */

/* ── error flash ── */
.ring__flash{position:absolute;inset:0;z-index:4;pointer-events:none;opacity:0;
  background:radial-gradient(120% 100% at 50% 50%,transparent 45%,rgba(255,77,109,.6) 100%)}
.ring__flash.on{animation:ringflash .5s ease}
@keyframes ringflash{0%{opacity:0}18%{opacity:1}100%{opacity:0}}

/* ── the board panel ── */
.ring__panel{position:absolute;inset:0;z-index:6;display:flex;overflow-y:auto;
  background:rgba(6,4,5,.88);backdrop-filter:blur(12px);padding:24px}
.ring__panelwrap{width:520px;max-width:100%;margin:auto}
.ring__medal{width:54px;height:54px;margin-bottom:10px}
.ring__board h3{font-family:var(--serif);font-weight:400;font-size:26px;margin-bottom:12px}
.ring__rows{list-style:none;display:flex;flex-direction:column;gap:4px;margin-bottom:10px}
.ring__rows li{display:grid;grid-template-columns:20px 44px 1fr auto auto;gap:10px;
  align-items:baseline;padding:8px 10px;border:1px solid var(--hair);border-radius:8px;
  font-size:9px;letter-spacing:.1em;color:var(--ink-dim)}
.ring__rows li.won{border-color:rgba(255,77,109,.5)}
.ring__who{color:var(--ink);letter-spacing:.16em}
.ring__got{color:var(--crimson);text-transform:uppercase}
.ring__when{opacity:.6}
.ring__boardnote{font-size:8px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--ink-dim)}
.ring__panelbtns{display:flex;gap:10px;margin-top:16px}

/* ── touch controls ──
   Sized in absolute px, not vw: a thumb is the same size on every phone,
   and 62px is about the smallest target that can be hit reliably while
   the other hand is holding the device. `touch-action:manipulation` and
   the preventDefault in the handler between them stop the double-tap
   zoom that would otherwise fire on a fast combination. */
/* THE PAD IS GONE. Swipe anywhere on the stage drives the game now —
   see bindInput(). A control with a position costs screen area and has
   to be hunted for; a gesture costs neither. Nothing replaces these
   rules: there is no longer any control furniture to style. */

/* ── phones ──
   The stage takes the whole viewport. There is no control furniture left
   to lay out — the game is driven by a swipe anywhere on it — so this
   block now only strips the desktop chrome.

   Keyed on pointer:coarse rather than width: a small window on a laptop
   still wants the desktop layout. */
@media (pointer:coarse){
  body{overflow:hidden}
  .stagewrap{gap:0}
  .hint{display:none}
  .back,.snd{font-size:8px;padding:6px 10px;opacity:.5}

  /* rotate prompt — the game is a 16:9 frame and a portrait phone can
     only ever show it as a stripe */
  .rotate{position:fixed;inset:0;z-index:60;display:none;
    flex-direction:column;align-items:center;justify-content:center;gap:14px;
    background:#07090C;text-align:center;padding:24px}
  .rotate p{font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--ink-dim)}
  .rotate svg{width:54px;height:54px;stroke:var(--crimson);fill:none;stroke-width:1.5}
}
@media (pointer:coarse) and (orientation:portrait){
  .rotate{display:flex}
}

@media (prefers-reduced-motion: reduce){
  .ring__grain{animation:none}
  .ring__shell{transform:none !important}
}
