/* ═══════════════════════════════════════════════════════════════════════════
   激突ドッジ / GEKITOTSU DODGE — UI stylesheet
   Owner: module I (ui). No external fonts, no external images: every surface
   here is gradients, clip-paths and system type.

   The look: a 1988 arcade marquee rebuilt with 2026 tools. Hard black keylines,
   warm gold trim, crimson/cobalt team accents, everything leaning into the
   same italic angle so the screen reads as one shouting object.

   Numbers that BEHAVIOUR depends on are published from core/config.js onto
   :root as --ui-* custom properties (see hud.js/screens.js). Everything below
   that is pure layout and lives here.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─── 0. tokens ─────────────────────────────────────────────────────────── */

:root {
  color-scheme: dark;

  /* Japanese-capable system stack. Ordered so macOS/iOS get Hiragino, Windows
     gets Yu Gothic / Meiryo, Android gets Noto, and nothing ever falls back to
     a serif that would break the arcade voice. */
  --font-jp:
    "Hiragino Kaku Gothic ProN", "Hiragino Sans", "Yu Gothic UI", "YuGothic",
    "Yu Gothic", "Noto Sans JP", "Noto Sans CJK JP", "Meiryo",
    system-ui, -apple-system, "Segoe UI", sans-serif;
  /* Display face for headings: heaviest grotesque available, CJK falls through
     to the gothic above (which we then fake-bold with a text-stroke). */
  --font-disp:
    "Arial Black", "Arial Bold", "Helvetica Neue", "Helvetica",
    "Hiragino Kaku Gothic StdN", var(--font-jp);
  /* Numerals: condensed, tabular, so the clock never jitters. */
  --font-num:
    "DIN Alternate", "DIN Condensed", "Arial Black", "Helvetica Neue",
    Impact, var(--font-jp);

  /* palette */
  --ink:        #07050d;
  --ink-2:      #120c1e;
  --panel:      rgba(15, 11, 27, 0.74);
  --panel-hi:   rgba(46, 36, 72, 0.72);
  --line:       rgba(255, 227, 176, 0.30);
  --cream:      #fff3e0;
  --cream-dim:  #d9cbb8;
  --gold:       #f2c14e;
  --gold-hot:   #ffe6a0;
  --hot:        #ff3b5c;
  --amber:      #ffa32e;
  --good:       #57e08a;
  --dead:       #4a4458;

  /* THE PANELS BREATHE THE SAME AIR AS THE COURT.
     Every plate used to be a fixed near-black (measured min rgb (6,4,16)), so
     on a sunlit 校庭 the HUD was four black holes punched into a daylight
     picture, and during a 必殺 — with the world dropping from luma 111 to 72 and
     going red — the roster panel measured (15.6,10.4,28.4) against its own
     resting (15.4,12.0,31.6): identical to the pixel.

     `--scene-tint` is the hook the lighting owner writes (B: lighting.js
     setMood, via the same :root the HUD publishes its timings onto). Plates mix
     it into their ink, so the readout picks up the hour of the day instead of
     denying it. This value is only the fallback for a build with no lighting. */
  --scene-tint: #5c6ea8;
  /* ...and the tint the HUD swaps to for its own staged beat (必殺 / 昇天). */
  --tint-cinematic: #b3324a;

  /* set from JS (config TEAMS) — these are only the fallbacks */
  --team-a:     #e63946;
  --team-a-lo:  #7d1420;
  --team-b:     #2a6fdb;
  --team-b-lo:  #143a75;

  /* shared geometry of the arcade "banner" shapes */
  --lean:       -9deg;
  --notch:      10px;

  /* behaviour timings — overwritten from config.js at boot */
  --ui-announce-in: 0.30s;
  --ui-announce-out: 0.26s;
  --ui-screen-in: 0.46s;
  --ui-screen-out: 0.28s;
  --ui-press-period: 1.25s;
  --ui-char-stagger: 0.03s;
  --ui-skew: -9deg;
  --ui-cinematic-fade: 0.22s;
  --ui-announce-hold: 1.3s;

  /* ── THE UI CLOCK — TWO MODES, ONE PAIR OF SLOTS ───────────────────────────
     Every looping decoration in this file writes the SAME two longhands:

         animation-delay: var(--ui-anim-delay);
         animation-play-state: var(--ui-anim-state);

     …and `hud.js publishCssVars()` fills those two slots once at boot, from
     `FLAGS.uiScrub` (config.js — the long note there is the primary source):

       ?capture=1  → `calc(0s - var(--ui-t))` / `paused`.  THE SCRUB.
                     `publishClock()` writes `--ui-t` every frame, so the picture
                     at a given `t` is a function of `t`.
                     Why (measured, r4-hit-001): `main.js __stepTo()` ticks the
                     engine in a tight loop for a headless capture, so wall time
                     barely moves while sim time moves by seconds — an `infinite`
                     CSS animation freezes wherever THAT RUN happened to get to.
                     ひじり was correctly at 2 remaining ticks, but the brightness
                     `tickCrit` had reached differed in every capture. §3.4 had
                     already found that trap and fixed it for the announce alone
                     (--ann-t); ten other decorations were left on the browser's
                     clock. The whole file keeps the contract now.

       production  → `0s` / `running`.  THE BROWSER'S OWN CLOCK.
                     No per-frame write to :root at all. Every rule below loops
                     `infinite`, so only the phase ORIGIN moves (t=0 → whenever
                     the element got the animation) and the picture is the same
                     loop. ⚠️ Not cosmetic: the per-frame `:root` write is what
                     killed the owner's iPhone at 40〜53 s (config.js FLAGS.uiScrub
                     carries the measurements). Guarded by `tools/uiclock.mjs`.

     ⚠️ Both longhands are written IMMEDIATELY AFTER the `animation` shorthand,
     inside the same block. Do not "tidy" them into the shorthand: `animation`
     resets play-state to `running` and delay to `0s`, and the capture silently
     stops being reproducible (the picture still moves — it just stops being a
     function of `t`, which is the one thing the gates rely on).
     ⚠️ Do not hard-code `paused` here again. A rule that pauses unconditionally
     is invisible in production (it just stops moving) and re-opens the leak the
     moment somebody re-adds a `--ui-t` publisher to feed it. */
  --ui-t: 0s;
  /* Defaults are the PRODUCTION pair, so the stylesheet is correct on its own
     and a boot that never reaches `publishCssVars()` cannot leave the HUD
     frozen. Capture overwrites both from JS. */
  --ui-anim-delay: 0s;
  --ui-anim-state: running;

  /* decoration periods — all overwritten from config.js UI at boot */
  --ui-score-bump: 0.45s;
  --ui-crit-pulse: 0.5s;
  --ui-cursor-nudge: 0.8s;
  --ui-clock-beat: 0.52s;
  --ui-hurt-pulse: 0.9s;
  --ui-super-flow: 0.9s;
  --ui-super-breathe: 0.7s;
  --ui-streak: 0.42s;
  --ui-sub-in: 0.44s;
  --ui-ray-period: 44s;
  --ui-hit-flash: 0.3s;
  --ui-hit-flash-strong: 0.52s;
  --ui-ref-tick: 0.34s;
  --ui-ref-pop: 0.3s;
  --ui-logo-idle: 3.6s;
  --ui-menu-bob: 0.6s;
  --ui-result-row-in: 0.4s;

  /* safe area, resolved once */
  --sa-t: env(safe-area-inset-top, 0px);
  --sa-r: env(safe-area-inset-right, 0px);
  --sa-b: env(safe-area-inset-bottom, 0px);
  --sa-l: env(safe-area-inset-left, 0px);
}

/* ─── 1. page shell + touch hardening ───────────────────────────────────── */

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

html, body {
  height: 100%;
  margin: 0;
  padding: 0;
  background: var(--ink);
  overflow: hidden;
  overscroll-behavior: none;
  overscroll-behavior-y: none;
}

body {
  font-family: var(--font-jp);
  color: var(--cream);
  line-height: 1.15;
  /* Touch defence, applied before anything else can go wrong: no double-tap
     zoom, no text selection on a long press, no blue tap flash, no rubber-band
     scroll stealing a drag from the virtual stick. */
  touch-action: manipulation;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  font-feature-settings: "palt" 1;   /* proportional CJK metrics: tighter kana */
}

#app {
  position: fixed;
  inset: 0;
  overflow: hidden;
  background:
    radial-gradient(120% 90% at 50% 8%, #1b1430 0%, #0a0713 62%, #05040a 100%);
}

#gl {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  /* The play surface must never scroll or zoom under a swipe. */
  touch-action: none;
}

#ui-root {
  position: absolute;
  inset: 0;
  z-index: 2;
  overflow: hidden;
  pointer-events: none;   /* children opt back in */
  contain: layout paint;
}

/* Headless capture: strip the interactive furniture, keep the readouts. */
body.is-capture .touch-stick,
body.is-capture .touch-pad { display: none !important; }

/* ─── 2. shared arcade primitives ───────────────────────────────────────── */

/* An angled plate with cut corners — the base shape of every HUD panel. */
.plate {
  position: relative;
  /* The dark end of the plate is MIXED WITH THE SCENE (see --scene-tint above),
     so the same panel sits in a noon 校庭 and in a red 必殺 without being the one
     rectangle in the frame that never changes. 82% ink keeps the contrast the
     white names were measured against; the remaining 18% is the daylight. */
  background:
    linear-gradient(160deg, rgba(58, 44, 90, 0.72) 0%,
      color-mix(in srgb, rgba(14, 10, 26, 0.86) 82%, var(--scene-tint)) 58%),
    color-mix(in srgb, var(--panel) 82%, var(--scene-tint));
  border: 1px solid var(--line);
  clip-path: polygon(
    var(--notch) 0, 100% 0, 100% calc(100% - var(--notch)),
    calc(100% - var(--notch)) 100%, 0 100%, 0 var(--notch));
  box-shadow:
    0 2px 0 rgba(0, 0, 0, 0.55),
    0 10px 26px rgba(0, 0, 0, 0.45),
    inset 0 1px 0 rgba(255, 245, 220, 0.16);
  backdrop-filter: blur(7px) saturate(1.15);
  -webkit-backdrop-filter: blur(7px) saturate(1.15);
}

/* Fine diagonal weave — keeps large dark panels from reading as flat plastic. */
.plate::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.5;
  background:
    repeating-linear-gradient(115deg,
      rgba(255, 255, 255, 0.045) 0 1px,
      rgba(255, 255, 255, 0) 1px 5px);
  mix-blend-mode: overlay;
}

/* Heavy display text with a hard keyline. Used by every shouted string. */
.shout {
  font-family: var(--font-disp);
  font-weight: 900;
  letter-spacing: 0.02em;
  transform: skewX(var(--lean));
  text-transform: uppercase;
  -webkit-text-stroke: 0.055em #150812;
  paint-order: stroke fill;
  text-shadow:
    0 0.055em 0 #2a0f1c,
    0 0.10em 0.16em rgba(0, 0, 0, 0.6);
}

/* Latin micro-labels: wide-tracked, dim, never competing with the numbers. */
.tag {
  font-family: var(--font-disp);
  font-weight: 900;
  font-size: 0.60rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--cream-dim);
  opacity: 0.72;
}

.num {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
  font-weight: 900;
}

/* ═══════════════════════════════════════════════════════════════════════════
   3. HUD
   Layout law: the top band and the bottom-left card are the only inhabited
   zones. The middle 60% of the screen stays empty so the match owns it.
   ═══════════════════════════════════════════════════════════════════════════ */

.hud {
  position: absolute;
  inset: 0;
  pointer-events: none;
  font-size: clamp(13px, 1.05vw, 17px);
  opacity: 0;
  transition: opacity 0.35s ease;
}
.hud.is-live { opacity: 1; }

/* ── THE READOUT STEPS BACK FOR THE TWO STAGED BEATS ────────────────────────
   必殺 と 昇天. Measured before this rule existed: on r3-super-005.png the world
   dropped from luma 111 to 72 and went red under the radial blur, while the
   roster panel read (15.6,10.4,28.4) against its own resting (15.4,12.0,31.6) —
   i.e. the loudest frame in the game carried four opaque black boards that had
   not moved a single value. The HUD was not merely failing to join the
   performance, it was flattening it.

   hud.js holds `is-cinematic` for UI.cinematicHold and drops it from its own dt
   (never the wall clock, so a capture at a given `t` is deterministic). The
   readout retreats — dimmer, desaturated, pushed a few px out of frame — and
   the plates swap their scene tint for the beat's own colour. */
.hud__top, .pc {
  transition:
    opacity var(--ui-cinematic-fade) ease,
    filter var(--ui-cinematic-fade) ease,
    transform var(--ui-cinematic-fade) cubic-bezier(.2, .9, .3, 1);
}
.hud.is-cinematic {
  --scene-tint: var(--tint-cinematic);
}
.hud.is-cinematic .hud__top {
  opacity: 0.35;
  filter: saturate(0.4);
  transform: translateY(-6px);
}
.hud.is-cinematic .pc {
  opacity: 0.35;
  filter: saturate(0.4);
  transform: translateY(6px);
}

/* ─── 3.1 top band — the roster board ───────────────────────────────────── */
/*
   原作の骨格（refs/ref-01.png / refs/ref-03.png の実測）:
   画面最上部、左が 1P・右が CPU。各チーム「内野3人の名前が縦に積まれ、その横に
   セグメント式のゲージ」。連続バーは使わない — ゲージは数えられる目盛の集合で、
   1目盛 = COMBAT.gaugeUnit、最大10目盛（りゅうじ）。
   B 側は行を左右反転させてあるので、両チームのゲージが画面中央へ向かって減る。
*/

.hud__top {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  padding:
    calc(var(--sa-t) + 9px)
    calc(var(--sa-r) + 18px)
    7px
    calc(var(--sa-l) + 18px);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: start;
  gap: clamp(8px, 1.6vw, 30px);
  /* NO BACKGROUND HERE. 統合の裁定 (INTEGRATION-NOTES.md 冒頭, 2026-07-27):
     「全幅の暗いグラデ帯をやめる。左パネル・中央タイマー・右パネルをそれぞれ独立した
     浮遊パネルにし、背景は各パネルの直下だけに敷く。パネル間から空と富士山が見えること」

     原作 (refs/ref-03.png) IS an opaque black bar with the world starting below
     it, and this file used to argue that point back at the ruling. Measured, the
     argument lost: with the band + its ::after fade, the gaps between the panels
     (x=340–700 and x=900–1260 at 1600×900) read 10–27 luma all the way down to
     y≈176 = 19.6% of the frame. C spent a whole pass raising the sky to 20% and
     every pixel of it was behind this rectangle. Ported straight, the bar kills
     the very view it is supposed to sit under.

     The legibility that the bar was defending now comes from each panel carrying
     its own opaque plate at `width: max-content` — the ink is backed exactly
     where the ink is, and nowhere else. Sky and Mt. Fuji come through the gaps. */
}

/* side ─────────────────────────────────────────────────────────────────── */

.side {
  --accent: var(--team-a);
  --accent-lo: var(--team-a-lo);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  min-width: 0;
}
.side--b {
  --accent: var(--team-b);
  --accent-lo: var(--team-b-lo);
  align-items: flex-end;
}

.side__head {
  display: flex;
  align-items: center;
  gap: clamp(5px, 0.7vw, 11px);
  padding: 3px 12px 3px 5px;
  --notch: 8px;
  width: max-content;
  max-width: 100%;
}
.side--b .side__head { flex-direction: row-reverse; padding: 3px 5px 3px 12px; }

/* Team colour bleeds in from the outside edge of its own panel. */
.side__head::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--accent) 52%, transparent) 0%,
    transparent 64%);
}
.side--b .side__head::after {
  background: linear-gradient(270deg,
    color-mix(in srgb, var(--accent) 52%, transparent) 0%,
    transparent 64%);
}

/* The FC prints a cyan "1" over the human squad and a cyan "C" over the CPU's.
   That chip is the single loudest piece of orientation on the board. */
.side__badge {
  position: relative;
  z-index: 1;
  flex: 0 0 auto;
  font-family: var(--font-num);
  font-weight: 900;
  font-size: clamp(0.62rem, 0.92vw, 0.9rem);
  line-height: 1;
  letter-spacing: 0.04em;
  padding: 4px 5px 5px;
  color: #04141b;
  background: linear-gradient(180deg, #c8fdff 0%, #6fe6f5 44%, #22bcd8 100%);
  border: 1px solid #05222b;
  border-radius: 2px;
  transform: skewX(var(--lean));
  box-shadow: 0 0 0.75em rgba(70, 224, 248, 0.55), 0 2px 0 #04202a;
}

.side__score {
  position: relative;
  z-index: 1;
  min-width: 1.5em;
  text-align: center;
  font-size: clamp(1.35rem, 2.2vw, 2.15rem);
  line-height: 0.94;
  color: #fff;
  transform: skewX(var(--lean));
  text-shadow:
    0 0 0.5em color-mix(in srgb, var(--accent) 80%, transparent),
    0 0.05em 0 #1a0810,
    0 0.09em 0.14em rgba(0, 0, 0, 0.7);
}
.side__score.is-bump { animation: scoreBump var(--ui-score-bump) cubic-bezier(.2,1.7,.4,1); }
@keyframes scoreBump {
  0%   { transform: skewX(var(--lean)) scale(1.9); filter: brightness(2.4); }
  55%  { transform: skewX(var(--lean)) scale(0.94); }
  100% { transform: skewX(var(--lean)) scale(1); filter: brightness(1); }
}

.side__names { position: relative; z-index: 1; min-width: 0; }
.side--b .side__names { text-align: right; }

.side__jp {
  font-size: clamp(0.78rem, 1.1vw, 1.02rem);
  font-weight: 800;
  letter-spacing: 0.06em;
  white-space: nowrap;
  color: var(--cream);
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.7);
}
.side__en {
  font-size: clamp(0.48rem, 0.64vw, 0.64rem);
  letter-spacing: 0.2em;
  white-space: nowrap;
  color: color-mix(in srgb, var(--accent) 62%, var(--cream));
  opacity: 0.9;
}

/* "内野 3" — the number that decides the match. */
.side__alive {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: baseline;
  gap: 3px;
  padding: 2px 7px;
  border: 1px solid color-mix(in srgb, var(--accent) 60%, transparent);
  border-radius: 3px;
  background: rgba(0, 0, 0, 0.42);
  white-space: nowrap;
}
.side__alive-label { font-size: 0.62em; letter-spacing: 0.1em; color: var(--cream-dim); }
.side__alive-n {
  font-size: 1.12em;
  color: var(--gold-hot);
  text-shadow: 0 0 0.6em rgba(255, 200, 90, 0.6);
}
.side__alive.is-low .side__alive-n { color: var(--hot); text-shadow: 0 0 0.7em rgba(255, 60, 90, 0.8); }

/* the board itself: three infield names, stacked ───────────────────────── */

/* The board is now a FLOATING PANEL, not a stripe on a band. `max-content` is
   the whole point: it ends where the last tick ends, so the column's remaining
   width is sky. Opacity carries the contrast the band used to carry — 0.90 over
   the brightest snow on Mt. Fuji still measures >10:1 for the white names. */
.side__board {
  position: relative;
  width: max-content;
  max-width: 100%;
  display: flex;
  flex-direction: column;
  gap: clamp(1px, 0.25vh, 4px);
  padding: 4px 11px 5px 7px;
  --notch: 9px;
  /* Same scene mix as .plate — this panel is the largest dark object in the
     frame and it was the flattest. */
  background:
    linear-gradient(163deg, rgba(52, 40, 82, 0.60) 0%,
      color-mix(in srgb, rgba(7, 4, 16, 0.92) 82%, var(--scene-tint)) 62%),
    color-mix(in srgb, rgba(7, 4, 16, 0.88) 82%, var(--scene-tint));
  border: 1px solid var(--line);
  border-top: 0;
  clip-path: polygon(
    0 0, 100% 0, 100% calc(100% - var(--notch)),
    calc(100% - var(--notch)) 100%, 0 100%);
  box-shadow: 0 2px 0 rgba(0, 0, 0, 0.5), 0 10px 24px rgba(0, 0, 0, 0.5);
}
.side--b .side__board {
  padding: 4px 7px 5px 11px;
  align-items: flex-end;
  clip-path: polygon(
    0 0, 100% 0, 100% 100%, var(--notch) 100%, 0 calc(100% - var(--notch)));
}

.row {
  --gutter: clamp(9px, 0.72vw, 13px);
  position: relative;
  display: flex;
  align-items: center;
  gap: clamp(5px, 0.66vw, 12px);
  padding: 1px 3px;
  transition: opacity 0.3s ease, filter 0.3s ease;
}
.side--b .row { flex-direction: row-reverse; }

/* Names are HIRAGANA, because the FC could not print katakana and that
   limitation became the series' face. The "pixel" weight is faked with a thick
   black keyline plus a hard, blur-free step shadow — no webfont is allowed
   here, and a soft drop shadow would read as 2004 web, not 1988 arcade. */
.row__name {
  flex: 0 0 auto;
  width: 5.4em;
  text-align: left;
  font-size: clamp(0.88rem, 1.28vw, 1.26rem);
  font-weight: 900;
  line-height: 1.05;
  letter-spacing: 0.015em;
  white-space: nowrap;
  color: #f2f7ff;
  -webkit-text-stroke: 0.14em #08050e;
  paint-order: stroke fill;
  text-shadow: 0.06em 0.08em 0 #08050e;
}
.side--b .row__name { text-align: right; }

/* segmented gauge ──────────────────────────────────────────────────────── */

.gauge {
  display: flex;
  align-items: center;
  gap: 3px;
  flex: 0 0 auto;
}
.side--b .gauge { flex-direction: row-reverse; }

.tick {
  --tick-w: clamp(7px, 0.62vw, 11px);
  --tick-h: clamp(15px, 1.5vw, 24px);
  display: block;
  width: var(--tick-w);
  height: var(--tick-h);
  border-radius: 1px;
  transform: skewX(var(--lean));
  /* AN EMPTY SLOT HAS TO BE COUNTABLE. It used to be rgba(5,3,11,0.52) inside a
     near-black border, which measured RGB (6.7,7.0,16.1) against a panel of
     (7.6,7.2,19.4) — an amplitude of 7/255, about 1.03:1. The gauge was legible
     only on its lit side, so a FULL outfielder (5 of 5) and a HALF infielder
     (5 of 10) drew the identical row of lights. The denominator is half of what
     the original's board says, so the empty slot is now a faint slot in the
     team's own colour: the row is always ten (or five) objects long, and you
     can read what has been taken as well as what is left. */
  background: color-mix(in srgb, var(--accent) 12%, rgba(0, 0, 0, 0.55));
  border: 1px solid color-mix(in srgb, var(--accent) 38%, #000);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.55);
}
.tick.is-lit {
  /* A LIT tick keeps the hard black keyline — that is the arcade reading, and it
     is also what separates "full" from "empty" now that empty carries colour. */
  border-color: rgba(0, 0, 0, 0.88);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent) 10%, #ffffff) 0%,
    color-mix(in srgb, var(--accent) 68%, #ffffff) 30%,
    var(--accent) 74%,
    color-mix(in srgb, var(--accent) 55%, var(--accent-lo)) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.72),
    inset 0 -1px 0 rgba(0, 0, 0, 0.4),
    0 0 0.45em color-mix(in srgb, var(--accent) 50%, transparent);
}
/* A lost tick burns before its slot goes dark. `--k` is written from JS and
   runs 1 → 0 over UI.gauge.tickFlash, so a headless capture is deterministic
   instead of depending on where a CSS animation happened to be. */
.tick.is-dying {
  background: linear-gradient(180deg, #ffffff 0%, #ffd0d8 34%, #ff3b5c 100%);
  border-color: rgba(255, 190, 200, 0.8);
  opacity: calc(0.2 + 0.8 * var(--k, 0));
  transform: skewX(var(--lean)) scaleY(calc(1 + 0.4 * var(--k, 0)));
  box-shadow: 0 0 calc(1.1em * var(--k, 0)) rgba(255, 80, 110, calc(0.95 * var(--k, 0)));
}

/* A critical row's remaining ticks breathe. SCRUBBED (see --ui-t in §0): a
   headless capture used to catch this at an arbitrary brightness, so "2 ticks
   left" looked like a different amount of danger in every shot of the same
   frame. */
.row.is-critical .tick.is-lit {
  animation: tickCrit var(--ui-crit-pulse) ease-in-out infinite alternate;
  animation-delay: var(--ui-anim-delay);
  animation-play-state: var(--ui-anim-state);
}
@keyframes tickCrit { from { filter: brightness(1); } to { filter: brightness(2.1) saturate(1.5); } }
.row.is-critical .row__name { color: #ffd9e0; }

/* struck ───────────────────────────────────────────────────────────────── */

/* Struck rows travel INWARD first (left board to the right, right board to the
   left), which is the same direction as .side.is-hit's nudge and as the card's
   pcHit. Three objects reacting to one hit must not disagree about which way
   the blow came from. */
.row.is-hit { animation: rowShakeA var(--ui-hit-flash) cubic-bezier(.3,.9,.3,1); }
.side--b .row.is-hit { animation-name: rowShakeB; }
@keyframes rowShakeA {
  0%, 100% { transform: translateX(0); }
  22% { transform: translateX(6px); }
  55% { transform: translateX(-5px); }
  80% { transform: translateX(2px); }
}
@keyframes rowShakeB {
  0%, 100% { transform: translateX(0); }
  22% { transform: translateX(-6px); }
  55% { transform: translateX(5px); }
  80% { transform: translateX(-2px); }
}

/* ── 強打 ───────────────────────────────────────────────────────────────────
   A 6-damage graze and a 必殺の直撃 used to play the identical shake, because
   onHit read neither `damage` nor `isSuper` off EV.HIT — the payload carried
   both the whole time. The readout's only answer to a finisher was that more
   ticks went dark, which is 「数が減った」, not 「効いた」 (SPEC §5.5).

   Twice the amplitude, and the return OVERSHOOTS once instead of settling
   straight — a heavy blow takes longer to stop moving, and that difference is
   the whole reading. The threshold is COMBAT.dmgStrong, the same one the 3D
   launch uses to promote a knockback (CHAR.knockback.strongDamage). */
.row.is-hit.is-hit-strong {
  animation-name: rowShakeStrongA;
  animation-duration: var(--ui-hit-flash-strong);
}
.side--b .row.is-hit.is-hit-strong { animation-name: rowShakeStrongB; }
@keyframes rowShakeStrongA {
  0%   { transform: translateX(14px) skewX(-3deg); filter: brightness(2.2); }
  12%  { filter: brightness(1); }
  30%  { transform: translateX(-9px) skewX(2deg); }
  52%  { transform: translateX(6px) skewX(-1deg); }
  72%  { transform: translateX(-4px); }
  88%  { transform: translateX(2px); }
  100% { transform: translateX(0); filter: brightness(1); }
}
@keyframes rowShakeStrongB {
  0%   { transform: translateX(-14px) skewX(3deg); filter: brightness(2.2); }
  12%  { filter: brightness(1); }
  30%  { transform: translateX(9px) skewX(-2deg); }
  52%  { transform: translateX(-6px) skewX(1deg); }
  72%  { transform: translateX(4px); }
  88%  { transform: translateX(-2px); }
  100% { transform: translateX(0); filter: brightness(1); }
}

/* knocked out — 原作では HP0 で退場（外野に落ちない） ──────────────────── */

/* A strike across the whole row read as a laser through the row ABOVE it, so
   the mark is a single hard ✕ sitting in the cursor's slot instead — the row is
   already dimmed and its gauge is already empty, this only has to say "out". */
.row__x {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: var(--gutter);
  display: none;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.side--b .row__x { left: auto; right: 0; }
.row__x::before {
  content: "×";
  font-size: 1.25em;
  font-weight: 900;
  line-height: 1;
  color: #ff4d68;
  -webkit-text-stroke: 0.09em #2a0009;
  paint-order: stroke fill;
  text-shadow: 0 0 0.55em rgba(255, 70, 100, 0.9);
}

.row.is-ko .row__name,
.row.is-ko .gauge {
  filter: grayscale(0.9) brightness(0.5);
  opacity: 0.6;
}
.row.is-ko .row__x { display: flex; animation: koStamp var(--ui-ko-flash) cubic-bezier(.15,1.6,.4,1) both; }
.row.is-ko .row__cursor { opacity: 0; }
@keyframes koStamp {
  0%   { opacity: 0; transform: scale(2.4) rotate(-10deg); }
  60%  { opacity: 1; transform: scale(0.94); }
  100% { opacity: 1; transform: scale(1) rotate(0); }
}

/* the player you are driving ───────────────────────────────────────────── */

.row__cursor {
  flex: 0 0 auto;
  width: var(--gutter);
  height: 0.66em;
  background: var(--gold-hot);
  clip-path: polygon(6% 0, 100% 50%, 6% 100%);
  filter: drop-shadow(0 0 4px rgba(255, 210, 110, 0.95));
  opacity: 0;
  /* NO transition. The cursor and the gold name are one statement — "this is the
     body you are driving" — and the name colour is not animatable state, it just
     switches. A 0.18s fade on the arrow alone showed the OLD row's arrow next to
     the NEW row's gold name for a fifth of a second, i.e. two cursors on screen.
     Possession changes every few seconds in dodgeball, so that was constant.
     Instant is also the arcade reading: the marker snaps, it does not dissolve. */
}
.side--b .row__cursor { clip-path: polygon(94% 0, 0 50%, 94% 100%); }
.row.is-active .row__cursor {
  opacity: 1;
  animation: cursorNudge var(--ui-cursor-nudge) ease-in-out infinite alternate;
  animation-delay: var(--ui-anim-delay);   /* §0: capture scrubs, production runs */
  animation-play-state: var(--ui-anim-state);
}
.side--b .row.is-active .row__cursor { animation-name: cursorNudgeB; }
.row.is-active .row__name { color: var(--gold-hot); }
.row.is-ko.is-active .row__cursor { opacity: 0; }
@keyframes cursorNudge  { from { transform: translateX(0); } to { transform: translateX(4px); } }
@keyframes cursorNudgeB { from { transform: translateX(0); } to { transform: translateX(-4px); } }

/* bench — the outfield trio. Subordinate on purpose: the original does not put
   them on the board at all, but ours can be hurt out there. */

/* Third floating panel. This one used to stretch the full column width and WAS
   the visual "band" the ruling was actually complaining about — a 1600px dark
   bar to back three 13.8px names. `max-content` shrinks it onto its own type.
   「たもつ」 measured 1.80:1 on Mt. Fuji's snow under the old weak scrim, so the
   subordination comes from SIZE and from the dashed 外野 tag, never from alpha. */
.side__benchwrap {
  position: relative;
  width: max-content;
  /* ...BUT NOT INTO THE MOUNTAIN. `max-content` alone let this strip grow to
     three full names plus three ten-slot gauges: measured on r4-match-000 it ran
     x=10..520 at y=148..178, straight across 富士山's left slope and the bottom of
     its snow line. C spent a whole pass opening the top 34% of the frame for sky
     and the HUD was clawing part of it back — for a gauge whose ticks are 4.4px
     wide at 1600px and cannot be counted anyway. 24vw is where the strip stops
     before the mountain's shoulder at this camera. */
  max-width: min(24vw, 100%);
  display: flex;
  align-items: center;
  gap: clamp(4px, 0.6vw, 9px);
  padding: 3px 10px 4px 9px;
  margin-top: 4px;
  --notch: 8px;
  background:
    linear-gradient(163deg, rgba(48, 36, 76, 0.52) 0%,
      color-mix(in srgb, rgba(7, 4, 16, 0.90) 82%, var(--scene-tint)) 62%),
    color-mix(in srgb, rgba(7, 4, 16, 0.86) 82%, var(--scene-tint));
  border: 1px solid var(--line);
  clip-path: polygon(
    0 0, 100% 0, 100% calc(100% - var(--notch)),
    calc(100% - var(--notch)) 100%, 0 100%, 0 var(--notch));
  box-shadow: 0 2px 0 rgba(0, 0, 0, 0.5), 0 8px 20px rgba(0, 0, 0, 0.5);
}
.side--b .side__benchwrap {
  flex-direction: row-reverse;
  padding: 3px 9px 4px 10px;
  clip-path: polygon(
    0 0, 100% 0, 100% 100%, var(--notch) 100%, 0 calc(100% - var(--notch)));
}

.side__benchlabel {
  flex: 0 0 auto;
  font-size: clamp(0.48rem, 0.62vw, 0.62rem);
  letter-spacing: 0.14em;
  color: var(--cream-dim);
  padding: 1px 4px 2px;
  border: 1px dashed rgba(255, 235, 200, 0.3);
  border-radius: 2px;
}

.side__bench { display: flex; gap: clamp(5px, 0.8vw, 14px); }
.side--b .side__bench { flex-direction: row-reverse; }

/* NAME OVER GAUGE, NOT NAME BESIDE GAUGE. Laid out side by side, one bench
   entry costs (name 55px + gap + ten ticks 63px) ≈ 122px, and three of them plus
   the 外野 tag ran the strip out to x=520 — across 富士山. Stacked, an entry is
   just max(name, gauge) ≈ 63px, so the same three names and the same thirty
   ticks fit in roughly half the width and the mountain keeps its slope. The
   denominator is untouched: still ten ticks, still 1目盛 = COMBAT.gaugeUnit,
   still the same object the board and the card draw. */
.row--bench {
  display: grid;
  grid-template-columns: var(--gutter) max-content;
  gap: 1px 3px;
  padding: 0;
  align-items: center;
}
/* The ▶ stays ON the name (an outfielder can be the character you are driving —
   r4-match-000 shows ▶さとる out there), and the gauge takes the line below,
   spanning the cursor gutter so it starts on the same edge as the arrow. */
.row--bench .row__cursor { grid-area: 1 / 1; }
.row--bench .row__name   { grid-area: 1 / 2; }
.row--bench .gauge       { grid-area: 2 / 1 / 3 / 3; }
.side--b .row--bench { grid-template-columns: max-content var(--gutter); }
.side--b .row--bench .row__cursor { grid-area: 1 / 2; }
.side--b .row--bench .row__name   { grid-area: 1 / 1; }
/* The smallest type in the game, and it lives on the brightest part of the
   picture. It gets a heavier keyline than the board names (0.18em vs 0.14em) —
   `paint-order: stroke fill` puts the ink BEHIND the glyph, so the counters of
   ひらがな stay open while the silhouette thickens. */
.row--bench .row__name {
  width: auto;
  font-size: clamp(0.7rem, 0.9vw, 0.9rem);
  -webkit-text-stroke: 0.18em #08050e;
  text-shadow: 0.05em 0.07em 0 #08050e;
}
.row--bench { --gutter: clamp(7px, 0.55vw, 10px); }
.row--bench .row__x::before { font-size: 1.05em; }
.row--bench .row__x::before, .row--bench .row__x::after { height: 2px; }
/* 2px is a FLOOR, not a taste: below it the ticks fuse into a red smear and the
   row stops being countable, which is the whole point of a segmented gauge. */
.gauge--bench { gap: 2px; }
/* SAME COUNT, LOWER TICK. The bench used to draw 5 slots for the board's 10, so
   1目盛 meant HP8 down here and HP4 thirty pixels up there — two scales for one
   quantity inside one HUD (SPEC §0.5 #5 / art-direction §5). It is now the same
   ten objects, subordinated by HEIGHT: a short row reads as a lesser row without
   lying about the arithmetic. Measured at 1600px: 4.4px wide × 9.6px tall with a
   2px gap, i.e. every tick has its own separated body. */
.gauge--bench .tick {
  --tick-w: clamp(3px, 0.28vw, 5px);
  --tick-h: clamp(7px, 0.60vw, 10px);
}
/* An unlit bench tick in the team's own dark colour measured luma 22 against a
   panel of 15 on the blue side — 4/5 and 5/5 were the same picture. The empty
   slot now carries its own hairline so the gauge is countable from BOTH ends. */
.gauge--bench .tick:not(.is-lit) {
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.14),
    inset 0 1px 2px rgba(0, 0, 0, 0.55);
}

/* centre column ────────────────────────────────────────────────────────── */

.mid {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}

/* The referee's count. 原作: ボールを持ち続けると審判がカウントして相手ボール。
   Hidden until the count actually starts, so the centre of the frame is empty
   for all of the time nobody is stalling. */
.ref {
  position: relative;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 2px 11px 3px;
  border: 1px solid rgba(255, 220, 160, 0.35);
  border-radius: 2px;
  background: rgba(9, 5, 17, 0.74);
  opacity: 0;
  transform: translateY(-7px) scale(0.9);
  transition: opacity 0.16s ease, transform 0.16s ease;
}
.ref.is-on { opacity: 1; transform: none; }
.ref[data-side="a"] { box-shadow: 0 0 0.9em color-mix(in srgb, var(--team-a) 55%, transparent); }
.ref[data-side="b"] { box-shadow: 0 0 0.9em color-mix(in srgb, var(--team-b) 55%, transparent); }

.ref__label {
  font-size: clamp(0.58rem, 0.78vw, 0.8rem);
  font-weight: 900;
  letter-spacing: 0.12em;
  color: var(--gold);
  text-shadow: 0 1px 0 #000;
}
.ref__count {
  min-width: 1ch;
  text-align: center;
  font-size: clamp(1.3rem, 2vw, 2rem);
  font-weight: 900;
  line-height: 1;
  color: var(--cream);
  transform: skewX(var(--lean));
  -webkit-text-stroke: 0.045em #150812;
  paint-order: stroke fill;
  text-shadow: 0 0.05em 0 #1a0810, 0 0 0.55em rgba(255, 226, 170, 0.4);
}
.ref.is-urgent { border-color: rgba(255, 70, 100, 0.8); background: rgba(38, 4, 14, 0.8); }
.ref.is-urgent .ref__count { color: var(--hot); }
.ref__count.is-tick { animation: refTick var(--ui-ref-tick) cubic-bezier(.2,1.6,.4,1); }
@keyframes refTick {
  0%   { transform: skewX(var(--lean)) scale(1.85); filter: brightness(2.2); }
  60%  { transform: skewX(var(--lean)) scale(0.95); }
  100% { transform: skewX(var(--lean)) scale(1); filter: brightness(1); }
}
.ref.is-pop { animation: refPop var(--ui-ref-pop) cubic-bezier(.2,1.5,.4,1); }
@keyframes refPop {
  0%   { transform: translateY(-9px) scale(0.82); }
  100% { transform: none; }
}

/* ─── 3.2 clock ─────────────────────────────────────────────────────────── */

.clock {
  position: relative;
  padding: 4px clamp(12px, 1.8vw, 26px) 6px;
  --notch: 13px;
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: clamp(96px, 11vw, 156px);
}

.clock__time {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  font-weight: 900;
  font-size: clamp(1.9rem, 3.5vw, 3.3rem);
  line-height: 0.9;
  letter-spacing: 0.01em;
  color: var(--cream);
  transform: skewX(var(--lean));
  -webkit-text-stroke: 0.045em #150812;
  paint-order: stroke fill;
  text-shadow:
    0 0.05em 0 #2a0f1c,
    0 0.10em 0.18em rgba(0, 0, 0, 0.75),
    0 0 0.7em rgba(255, 226, 170, 0.28);
}
.clock__label {
  margin-top: 2px;
  font-family: var(--font-disp);
  font-weight: 900;
  font-size: clamp(0.44rem, 0.62vw, 0.62rem);
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  color: var(--gold);
  opacity: 0.85;
}

.clock.is-warn .clock__time { color: var(--amber); text-shadow: 0 0.05em 0 #3a1500, 0 0 0.8em rgba(255, 163, 46, 0.55); }

/* The last ten seconds beat. Scrubbed to --ui-t (§0) — the clock is the one
   readout whose ANIMATION is also information, and a capture that caught it at a
   random phase said nothing reproducible about how urgent the moment was. */
.clock.is-urgent .clock__time {
  color: #fff;
  animation: clockBeat var(--ui-clock-beat) cubic-bezier(.2,.8,.3,1) infinite;
  animation-delay: var(--ui-anim-delay);
  animation-play-state: var(--ui-anim-state);
}
.clock.is-urgent { border-color: rgba(255, 70, 100, 0.75); }
.clock.is-urgent::after {
  content: "";
  position: absolute;
  inset: -1px;
  pointer-events: none;
  background: radial-gradient(100% 130% at 50% 100%, rgba(255, 45, 80, 0.42), transparent 70%);
  animation: clockGlow var(--ui-clock-beat) ease-in-out infinite;
  animation-delay: var(--ui-anim-delay);
  animation-play-state: var(--ui-anim-state);
}
@keyframes clockBeat {
  0%   { transform: skewX(var(--lean)) scale(1.14); color: #fff; }
  30%  { transform: skewX(var(--lean)) scale(0.99); color: var(--hot); }
  100% { transform: skewX(var(--lean)) scale(1); color: var(--hot); }
}
@keyframes clockGlow { 0% { opacity: 1; } 100% { opacity: 0.25; } }

/* ─── 3.3 player card (bottom-left) ─────────────────────────────────────── */

.pc {
  position: absolute;
  left: calc(var(--sa-l) + 16px);
  bottom: calc(var(--sa-b) + 16px);
  display: flex;
  align-items: stretch;
  gap: 10px;
  padding: 8px 18px 8px 8px;
  --notch: 12px;
  --accent: var(--team-a);
  --accent-lo: var(--team-a-lo);
  min-width: clamp(210px, 22vw, 320px);
  /* transform keeps its own springier curve; opacity/filter ride the cinematic
     fade so the card retreats and returns with the rest of the readout. */
  transition:
    transform 0.3s cubic-bezier(.2,1.4,.4,1),
    opacity var(--ui-cinematic-fade) ease,
    filter var(--ui-cinematic-fade) ease;
}
/* A NEW CARD SLIDING IN FROM THE LEFT = "you are driving somebody else now".
   That is all this means, and it is only used by updatePlayerCard's swap. */
.pc.is-swap { animation: pcSwap var(--ui-card-swap) cubic-bezier(.2,1.5,.4,1); }
@keyframes pcSwap {
  0%   { transform: translateX(-22px) scale(0.94); filter: brightness(2.2); }
  100% { transform: translateX(0) scale(1); filter: brightness(1); }
}
/* BEING HIT IS THE OPPOSITE GESTURE: the card is struck and shoved, it does not
   arrive. Same direction as the struck team's row (rowShakeA pushes right —
   INTO the frame — for the left-hand board, which is where this card lives), and
   it lasts exactly UI.hitFlash, the knob that already existed for this and that
   the card was not reading. */
.pc.is-hit { animation: pcHit var(--ui-hit-flash) cubic-bezier(.3,.9,.3,1); }
@keyframes pcHit {
  0%   { transform: translateX(6px) skewX(2deg); filter: brightness(2.4) saturate(1.6); }
  38%  { transform: translateX(-3px) skewX(-1deg); filter: brightness(1.35); }
  70%  { transform: translateX(2px) skewX(0.5deg); }
  100% { transform: translateX(0) skewX(0); filter: brightness(1); }
}
/* ...and a strong one shoves it harder and holds the flare longer. */
.pc.is-hit.is-hit-strong {
  animation-name: pcHitStrong;
  animation-duration: var(--ui-hit-flash-strong);
}
@keyframes pcHitStrong {
  0%   { transform: translateX(15px) skewX(5deg) scale(1.04); filter: brightness(3.4) saturate(1.9); }
  30%  { transform: translateX(-8px) skewX(-3deg) scale(0.98); filter: brightness(1.7) saturate(1.3); }
  56%  { transform: translateX(5px) skewX(1.5deg); filter: brightness(1.2); }
  78%  { transform: translateX(-3px); }
  100% { transform: translateX(0) skewX(0) scale(1); filter: brightness(1); }
}

/* The struck side's board leans inward once — the hit gets an owner. */
.side.is-hit .side__board { animation: sideNudgeA var(--ui-hit-flash) cubic-bezier(.3,.9,.3,1); }
.side--b.is-hit .side__board { animation-name: sideNudgeB; }
@keyframes sideNudgeA {
  0%   { transform: translateX(7px); }
  60%  { transform: translateX(-2px); }
  100% { transform: translateX(0); }
}
@keyframes sideNudgeB {
  0%   { transform: translateX(-7px); }
  60%  { transform: translateX(2px); }
  100% { transform: translateX(0); }
}
/* ONE FRAME OF WHITE, ON THE STRONG HIT ONLY. The lean says WHO took it; this
   says HOW HARD. It is deliberately a single hard step at the head of the
   animation and gone by 12% — a fade would read as a glow, and a glow is a
   state, not an impact. */
.side.is-hit.is-hit-strong .side__board {
  animation-name: sideNudgeStrongA;
  animation-duration: var(--ui-hit-flash-strong);
}
.side--b.is-hit.is-hit-strong .side__board { animation-name: sideNudgeStrongB; }
/* 5% of UI.hitFlashStrong ≈ 26ms ≈ a frame and a half at 60fps. Any longer and
   the white stops being an impact and becomes a state. */
@keyframes sideNudgeStrongA {
  0%   { transform: translateX(16px); filter: brightness(3.6) saturate(0.25); }
  5%   { filter: brightness(1.5) saturate(0.85); }
  22%  { filter: brightness(1); }
  38%  { transform: translateX(-6px); }
  66%  { transform: translateX(3px); }
  100% { transform: translateX(0); filter: brightness(1); }
}
@keyframes sideNudgeStrongB {
  0%   { transform: translateX(-16px); filter: brightness(3.6) saturate(0.25); }
  5%   { filter: brightness(1.5) saturate(0.85); }
  22%  { filter: brightness(1); }
  38%  { transform: translateX(6px); }
  66%  { transform: translateX(-3px); }
  100% { transform: translateX(0); filter: brightness(1); }
}

/* ── mugshot — pure CSS face, no bitmap anywhere ─────────────────────────────
   ⚠️ THIS AVATAR IS THE LARGEST PIECE OF CHARACTER ART THE UI OWNS, and until
   2026-07-27 it was drawn in a different language from the game it labels. It
   was an airbrushed radial-gradient ball with no black keyline anywhere, soft
   brown hair mass and a rounded-rectangle mouth — i.e. an emoji sitting next to
   3D characters built from thick black outlines and flat fills.
   research/art-direction.md §4 fixes three things and the mug broke all three:
   「太い黒のアウトライン」「色数は少なく面で塗る。グラデーションはほぼ無い」「への字口」.

   So: every part is FLAT (hard-stop two-tone at most), every part carries a real
   black keyline, the hair is a per-character silhouette (--hair-shape, set from
   the roster's own style id by hud.js dressMug), the headband only appears on
   the characters who actually wear one, and the default mouth is への字.

   The keyline is 2px of solid ink. Parts without a clip-path get it as a
   `box-shadow` spread; a clipped part cannot (clip-path eats the shadow too), so
   the hair builds its outline out of four 1-directional drop-shadows instead. */
.mug {
  --mug-ink: #100a08;
  --mug-line: 2px;
  position: relative;
  flex: 0 0 auto;
  width: clamp(44px, 4.4vw, 62px);
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: 4px;
  border: 1px solid rgba(255, 235, 200, 0.28);
  /* Two flat fields, hard stop — a backdrop, not a soft-box render. */
  background: linear-gradient(180deg,
    #17112a 0 54%,
    color-mix(in srgb, var(--accent) 62%, #000) 54% 100%);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.6);
}
/* Jersey: two flat fields split on a hard stop, with the same ink keyline. */
.mug__jersey {
  position: absolute;
  left: -12%; right: -12%; bottom: -14%;
  height: 42%;
  border-radius: 46% 46% 0 0;
  background: linear-gradient(180deg,
    var(--accent) 0 62%, var(--accent-lo) 62% 100%);
  box-shadow: 0 0 0 var(--mug-line) var(--mug-ink);
}
/* HEAD = ONE FLAT SKIN FIELD + ONE SHADOW FACET. Two tones, a hard edge between
   them, and a 2px ink ring — the same construction as the 3D heads (flat fill +
   inverted-hull outline), not an airbrushed sphere. */
.mug__head {
  position: absolute;
  left: 50%;
  top: 16%;
  width: 62%;
  height: 62%;
  margin-left: -31%;
  border-radius: 46% 46% 44% 44%;
  background: var(--skin, #f2c096);
  box-shadow: 0 0 0 var(--mug-line) var(--mug-ink);
}
/* The single shadow facet, cut with a straight edge down the lit side. ::before
   is the FIRST child in paint order, so brows/eyes/mouth stay on top of it. */
.mug__head::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  /* 62%, not 74%: the roster's pale tone (PALE 0xfcdcbd) is close enough to
     white that a light facet split read as one flat sheet of paper. Two tones
     have to be TWO tones at the palest skin in the game, not only at the
     darkest. */
  background: color-mix(in srgb, var(--skin, #f2c096) 62%, #8a3f2c);
  clip-path: polygon(100% 0, 100% 100%, 0 100%, 46% 26%);
}
/* HAIR IS A SILHOUETTE, NOT A COLOUR. --hair-shape is set per character by
   dressMug (roster `look.hair`, the same ids as chars/rig.js HAIR_STYLES); the
   polygons live below. Default = the お椀 the whole roster used to share.
   clip-path would eat a box-shadow keyline, so the ink is four hard 1-direction
   drop-shadows — an outline that follows the cut, whatever the cut is. */
.mug__hair {
  position: absolute;
  left: -6%; right: -6%; top: -14%;
  height: 54%;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--hair, #2a1a14) 78%, #c9a58e) 0 34%,
    var(--hair, #2a1a14) 34% 100%);
  /* GEOMETRY THE SHAPES ARE CUT AGAINST (measured off this rule, not guessed):
     the hair box starts 14% of the head's height ABOVE the head, so the skull's
     dome crosses this box at y ≈ 26%. A polygon whose top edge sits at y = 0
     therefore floats over the head like a hat — which is exactly what the first
     pass drew. Every style below either starts its mass at the dome line or
     deliberately escapes it (spikes, mohawk, pompadour). */
  clip-path: var(--hair-shape,
    polygon(6% 84%, 8% 40%, 20% 26%, 50% 20%, 80% 26%, 92% 40%, 94% 84%,
      76% 68%, 56% 88%, 32% 66%));
  filter:
    drop-shadow(var(--mug-line) 0 0 var(--mug-ink))
    drop-shadow(calc(-1 * var(--mug-line)) 0 0 var(--mug-ink))
    drop-shadow(0 var(--mug-line) 0 var(--mug-ink))
    drop-shadow(0 calc(-1 * var(--mug-line)) 0 var(--mug-ink));
}
/* 髪型ごとのシルエット。りゅうじ の逆立ち髪と たもつ の坊主が同じお椀だった
   ので、3人並べても髪色と肌色しか違わなかった (blind A/B 指摘2 の再生産)。 */
/* 逆立ち: the only style that leaves the dome, and it leaves it hard. */
.mug[data-hair="spiky"] {
  --hair-shape: polygon(4% 60%, 8% 30%, 18% 4%, 28% 30%, 40% 0, 52% 26%,
    64% 2%, 76% 30%, 88% 8%, 96% 36%, 96% 62%, 74% 50%, 50% 66%, 26% 48%);
}
/* リーゼント: volume swept up and back off the forehead. */
.mug[data-hair="pompadour"] {
  --hair-shape: polygon(6% 64%, 4% 34%, 12% 12%, 30% 2%, 52% 6%, 62% 22%,
    80% 22%, 92% 36%, 94% 64%, 76% 52%, 50% 66%, 26% 50%);
}
/* 坊主: a thin band locked onto the skull — the shortest silhouette we own. */
.mug[data-hair="buzz"] {
  --hair-shape: polygon(10% 56%, 14% 36%, 26% 26%, 50% 22%, 74% 26%, 86% 36%,
    90% 56%, 74% 48%, 50% 52%, 26% 48%);
}
/* 直毛の前髪: one straight cut across the brow. */
.mug[data-hair="flat"] {
  --hair-shape: polygon(6% 82%, 8% 34%, 20% 24%, 50% 20%, 80% 24%, 92% 34%, 94% 82%);
}
/* 長髪: falls past the jaw on both sides. */
.mug[data-hair="long"] {
  --hair-shape: polygon(6% 40%, 10% 28%, 24% 20%, 50% 16%, 76% 20%, 90% 28%,
    94% 40%, 94% 100%, 78% 100%, 78% 52%, 60% 60%, 40% 60%, 22% 52%,
    22% 100%, 6% 100%);
}
/* 後ろで結んだ髪: dome plus a tail hanging down the far side. */
.mug[data-hair="ponytail"] {
  --hair-shape: polygon(6% 60%, 8% 34%, 22% 22%, 50% 18%, 76% 22%, 90% 30%,
    92% 46%, 100% 54%, 100% 92%, 88% 92%, 84% 56%, 68% 54%, 46% 66%, 24% 50%);
}
/* モヒカン: a crest and two bare temples. */
.mug[data-hair="mohawk"] {
  --hair-shape: polygon(34% 62%, 36% 26%, 44% 2%, 56% 2%, 64% 26%, 66% 62%,
    58% 48%, 42% 48%);
}
/* ハチマキ is a CHARACTER TRAIT (roster.js `headband`), not furniture: it used to
   be painted on every face in the game, including the ones defined as not
   wearing one. hud.js dressMug toggles `.has-band`. */
.mug__band { display: none; }
.mug.has-band .mug__band {
  display: block;
  position: absolute;
  left: -8%; right: -8%; top: 30%;
  height: 15%;
  background: linear-gradient(180deg, var(--accent) 0 58%, var(--accent-lo) 58% 100%);
  box-shadow: 0 0 0 var(--mug-line) var(--mug-ink);
}
.mug__eye {
  position: absolute;
  top: 55%;
  width: 15%;
  height: 19%;
  border-radius: 50%;
  background: #14090f;
  box-shadow: inset 0 -0.5px 0 rgba(255, 255, 255, 0.2);
}
.mug__eye::after {
  content: "";
  position: absolute;
  left: 18%; top: 12%;
  width: 42%; height: 42%;
  border-radius: 50%;
  background: #fff;
  opacity: 0.92;
}
.mug__eye--l { left: 20%; }
.mug__eye--r { right: 20%; }
/* 太い眉 — research/art-direction.md §4 lists it with the big eyes and the への字
   mouth as a condition for the character reading as 原作 at all. At 7% of a 38px
   head that was a 2.7px line in hair-brown under the headband: invisible at 4×
   magnification. Black, 11% tall, and explicitly above the hair in paint order. */
.mug__brow {
  position: absolute;
  z-index: 3;
  top: 44%;
  width: 24%;
  height: 11%;
  border-radius: 1px;
  background: var(--ink);
  transition: transform 0.16s ease, top 0.16s ease;
}
/* 八の字: the inner ends ride LOW, which is the whole face of this series. */
.mug__brow--l { left: 14%; transform: rotate(14deg); }
.mug__brow--r { right: 14%; transform: rotate(-14deg); }

/* ── expressions ─────────────────────────────────────────────────────────────
   textures.js face() carries five expressions for the 3D head while this avatar
   held one smile from HP40 to HP0 — the card was the only place in the frame
   that looked like a modern app icon. Same vocabulary as face():
   'idle' | 'angry' | 'hurt' | 'shout' | 'ko'. hud.js writes data-face. */
.mug[data-face="hurt"] .mug__brow--l { transform: rotate(26deg); top: 47%; }
.mug[data-face="hurt"] .mug__brow--r { transform: rotate(-26deg); top: 47%; }
.mug[data-face="hurt"] .mug__eye { height: 13%; top: 58%; border-radius: 40%; }

.mug[data-face="shout"] .mug__brow--l { transform: rotate(20deg); top: 41%; }
.mug[data-face="shout"] .mug__brow--r { transform: rotate(-20deg); top: 41%; }
/* ── 叫び口 ─────────────────────────────────────────────────────────────────
   A shout is the one time the mouth is OPEN. The first attempt drew it as a
   single dark maroon oval (#2a060e) and hung the 2px keyline (#100a08) off it —
   two inks 6/255 apart, i.e. NO keyline at all. Magnified 8×, #7 はやて's shout
   read as a hole punched through the lower half of the face, and the hole's
   bottom edge merged with the jaw line of .mug__head. That is the exact fault
   the blind A/B raised against the 3D heads (「髪が一枚の暗い塊」), reproduced by
   the UI in the largest piece of character art it owns.

   research/art-direction.md §4 asks for 「太い黒のアウトライン」 and 「遠目でも表情
   が読める」, and both need the mouth to be a LIT interior with ink around it,
   not ink. So: three flat tones — a lit mouth interior (the keyline can bite on
   it), a tongue below, a bar of teeth above — and it is pulled up and shortened
   (24%→18% tall, top 70%→68%) so its outline clears the jaw instead of fusing
   with it. Still flat fills, still no gradient. */
.mug[data-face="shout"] .mug__mouth {
  top: 68%;
  width: 32%;
  height: 18%;
  margin-left: -16%;
  border-radius: 38% 38% 52% 52%;
  background: #6b1020;
  clip-path: none;
  overflow: hidden;
  box-shadow: 0 0 0 var(--mug-line) var(--mug-ink);
}
/* 舌 — the warm accent the series paints inside an open shout. */
.mug[data-face="shout"] .mug__mouth::after {
  content: "";
  position: absolute;
  left: 18%;
  right: 18%;
  bottom: -14%;
  height: 56%;
  border-radius: 50% 50% 44% 44%;
  background: var(--accent, #e63946);
  box-shadow: 0 0 0 1px var(--mug-ink);
}
/* 歯 — one hard white bar on the upper lip. It is what makes the opening read
   as a MOUTH at a glance rather than as a gap in the face. */
.mug[data-face="shout"] .mug__mouth::before {
  content: "";
  position: absolute;
  left: 6%;
  right: 6%;
  top: 0;
  height: 26%;
  z-index: 1;
  background: #fff6ea;
  box-shadow: 0 1px 0 var(--mug-ink);
}

.mug[data-face="ko"] .mug__brow--l { transform: rotate(-16deg); top: 42%; }
.mug[data-face="ko"] .mug__brow--r { transform: rotate(16deg); top: 42%; }
/* ✕ eyes. The pupil becomes the bed for two crossed bars. */
.mug[data-face="ko"] .mug__eye { background: transparent; box-shadow: none; }
.mug[data-face="ko"] .mug__eye::before,
.mug[data-face="ko"] .mug__eye::after {
  content: "";
  position: absolute;
  left: -14%;
  top: 40%;
  width: 128%;
  height: 22%;
  min-height: 2px;
  border-radius: 1px;
  background: var(--ink);
  opacity: 1;
}
.mug[data-face="ko"] .mug__eye::before { transform: rotate(40deg); }
.mug[data-face="ko"] .mug__eye::after  { transform: rotate(-40deg); }
/* 昇天した顔: the mouth goes slack — a plain ink bar, no chevron. */
.mug[data-face="ko"] .mug__mouth {
  top: 80%;
  width: 24%;
  margin-left: -12%;
  height: 7%;
  border-radius: 1px;
  background: var(--mug-ink);
  clip-path: none;
  box-shadow: none;
}
/* への字口 — research/art-direction.md §4 lists it beside the thick brows and the
   big eyes as a condition for the face reading as 原作 at all. It was a small
   rounded rectangle, i.e. the one mouth shape the series never draws. A chevron
   cut out of a solid ink block: no gradient, no radius, a stroke with corners. */
.mug__mouth {
  position: absolute;
  left: 50%;
  top: 74%;
  width: 34%;
  height: 14%;
  margin-left: -17%;
  border-radius: 0;
  background: var(--mug-ink);
  box-shadow: none;
  clip-path: polygon(0 62%, 50% 0, 100% 62%, 100% 100%, 50% 38%, 0 100%);
}
/* low HP: the への字 clenches — deeper V, heavier stroke, blood in the ink. */
.pc.is-hurt .mug__mouth {
  top: 72%;
  height: 18%;
  background: #4b0714;
  clip-path: polygon(0 54%, 50% 0, 100% 54%, 100% 100%, 50% 44%, 0 100%);
}
.pc.is-hurt .mug__head { filter: saturate(1.35) brightness(0.94); }
.pc.is-hurt .mug::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(200deg, transparent 55%, rgba(255, 40, 70, 0.4));
  animation: hurtPulse var(--ui-hurt-pulse) ease-in-out infinite alternate;
  animation-delay: var(--ui-anim-delay);   /* §0: capture scrubs, production runs */
  animation-play-state: var(--ui-anim-state);
}
@keyframes hurtPulse { from { opacity: 0.35; } to { opacity: 0.9; } }

/* ── 焼いた3Dの顔 (2026-08-06) ───────────────────────────────────────────────
   上の多角形は「同じ人を指しているのに別人」だった。共有していたのは肌色・髪色・
   髪型id・ハチマキの有無だけで、顔そのものは UI の創作だったので、3D の りゅうじ と
   カードの りゅうじ は似ていない。`assets/mugs/<id>-<表情>.png` は**出荷中のメッシュ
   とアトラスとマテリアルとライトで焼いた本人の頭**で、背景はアルファで抜いてある
   （tools/mugbake.mjs）。

   ★ 差し替えるのは**中身だけ**。枠（.mug 自身の縁・角丸・背景のチーム色・
     被弾の赤み）はオーナー確認済みの意匠なので 1 行も触らない。
   ★ 上の多角形は消さない。生成キャストが無い構成（`?body=procedural`）には画像が
     無く、そのときは `.has-photo` が立たないので従来の顔がそのまま出る。 */
.mug__photos { position: absolute; inset: 0; display: none; }
.mug.has-photo .mug__photos { display: block; }
/* 画像が出ているあいだ、描いた顔と描いたユニフォームは引っ込む（頭の中に眉・目・
   口・髪・ハチマキが全部入っているので、`.mug__head` 1 つで足りる）。 */
.mug.has-photo .mug__jersey,
.mug.has-photo .mug__head { display: none; }
.mug__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* 焼いてあるのは正方形だが、枠は正方形とは限らない —— `.pc` は
     `align-items: stretch` なので操作カードの枠は 62x90 の縦長になる（実測）。
     · cover にすると縦を埋めるかわりに左右を 15% ずつ削り、髪の広い体（いわお・
       けんじ・たけし）の輪郭が切れる。顔を切るくらいなら余らせる。
     · contain の余りを**上下に均等**に置くと、下端に「画像がここで終わった」と
       読める横一文字の継ぎ目が出る（実測・`.pc` で 14px）。下端に寄せると
       余りは全部**上**へ行き、そこはもともと枠の暗い背景なので消える。胸像が
       枠の下辺に立っている絵になる。 */
  object-fit: contain;
  object-position: bottom center;
  opacity: 0;
  user-select: none;
  -webkit-user-drag: none;
}
/* 表情の出し分けは CSS だけでやる。hud.js は今までどおり `data-face` を書くだけで、
   画像の枚数も名前も知らなくていい。 */
.mug__photo[data-expr="idle"] { opacity: 1; }
.mug[data-face="hurt"] .mug__photo,
.mug[data-face="shout"] .mug__photo,
.mug[data-face="ko"] .mug__photo { opacity: 0; }
.mug[data-face="hurt"] .mug__photo[data-expr="hurt"],
.mug[data-face="shout"] .mug__photo[data-expr="shout"],
.mug[data-face="ko"] .mug__photo[data-expr="ko"] { opacity: 1; }
/* 低HPの色転び。描いた顔では `.mug__head` に掛けていたのと同じ意図・同じ値。 */
.pc.is-hurt .mug.has-photo .mug__photos { filter: saturate(1.35) brightness(0.94); }

/* ── THE TWO BARS SHARE ONE RIGHT EDGE ───────────────────────────────────────
   Measured on r4-hud.png: inside a 316px-wide card, the HP tick row ended at
   x=222 and the SUPER track ran on to x=279 — a 57px (18% of the card) stagger
   between two bars stacked 22px apart. The cause was structural, not a stray
   margin: `.pc__hprow` was 「10個の固定幅目盛 + min-width 2.4em の数値」 while
   `.pc__superrow` was `.super { flex: 1 }`, i.e. two different layout systems
   solving for two different widths in the same card.

   One grid now owns both rows. `display: contents` dissolves the two row
   wrappers (they stay in the DOM because hud.js builds and updates through
   them), so the gauge and the SUPER track land in the SAME column and the
   number and the SUPER label land in the second one. The gauge column is
   `max-content` — it is a countable object and it sizes itself; the bar then
   stretches to exactly that, which is what makes the two right edges the same
   edge by construction rather than by a hand-tuned width. */
.pc__body {
  display: grid;
  grid-template-columns: max-content max-content;
  justify-content: start;
  align-content: center;
  column-gap: 8px;
  row-gap: 5px;
  min-width: 0;
  flex: 1;
}

.pc__nameline { grid-column: 1 / -1; display: flex; align-items: baseline; gap: 7px; min-width: 0; }
/* The rows are addressing handles for hud.js, not boxes. */
.pc__hprow, .pc__superrow { display: contents; }
.pc__no {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-weight: 900;
  font-size: 1.14em;
  color: var(--gold-hot);
  transform: skewX(var(--lean));
  text-shadow: 0 1px 0 #000, 0 0 0.5em rgba(255, 200, 90, 0.5);
}
.pc__no::before { content: "#"; font-size: 0.7em; opacity: 0.72; }
/* Hiragana given name, matching the board. */
.pc__name {
  font-size: 1.04em;
  font-weight: 900;
  letter-spacing: 0.03em;
  white-space: nowrap;
  color: #f2f7ff;
  -webkit-text-stroke: 0.1em #08050e;
  paint-order: stroke fill;
  text-shadow: 0.05em 0.07em 0 #08050e;
}
/* ...with the kanji underneath it, small: the card is the one place with room
   to say who this actually is. */
.pc__kanji {
  font-size: 0.68em;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--cream-dim);
  opacity: 0.82;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
/* A kana-only roster leaves this empty; an empty span must not spend a gap. */
.pc__kanji:empty { display: none; }
.pc__role {
  margin-left: auto;
  flex: 0 0 auto;
  font-size: 0.6em;
  letter-spacing: 0.14em;
  padding: 1px 5px;
  border-radius: 2px;
  color: #0d0812;
  background: var(--gold);
}

/* The card carries the SAME segmented gauge as the board, one size up. Health
   is one countable object everywhere in this game: two different readouts of
   the same number would be two different games. */
.gauge--card { gap: 3px; align-self: center; }
.gauge--card .tick {
  --tick-w: clamp(8px, 0.72vw, 13px);
  --tick-h: clamp(11px, 1.05vw, 17px);
}
.pc__hpnum {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-weight: 900;
  font-size: 0.82em;
  min-width: 2.4em;
  text-align: right;
  align-self: center;
  color: var(--cream);
}

/* super gauge — segmented, and it *burns* when it is full */
.super {
  position: relative;
  /* Grid row 3, column 1 — the same column the HP ticks occupy, so the bar is
     exactly as wide as the gauge above it (see .pc__body). */
  align-self: center;
  height: 11px;
  border-radius: 2px;
  overflow: hidden;
  background: linear-gradient(180deg, #1e1730, #0a0714);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.9);
}
.super__fill {
  position: absolute;
  inset: 0;
  transform-origin: left center;
  will-change: transform;
  background: linear-gradient(90deg, #ff8a3c 0%, #ffd166 45%, #fff3c4 100%);
}
.pc__superlabel {
  align-self: center;
  justify-self: end;
  font-family: var(--font-disp);
  font-weight: 900;
  font-size: 0.54em;
  letter-spacing: 0.16em;
  color: var(--gold);
  transform: skewX(var(--lean));
  text-shadow: 0 1px 0 #000;
}
.super.is-full + .pc__superlabel, .pc__superrow .super.is-full ~ .pc__superlabel { color: var(--gold-hot); }

/* ── CPU 代行中の帯 (2026-08-04) ──────────────────────────────────────────────
   プレイヤーカードは「あなたが動かしている人」の札。動かしていないなら、札の上で
   そう言う。カードの中ではなく **上に載せる** ので、出入りしても中の行が動かない
   ＝ HP もゲージも読み位置が変わらない。

   § レイアウト法 (SPEC §5.6) は守る: 帯は左下のカードの占有域から出ない。
   § 出ている間だけカード自体の彩度を落とす。「握っていない」は色の情報でもある。
   § 点滅は --ui-t にスクラブする (§0)。壁時計で回すとヘッドレスの1枚が run ごとに
     違う位相で写り、目視の A/B ができなくなる。 */
.pc__auto {
  grid-column: 1 / -1;
  display: none;
  align-items: center;
  gap: 7px;
  padding: 3px 8px;
  margin-bottom: 1px;
  border: 2px solid #08050e;
  border-radius: 3px;
  background: linear-gradient(180deg, rgba(52, 40, 18, 0.98), rgba(24, 17, 8, 0.98));
  box-shadow: 0 0 0 1px rgba(255, 214, 102, 0.45) inset;
  white-space: nowrap;
}
.pc.is-auto .pc__auto { display: flex; }
/* 握られていないカードは色が抜ける。文字だけだと視界の隅では気づかない。 */
.pc.is-auto { filter: saturate(0.35) brightness(0.88); }
/* ...ただし帯そのものは抜かない。filter は子孫にも掛かるので、いちばん読ませたい
   1 行が一緒に沈む（実測: 彩度が抜けて金の枠が灰色になった）。ここで打ち消す。 */
.pc.is-auto .pc__auto { filter: saturate(3) brightness(1.2); }
.pc__autodot {
  width: 9px;
  height: 9px;
  flex: none;
  border-radius: 50%;
  border: 2px solid #08050e;
  background: var(--gold-hot);
  animation: autoBlink 0.9s steps(1, end) infinite;
  animation-delay: var(--ui-anim-delay);   /* §0: capture scrubs, production runs */
  animation-play-state: var(--ui-anim-state);
}
@keyframes autoBlink { 0% { opacity: 1; } 50% { opacity: 0.15; } 100% { opacity: 1; } }
.pc__autotext {
  font-size: 0.72em;
  font-weight: 900;
  letter-spacing: 0.04em;
  color: #ffe8a8;
  -webkit-text-stroke: 0.09em #08050e;
  paint-order: stroke fill;
}
.pc__autosub {
  margin-left: auto;
  font-size: 0.54em;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--cream-dim);
  opacity: 0.85;
}

.super__scan {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(115deg,
    rgba(255, 255, 255, 0.34) 0 3px, transparent 3px 9px);
  opacity: 0.5;
}
.super__ticks {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(90deg,
    transparent 0 calc(25% - 1px), rgba(0, 0, 0, 0.75) calc(25% - 1px) 25%);
}
/* Both scrubbed to --ui-t (§0): "the bar is full" is a game state the player
   acts on, and a capture that froze the flow at a random phase made a full bar
   look like a different amount of charge in every shot. */
.super.is-full .super__fill {
  background: linear-gradient(90deg, #fff 0%, #ffd166 30%, #ff5470 60%, #ffe9a8 100%);
  background-size: 220% 100%;
  animation: superFlow var(--ui-super-flow) linear infinite;
  animation-delay: var(--ui-anim-delay);
  animation-play-state: var(--ui-anim-state);
}
.super.is-full {
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.9), 0 0 14px rgba(255, 170, 60, 0.85);
  animation: superBreathe var(--ui-super-breathe) ease-in-out infinite alternate;
  animation-delay: var(--ui-anim-delay);
  animation-play-state: var(--ui-anim-state);
}
@keyframes superFlow { from { background-position: 0 0; } to { background-position: 220% 0; } }
@keyframes superBreathe { from { filter: brightness(1); } to { filter: brightness(1.45); } }
.super.is-charge { animation: superCharge var(--ui-charge-pulse) ease-out; }
@keyframes superCharge {
  0% { filter: brightness(2.6) saturate(1.6); transform: scaleY(1.5); }
  100% { filter: brightness(1); transform: scaleY(1); }
}
/* Firing is the opposite gesture: the bar is emptied, so it flushes white and
   sags instead of popping. Longer than the charge (UI.superDrainFlash) because
   this is the payoff, not the tick. hud.js keeps the two classes exclusive. */
.super.is-drain { animation: superDrain var(--ui-super-drain) cubic-bezier(.2,.8,.3,1); }
@keyframes superDrain {
  0%   { filter: brightness(3.2) saturate(0.2); transform: scaleY(1.28); }
  22%  { filter: brightness(1.9) saturate(0.8); transform: scaleY(0.86); }
  100% { filter: brightness(1); transform: scaleY(1); }
}

/* ─── 3.4 announce ──────────────────────────────────────────────────────── */

.ann {
  position: absolute;
  left: 0;
  right: 0;
  /* THE SHOUT MUST NOT ERASE THE PICTURE IT IS NAMING. At 34% the headline band
     (y≈306–470 at 1600×900) landed exactly on the camera's look-at (2, 1.2, 0),
     so 「試合開始」 covered the thrower and 「みつどもえ」 covered the net and the
     shooter's upper body — the worst possible failure for this axis: 必殺の名前が
     必殺の絵を消す。Now that the roster band is three floating panels, the strip
     under them is free, and the frame divides vertically: sky/富士山 → HUD →
     shout → 主役. Numbers live in config (UI.announceTop / announceTopSuper). */
  top: calc(var(--ui-ann-top, 0.22) * 100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  /* The headline overshoots DOWNWARD on the way in (chSlam hits scale(1.06) at
     78% and every glyph is rotated up to ±4.5deg), so a 4px gap was not a gap:
     for the ~0.3s of the entrance the main line landed on top of the Latin sub
     and 'FIGHT!' read as 'FIGHTD'. This is the room those two transforms need. */
  gap: clamp(8px, 1.1vh, 18px);
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
}
.ann.is-on { opacity: 1; visibility: visible; }

/* Angled ribbon behind the words so they never sit on raw gameplay. */
.ann__band {
  position: absolute;
  left: -6%;
  right: -6%;
  top: -0.06em;
  /* -0.32em, not -0.40em: .ann__main gained 0.08em of padding-bottom as escape
     room for the entrance overshoot, and the ribbon must not grow with it. */
  bottom: -0.32em;
  z-index: 0;
  background: linear-gradient(90deg,
    transparent 0%, rgba(10, 5, 16, 0.0) 4%,
    rgba(10, 5, 16, 0.52) 18%, rgba(10, 5, 16, 0.60) 82%,
    rgba(10, 5, 16, 0.0) 96%, transparent 100%);
  transform: skewY(-1.4deg);
  /* The glyphs land LEFT TO RIGHT (--i staggers them), so the ribbon has to open
     the same way. Centre-origin growth made the band arrive from both ends at
     once while the words arrived from one. */
  transform-origin: 0% 50%;
  opacity: 0;
}
/* THE RIBBON OPENS AT THE SPEED OF THE WORDS. A flat --ui-announce-in (0.30s)
   band reached full width at 0.165s while a 7-glyph announce does not finish
   landing until 0.30 + 6×0.03 = 0.48s, so for ~0.3s the middle of the screen
   held an empty black parallelogram. `--ann-n` is the glyph count, written by
   hud.js showAnnounce(); the band now finishes exactly when the last glyph does. */
.ann.is-in .ann__band {
  animation: bandIn
    calc(var(--ui-announce-in) + var(--ann-n, 6) * var(--ui-char-stagger))
    cubic-bezier(.15,1.4,.35,1) both;
  animation-delay: calc(0s - var(--ann-t, 0s));
}
@keyframes bandIn {
  0%   { opacity: 0; transform: skewY(-1.4deg) scaleX(0.14) scaleY(2.6); }
  /* Opacity is up early (the first glyph needs its bed); the REST of the entrance
     is spent stretching to the right, chasing the glyph stagger. */
  30%  { opacity: 1; transform: skewY(-1.4deg) scaleX(0.44) scaleY(0.94); }
  100% { opacity: 1; transform: skewY(-1.4deg) scaleX(1) scaleY(1); }
}

.ann__main {
  position: relative;
  display: flex;
  justify-content: center;
  flex-wrap: nowrap;
  font-family: var(--font-disp);
  font-weight: 900;
  font-size: clamp(2.6rem, 8.4vw, 7.2rem);
  line-height: 0.94;
  letter-spacing: 0.005em;
  white-space: nowrap;
  padding-bottom: 0.08em;   /* escape room for the chSlam overshoot */
}

/* ── ★ 柔らかい影は「語」に1面・鋭いキーラインは「文字」ごと（2026-08-11）──────
   ぼかしは**画素ではなく面の枚数**で課金される。見出しが出ている 2.6 秒のあいだ
   ずっと 12fps に落ちる欠陥（オーナー報告「必殺技の時に全部の技で毎回コマ落ちする」）
   の主犯がここだった。`filter` の `drop-shadow` は**その要素の描画結果を1枚の面**
   としてぼかすので、`.ann__ch`（glyph 1文字）に大半径のぼかしを置くと、ぼけた面が
   **文字数ぶん**毎コマ焼き直される。実測 2026-08-11（1600x900・`tools/superperf.mjs`）:

     出荷そのまま                                    必殺中のコマ間隔 中央 82.0ms
     `.ann__ch` の filter を none                                    16.7ms
     柔らかい 2 本を `.ann__chars` へ寄せる（＝この形）              16.7ms
     半径を倍にする（16px→32px・26px→52px）           変化なし ＝ 半径の問題ではない
     font-size を 8.4rem→3rem                         変化なし ＝ 文字の大きさでもない
     postfx/particles/shadows/bloom を off・解像度 1/6 変化なし ＝ WebGL 側は無関係

   だから柔らかい影（キャストシャドウとオーラ）は**語のまとまり `.ann__chars` に
   1 面だけ**掛け、鋭いキーライン（`0 0 1〜4px` の多重インクと `0 Npx 0` の押し出し）
   は文字ごとに残す。輪郭を作っているのはキーラインの方なので、そこは 1px も動かさない。
   ★ 見た目の差はオーナーが比較画像を見て承認済み（2026-08-11・平均差 1.34/255）＝
     光の輪が「文字ごと」から「語の輪郭」に締まる。実装後に撮り直した実測も同程度で、
     `--scene=super` の同一 t で見出し周辺の平均差は 必殺 1.65/255・試合開始 0.77/255。
   ⚠️ この分業を崩して `.ann__ch` 側に大半径のぼかしを足すと欠陥がそのまま戻る。
     `node tools/superperf.mjs --break=ink` がその壊し方を再現するネガティブテスト。
   ⚠️ 変種（is-super / is-ascend / is-call / is-win）も同じ分業で書くこと。
     `.ann__ch` を上書きするときは**鋭い方だけ**、柔らかい方は `.ann__chars` へ。 */

/* Every character is its own slammed object. `--i` staggers them. */
.ann__chars {
  position: relative;   /* keeps the glyphs above the ribbon without z-index:-1 */
  z-index: 2;
  display: flex;
  flex-wrap: nowrap;
  align-items: baseline;
  /* 語に1面だけのキャストシャドウ（上の註）。文字ごとには掛けない。 */
  filter: drop-shadow(0 10px 14px rgba(0, 0, 0, 0.75));
}

/* ── ★ 低段だけ柔らかい影を落とす（2026-08-11 夕・オーナー裁定）────────────────
   `<html data-ann-soft="0">` は `UI.annSoftShadow` が false のとき ＝ 画面の短辺が
   `RENDER.lowTierShortSide`(480px) 以下のとき（config.js 末尾の QUALITY・
   hud.js publishCssVars が写す）。**デスクトップには一生当たらない**。

   落とすのは `.ann__chars` の**柔らかい 1 面だけ**。鋭いキーライン（`.ann__ch` の
   `0 0 1〜4px` の多重インクと `0 Npx 0` の押し出し）はこのセレクタの外なので、
   低段でも文字の輪郭は 1px も痩せない。
   ⚠️ セレクタが `.ann .ann__chars`（0,3,1）なのは、変種の `.ann.is-super .ann__chars`
     ほか 4 本（どれも 0,3,0）に**詳細度で勝つ**ため。`!important` は使わない
     ＝ 上の probe（tools/superperf.mjs `--ab=annsoft`）が同じ土俵で比較できる。
   実測の根拠は config.js `UI.annSoftShadow` の註（モバイル条件のペア A/B）。 */
html[data-ann-soft="0"] .ann .ann__chars { filter: none; }

/* ── ★ 診断用: 見出しを丸ごと出さない（`?off=ann`・config.js FLAGS.ann の註）────
   `<html data-ann-hide="1">` は `FLAGS.ann` が false のときだけ付く（hud.js
   publishCssVars）。段（低段/高段）とは無関係、URL でだけ決まる。既定は属性が
   無い ＝ オーナー承認済みの絵のまま。
   ⚠️ セレクタの詳細度 (0,2,1) は `.ann.is-on`(0,2,0) より高いので、`opacity:1;
   visibility:visible` を跨いで確実に隠す（`!important` は使わない）。
   `tools/superperf.mjs` の `--probe=annhide`（`.ann{display:none!important}`）と
   同じ最終効果（`.ann` の computed display が `none`）になるよう選んである。 */
html[data-ann-hide="1"] .ann { display: none; }

/* ── ★ 診断用: 見出しの層をさらに3枚に割る（`?off=annink|anngrad|annband`・
   config.js FLAGS.annink/anngrad/annband の註・2026-08-12）───────────────────
   `<html data-ann-ink/data-ann-grad/data-ann-band="1">` は対応する FLAGS が false の
   ときだけ付く（hud.js publishCssVars）。3 つとも既定は属性が無い ＝ 本番の絵のまま。
   `tools/superperf.mjs` の `--probe=annink|anngrad|annband` と同じ最終効果になるよう
   選んである。`!important` が要る理由は annink の註（filter は chSlamSuper の入場
   キーフレームでも書かれるので、specificity だけでは倒せない）。 */
html[data-ann-ink="1"] .ann.is-super .ann__ch,
html[data-ann-ink="1"] .ann__ch { filter: none !important; }

html[data-ann-grad="1"] .ann__ch {
  color: #ffcf6a !important;
  background-image: none !important;
  -webkit-background-clip: border-box !important;
  background-clip: border-box !important;
}

html[data-ann-band="1"] .ann.is-super .ann__band,
html[data-ann-band="1"] .ann__band { background-image: none !important; }

.ann__ch {
  display: inline-block;
  color: transparent;
  background-image: linear-gradient(178deg, #fffdf4 0%, #ffe08a 38%, #ffab2e 62%, #ff5a3c 100%);
  -webkit-background-clip: text;
  background-clip: text;
  /* 鋭いキーラインと押し出しだけ。柔らかい影は `.ann__chars` が持つ。 */
  filter:
    drop-shadow(0 0 1px #2a0d18) drop-shadow(0 0 2px #2a0d18)
    drop-shadow(0 4px 0 #3d0f1e);
  transform-origin: 50% 60%;
  will-change: transform, opacity;
}
.ann.is-in .ann__ch {
  animation: chSlam var(--ui-announce-in) cubic-bezier(.16,1.6,.35,1) both;
  animation-delay: calc(var(--i) * var(--ui-char-stagger) - var(--ann-t, 0s));
}
@keyframes chSlam {
  0%   { opacity: 0; transform: scale(3.4) rotate(var(--rot, 0deg)) translateY(-0.14em) skewX(calc(var(--ui-skew) * 2.2)); filter: blur(6px) drop-shadow(0 0 22px #ffce6a); }
  55%  { opacity: 1; transform: scale(0.9) rotate(calc(var(--rot, 0deg) * -0.35)) skewX(var(--ui-skew)); filter: blur(0); }
  78%  { transform: scale(1.06) skewX(var(--ui-skew)); }
  100% { opacity: 1; transform: scale(1) rotate(var(--rot, 0deg)) skewX(var(--ui-skew)); }
}
.ann.is-out .ann__ch {
  animation: chOut var(--ui-announce-out) cubic-bezier(.5,0,.9,.3) both;
  animation-delay: calc(var(--i) * var(--ui-char-stagger) * 0.5 - var(--ann-t, 0s));
}
@keyframes chOut {
  0%   { opacity: 1; transform: scale(1) skewX(var(--ui-skew)); }
  100% { opacity: 0; transform: scale(1.5) translateY(-0.22em) skewX(calc(var(--ui-skew) * 2.4)); filter: blur(9px); }
}
/* Exit matches too: chOut blurs and scales UP, so a band that collapsed to
   scaleY(0.1) was travelling the opposite way from the words sitting on it. */
.ann.is-out .ann__band {
  animation: bandOut var(--ui-announce-out) ease-in both;
  animation-delay: calc(0s - var(--ann-t, 0s));
}
@keyframes bandOut {
  to { opacity: 0; transform: skewY(-1.4deg) scaleX(1.1) scaleY(1.35); filter: blur(7px); }
}

.ann__sub {
  position: relative;
  /* Above .ann__chars (z-index 2). #ui-root is `contain: layout paint`, so it
     is a stacking context and a positive z-index on the glyph row beat a plain
     `position: relative` sub every time — the headline was painted OVER the
     subtitle, not just next to it. The band is 0, the glyphs are 2, the Latin
     line is 3: nothing in the announce is ambiguous about who is on top. */
  z-index: 3;
  font-family: var(--font-disp);
  font-weight: 900;
  font-size: clamp(0.62rem, 1.35vw, 1.15rem);
  letter-spacing: 0.5em;
  text-indent: 0.5em;
  color: var(--gold-hot);
  transform: skewX(var(--ui-skew));
  text-shadow: 0 2px 0 #2a0d18, 0 0 14px rgba(255, 190, 90, 0.7);
  opacity: 0;
}
.ann.is-in .ann__sub {
  animation: subIn var(--ui-sub-in) ease-out both;
  animation-delay: calc(0.1s - var(--ann-t, 0s));
}
@keyframes subIn {
  0% { opacity: 0; letter-spacing: 1.4em; }
  100% { opacity: 1; letter-spacing: 0.5em; }
}
.ann.is-out .ann__sub { animation: none; opacity: 0; transition: opacity var(--ui-announce-out); }

/* Two speed streaks that rip past behind the words. */
.ann__streak {
  position: absolute;
  left: -20%;
  right: -20%;
  height: 0.09em;
  background: linear-gradient(90deg, transparent, #fff, transparent);
  opacity: 0;
  pointer-events: none;
}
.ann__streak { z-index: 1; }
.ann__streak--1 { top: 26%; }
.ann__streak--2 { top: 68%; }
.ann.is-in .ann__streak--1 {
  animation: streak var(--ui-streak) cubic-bezier(.2,.8,.3,1) both;
  animation-delay: calc(0.04s - var(--ann-t, 0s));
}
.ann.is-in .ann__streak--2 {
  animation: streak var(--ui-streak) cubic-bezier(.2,.8,.3,1) both reverse;
  animation-delay: calc(0.11s - var(--ann-t, 0s));
}
@keyframes streak {
  0%   { opacity: 0; transform: translateX(-60%) scaleX(0.2); }
  35%  { opacity: 0.95; }
  100% { opacity: 0; transform: translateX(60%) scaleX(1.6); }
}

/* variants */
.ann.is-ko .ann__ch {
  background-image: linear-gradient(178deg, #ffffff 0%, #ffd2dc 26%, #ff3b5c 62%, #7d0f22 100%);
}
.ann.is-ko .ann__sub { color: #ff8fa3; }
/* ── 必殺 ────────────────────────────────────────────────────────────────────
   THE LOUDEST THING IN THE GAME, and it was quieter than the referee. Measured
   against r1-hit-001.png: 「試合開始」 inherits the base .ann__ch filter, i.e. a
   hard `drop-shadow(0 4px 0)` extrusion AND a heavy `0 10px 14px` cast shadow —
   it lands like a sheet of steel. 必殺 overrode `filter` wholesale and threw the
   cast shadow away, so 「みつどもえ」 was a soft glow with almost no keyline, and its
   magenta lower half dissolved into the red-brown dirt behind it.

   With background-clip:text the glyph colour is the BACKGROUND, so
   -webkit-text-stroke cannot thicken it — a keyline can only be built by
   stacking drop-shadows. Three of them, then the extrusion, then the cast
   shadow, then the aura. The stack is a variable so the entrance keyframes can
   carry it too instead of dropping the ink for the first 0.17s.
   ⚠️ 2026-08-11: キャストシャドウとオーラ（柔らかい 2 本）はここから
      `.ann.is-super .ann__chars` へ移した。ここに残っているのは鋭い 4 本だけ。
      理由は `.ann__chars` の上の★の註（ぼけた面を文字数ぶん焼くと 12fps になる）。 */
.ann.is-super .ann__ch {
  --ann-ink:
    drop-shadow(0 0 2px #16062a) drop-shadow(0 0 2px #16062a) drop-shadow(0 0 4px #16062a)
    drop-shadow(0 7px 0 #2a0b48);
  background-image: linear-gradient(178deg, #ffffff 0%, #c9f3ff 22%, #7a4dff 58%, #ff3bd0 100%);
  filter: var(--ann-ink);
}
/* ...そして落ち影と紫のオーラは語のまとまりに1面だけ。 */
.ann.is-super .ann__chars {
  filter:
    drop-shadow(0 10px 16px rgba(0, 0, 0, 0.82))
    drop-shadow(0 0 26px rgba(160, 110, 255, 0.9));
}
.ann.is-super .ann__main { font-size: clamp(2.9rem, 9.6vw, 8.4rem); }
/* 必殺 is the biggest type in the game, so it starts higher than the other calls
   or its own body reaches back down onto the shooter. */
.ann.is-super { top: calc(var(--ui-ann-top-super, 0.16) * 100%); }
.ann.is-super .ann__band {
  background:
    radial-gradient(45% 130% at 50% 45%, rgba(150, 90, 255, 0.45), transparent),
    linear-gradient(90deg, transparent, rgba(28, 6, 48, 0.86) 18%, rgba(28, 6, 48, 0.90) 82%, transparent);
}
/* A deeper crouch and a bigger overshoot than chSlam, so the step up from
   is-fight to is-super is visible as MOTION and not only as size. */
.ann.is-super.is-in .ann__ch { animation-name: chSlamSuper; }
@keyframes chSlamSuper {
  0%   { opacity: 0; transform: scale(4.2) rotate(var(--rot, 0deg)) translateY(-0.18em) skewX(calc(var(--ui-skew) * 2.4));
         filter: blur(9px) var(--ann-ink); }
  55%  { opacity: 1; transform: scale(0.82) rotate(calc(var(--rot, 0deg) * -0.4)) skewX(var(--ui-skew));
         filter: blur(0) var(--ann-ink); }
  86%  { transform: scale(1.14) skewX(var(--ui-skew));
         filter: blur(0) var(--ann-ink); }
  100% { opacity: 1; transform: scale(1) rotate(var(--rot, 0deg)) skewX(var(--ui-skew));
         filter: blur(0) var(--ann-ink); }
}
.ann.is-super .ann__sub { color: #d9c2ff; }
.ann.is-fight .ann__ch {
  background-image: linear-gradient(178deg, #ffffff 0%, #ffe98a 32%, #ff7a1f 70%, #d81b3c 100%);
}
.ann.is-ready .ann__main { font-size: clamp(2.1rem, 6.4vw, 5.4rem); }

/* ── 審判のコール ────────────────────────────────────────────────────────────
   ラインオーバー / カウントオーバー / 試合終了. A whistle, not a finisher: white
   into cold gold, no colour drama, and HALF the type size of a 必殺 so the
   announce layer keeps a step to climb when a real super lands. Fouls interrupt
   the rally constantly — this one has to be readable and then get out. */
.ann.is-call .ann__main { font-size: clamp(2.0rem, 5.6vw, 4.4rem); }
.ann.is-call .ann__ch {
  background-image: linear-gradient(178deg, #ffffff 0%, #f4f7ff 34%, #ffd98a 74%, #c88f2a 100%);
  /* 鋭い方だけ（柔らかいキャストシャドウは `.ann__chars` へ・上の★の註）。 */
  filter:
    drop-shadow(0 0 1px #14121e) drop-shadow(0 0 2px #14121e)
    drop-shadow(0 3px 0 #23202f);
}
.ann.is-call .ann__chars { filter: drop-shadow(0 7px 10px rgba(0, 0, 0, 0.7)); }
.ann.is-call .ann__band {
  background: linear-gradient(90deg, transparent, rgba(8, 8, 16, 0.58) 18%, rgba(8, 8, 16, 0.62) 82%, transparent);
}
.ann.is-call .ann__sub { color: #dfe6f5; }

/* ── 勝敗 ────────────────────────────────────────────────────────────────────
   「紅蓮高校 の勝ち」/「引き分け」. Trophy gold, and bigger than the referee but
   not louder than a 必殺 — the match is over, nothing has to compete with it. */
.ann.is-win .ann__main { font-size: clamp(2.4rem, 7.2vw, 6.2rem); }
.ann.is-win .ann__ch {
  background-image: linear-gradient(178deg, #ffffff 0%, #fff0b8 24%, #ffb02e 62%, #c9561a 100%);
  /* 鋭い方だけ（金のオーラは `.ann__chars` へ・上の★の註）。 */
  filter:
    drop-shadow(0 0 1px #2a1406) drop-shadow(0 0 3px #2a1406)
    drop-shadow(0 4px 0 #46220a);
}
.ann.is-win .ann__chars { filter: drop-shadow(0 0 26px rgba(255, 190, 90, 0.8)); }
.ann.is-win .ann__band {
  background: linear-gradient(90deg, transparent, rgba(30, 14, 4, 0.60) 18%, rgba(30, 14, 4, 0.64) 82%, transparent);
}
.ann.is-win .ann__sub { color: #ffdf9a; }

/* ── てんしになった ─────────────────────────────────────────────────────────
   シリーズの魂 (SPEC §0.5 #9). Neither a foul nor a technique: the frame goes
   quiet and pale. Cold white into halo gold, and a band that is a glow rather
   than a curtain. */
/* THE ORDER OF THIS FILTER STACK IS THE WHOLE FIX.
   Measured on the scene=hit t=3.0 capture: around the 「て」 of 「てんしになった」
   (x430-470, y196-290) the DARKEST pixel was 102/255 and p05 was 110 — there was
   no black keyline at all, and the 紅白 tent and the crowd behind it were reading
   straight through the glyph. The same typographic system draws 「試合開始」 at
   min 7 / p05 23 in the same kind of region. SPEC §0.5 #9 calls this moment
   「シリーズの魂」, and it was the weakest announce on the board — quieter than a
   routine call, i.e. the hierarchy was inverted at the one place it must not be.

   Cause: this rule opened with a single 2px 薄紫 (#2b2338) and then spent the
   rest of the stack on GLOW. With `-webkit-background-clip: text` the glyph
   colour is a background, so `-webkit-text-stroke` cannot thicken it and the ink
   can only be built by stacking drop-shadows — which means the ink must come
   FIRST and the aura LAST, exactly as is-super and is-call already do:
   黒の多重キーライン → 押し出し → キャストシャドウ → 外向きのグロー. */
.ann.is-ascend .ann__ch {
  background-image: linear-gradient(178deg, #ffffff 0%, #eaf6ff 22%, #ffe9a8 62%, #ffb54a 100%);
  /* 順序（黒のキーライン → 押し出し）はそのまま。キャストシャドウと光輪は
     `.ann__chars` へ移した（2026-08-11・上の★の註）。 */
  filter:
    drop-shadow(0 0 2px #14101c) drop-shadow(0 0 2px #14101c) drop-shadow(0 0 4px #14101c)
    drop-shadow(0 5px 0 #2a2340);
}
.ann.is-ascend .ann__chars {
  filter:
    drop-shadow(0 10px 16px rgba(0, 0, 0, 0.82))
    drop-shadow(0 0 30px rgba(255, 236, 180, 0.9));
}
/* ...and the halo it sits in is a BED, not a haze. At 0.30 the centre was still
   mostly the 紅白テント; at 0.55 the word stands on its own light, which is also
   the more literal reading of 昇天. The dark ring is pulled inward (46% → 34%) so
   the transition happens under the glyphs instead of outside them. */
.ann.is-ascend .ann__band {
  background: radial-gradient(60% 140% at 50% 40%,
    rgba(255, 250, 232, 0.55) 0%, rgba(18, 15, 32, 0.58) 34%, rgba(18, 15, 32, 0.44) 66%, transparent 100%);
}
.ann.is-ascend .ann__sub { color: #ffeec2; }
/* ...AND IT GOES UP. 3D 側は character.js `_ascendT` で体を持ち上げているのに、
   文字だけがその場に留まっていては「昇天」にならない。The drift runs for the whole
   hold (--ui-announce-hold, published from UI.announceHold) and is scrubbed by
   --ann-t like every other announce keyframe, so the capture stays deterministic.
   The exit keeps travelling the same way instead of blowing outward. */
.ann.is-ascend.is-in .ann__chars {
  animation: annAscend var(--ui-announce-hold) cubic-bezier(.3, .5, .35, 1) both;
  animation-delay: calc(0s - var(--ann-t, 0s));
}
/* 「昇る」は translateY だけで言う。The drift used to fade the row to 0.92 on the
   way in and 0.86 on the way up, and since .ann__chars carries EVERY glyph, that
   fade multiplied the whole headline — the loudest moment in the game was the
   only one drawn at less than full opacity. Transparency is the one tool that
   cannot express 昇天 here, because it works by deleting the thing that is
   supposed to be ascending. Height says it; opacity stays at 1 and the exit
   (chAscendOut) is the only place anything fades. */
@keyframes annAscend {
  0%   { transform: translateY(0.10em); opacity: 1; }
  22%  { transform: translateY(0); opacity: 1; }
  100% { transform: translateY(-0.44em); opacity: 1; }
}
.ann.is-ascend.is-out .ann__ch { animation-name: chAscendOut; }
@keyframes chAscendOut {
  0%   { opacity: 1; transform: scale(1) skewX(var(--ui-skew)); }
  100% { opacity: 0; transform: scale(0.9) translateY(-1.1em) skewX(var(--ui-skew)); filter: blur(3px); }
}

/* ── THE ANNOUNCE RUNS ON THE GAME'S CLOCK, NOT THE BROWSER'S ────────────────
   Every announce keyframe is `paused` and pulled to its position by a negative
   `animation-delay` of `--ann-t`, which hud.js `scrubAnnounce()` writes each
   frame from the HUD's own dt (see the comment there).

   Why: `main.js __stepTo()` runs the deterministic capture by ticking the engine
   in a tight loop, so wall-clock advances by almost nothing while sim time
   advances by seconds. A wall-clock animation therefore froze every headless
   shot of a shout inside its first ~0.1s — and at a DIFFERENT 0.1s each run,
   depending on how fast the renderer happened to be. The 必殺 could not be
   reviewed from a capture at all, and two runs of the same `--t` disagreed. This
   is the contract the gauge already keeps with `--k` (stepGauge): the picture at
   `t` is a function of `t`.

   ⚠️ THIS BLOCK MUST STAY LAST IN §3.4. `animation-play-state` is part of the
   `animation` shorthand, so any shorthand written after this rule resets it to
   `running` and the scrub silently stops working.

   ⚠️ 2026-08-06: §0 の 13 本は本番でスクラブをやめた（毎フレームの :root 書き込みが
   オーナーの iPhone を 40〜53 秒で殺していた）が、**ここは両モードとも paused の
   まま**。理由は 3 つあって、どれも「同じだから揃える」より重い:
     · 書き先が違う。`--ann-t` は `.ann` 要素に書く。`--ui-t` は :root ＝ 継承の
       根なので、1 回の書き込みが**文書中の全要素**の計算スタイルを無効にする。
       この差が犯人と無罪を分けている。
     · 出ている間だけ。announce は一瞬で、`--ann-t` は表示中しか書かない。
       `--ui-t` は**タイトル画面から結果画面まで一度も止まらず**書き続けていた。
     · 実機の証拠がそう言っている。当日の診断フラグ `?off=uiClock`（＝ `--ui-t` の
       書き込み**だけ**を止める。役目を終えたので FLAGS.uiScrub に置き換え済み）で
       215 秒・8322 フレーム落ちなかった run では、被弾も KO も起きている ＝
       `--ann-t` は毎フレーム書かれていた。
   それに announce は有限長の一発ものなので、free-run にすると「いつ始まったか」が
   絵に出る（§0 の 13 本は全部 infinite ループなので出ない）。 */
.ann.is-in .ann__band,
.ann.is-in .ann__ch,
.ann.is-in .ann__sub,
.ann.is-in .ann__streak,
.ann.is-ascend.is-in .ann__chars,
.ann.is-out .ann__band,
.ann.is-out .ann__ch {
  animation-play-state: paused;
}

/* ═══════════════════════════════════════════════════════════════════════════
   4. touch controls — structure comes from core/input.js:
      .touch-stick > .touch-stick__knob
      .touch-pad   > button.touch-btn[data-a]
      NOTE: input.js writes `transform: translate(...)` on the knob every frame,
      so the knob must be centred with offsets, never with a transform.
   ⚠️ 2026-08-12 の並び替え（オーナー実機報告「右のボタンが押しにくいので、押しやすい
      レイアウトにして欲しい。特に投げるボタン。」）は**縦持ちの @media が全部持って
      いる**。この節の寸法・位置は横持ち／卓上のもので、1px も動いていない。
   ═══════════════════════════════════════════════════════════════════════════ */

.touch-stick, .touch-pad {
  position: absolute;
  z-index: 5;
  pointer-events: auto;
  touch-action: none;
  display: none;                 /* shown only on touch-capable pointers */
}

@media (pointer: coarse), (hover: none) {
  .touch-stick, .touch-pad { display: block; }
}

/**
 * ★ 2026-08-11・オーナー実機報告「スマホの縦表示だと画面上にボタンがあって見にくい」
 *   （＝ PRODUCT-PLAN O11「タッチ furniture のコート乗り」に裁定が下りた）に対する
 *   **1本目の手当て: 濃さを落とす**。手当ては 3 本ある — ①濃さ（この節と `.touch-btn`）
 *   ②寸法 ③隅寄せ（②③は縦持ちの節）。
 *
 * このスティックの実体は**ドラッグ領域**で、円盤の塗りは装飾でしかない（指の位置を
 * 伝えているのは金のノブ）。ところが塗りは中心から 62% まで rgba(10,6,18,**0.42**) ＝
 * 不透明度 4 割の黒で、円盤の下に居る選手が実際に暗く沈んでいた
 * （before の実測画像・390x844 縦持ち: スティック越しに味方が 1 人埋まっている）。
 *
 * だから **塗りと縁と落ち影だけを薄くし、ノブは 1bit も触らない**。
 *   塗り 0.42 -> 0.20 / 外周 0.18 -> 0.08 / 縁 0.28 -> 0.24 / 落ち影 0.45 -> 0.28
 * ⚠️ 寸法（＝当たり判定）はこの節では 1px も変えていない。縦持ちの縮小は下の
 *    `@media (orientation: portrait)` の節が持つ。
 * ⚠️ 効き目は `tools/thumbcover.mjs` が**被覆の実測（α 加重 px²）**で毎回数える。
 *    面積だけを測るゲートでは「薄くした」は 1px² も動かないので、あちらは既知の
 *    2 色の背景から画素ごとの α を割り出して測っている。
 */
.touch-stick {
  /* ★ 置き場所は**変数で持つ**（2026-08-11・浮かぶスティック）。下の
   *   `.touch-stick.is-float::after`（透明な当たり判定の帯）が「画面の左端・下端
   *   まで」を表すのに、円盤が端からどれだけ離れているかを知る必要があるため。
   *   向きごとの上書きは各 @media が `--stick-l` / `--stick-b` だけを書き換える
   *   ＝ 円盤の位置と帯の位置が**必ず同じ 1 つの値から出る**。 */
  --stick-l: 22px;
  --stick-b: 26px;
  --stick-zone-up: 96px;
  --stick-zone-right: 96px;
  left: calc(var(--sa-l) + var(--stick-l));
  bottom: calc(var(--sa-b) + var(--stick-b));
  width: 150px;
  height: 150px;
  border-radius: 50%;
  background:
    radial-gradient(circle at 50% 42%, rgba(255, 240, 210, 0.08) 0%, rgba(10, 6, 18, 0.20) 62%, rgba(10, 6, 18, 0.08) 100%);
  border: 2px solid rgba(255, 232, 190, 0.24);
  box-shadow:
    inset 0 6px 18px rgba(0, 0, 0, 0.30),
    0 6px 22px rgba(0, 0, 0, 0.28);
  overflow: visible;
}
/* cardinal ticks so the ring reads as a control, not a smudge */
.touch-stick::before {
  content: "";
  position: absolute;
  inset: 14px;
  border-radius: 50%;
  background:
    conic-gradient(from 0deg,
      rgba(255, 232, 190, 0.34) 0 3deg, transparent 3deg 87deg,
      rgba(255, 232, 190, 0.34) 87deg 93deg, transparent 93deg 177deg,
      rgba(255, 232, 190, 0.34) 177deg 183deg, transparent 183deg 267deg,
      rgba(255, 232, 190, 0.34) 267deg 273deg, transparent 273deg 360deg);
  -webkit-mask: radial-gradient(circle, transparent 62%, #000 66%, #000 100%);
  mask: radial-gradient(circle, transparent 62%, #000 66%, #000 100%);
}

/**
 * ★ 指を受ける帯（2026-08-11・オーナー実機報告「全体的に移動ボタンが押しにくい」）。
 *
 * **完全に透明**で、`background` も `border` も `box-shadow` も持たない ＝
 * `tools/thumbcover.mjs` が数える α 加重の被覆（cover / ink）に **1px² も乗らない**。
 * 2026-08-11 に減らしたコートの被覆はそのままで、指を置ける面積だけが増える。
 *
 * 大きさは「画面の左端と下端まで／上と右は親指の弧のぶん外へ」。実測（390x844・
 * ノッチ込み）で **8,700px² → 42,112px²（4.8 倍）**。
 * ⚠️ 起点は円盤の**パディング箱**（絶対配置の包含ブロック）なので、縁の 2px ぶん
 *    だけ画面端に届かない（実測で左端 x=2・下端 y=842）。親指には関係のない差。
 * ⚠️ この帯は `.is-float` が付いている間だけ生える。`src/ui/touchstick.js` が
 *    「指の所を原点にする」のと**同じスイッチ**（`FLAGS.floatstick`）で付け外し
 *    する ＝ 判定だけ広い固定スティック（帯の端に置くと即ダッシュ）は作れない。
 * ⚠️ `pointer-events` は親から継いで `auto`。擬似要素は事象の的にならないので、
 *    ここに置かれた指は `.touch-stick` の pointerdown として届く（＝ core/input.js
 *    の結線に 1 行も足さずに判定だけ広がる）。
 */
.touch-stick.is-float::after {
  content: "";
  position: absolute;
  left: calc(-1 * (var(--sa-l) + var(--stick-l)));
  bottom: calc(-1 * (var(--sa-b) + var(--stick-b)));
  top: calc(-1 * var(--stick-zone-up));
  right: calc(-1 * var(--stick-zone-right));
}

.touch-stick__knob {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 62px;
  height: 62px;
  margin-left: -31px;
  margin-top: -31px;
  border-radius: 50%;
  background:
    radial-gradient(circle at 38% 30%, #fff6e2 0%, var(--gold) 40%, #a8701c 100%);
  border: 2px solid rgba(30, 16, 8, 0.55);
  box-shadow:
    0 5px 12px rgba(0, 0, 0, 0.6),
    inset 0 -4px 8px rgba(120, 60, 10, 0.55),
    inset 0 3px 6px rgba(255, 255, 255, 0.45);
}

/* 2026-08-05: 4つ目（パス／しゃがみ）が入ったぶん背が伸びた 190 -> 220。
   幅は据え置き — 新しいボタンは 投 の**真上**に積んだので横へは広がらない。
   （横へ伸ばす案は縦型で死ぬ: 縦持ちのパッドは右から 200px を占め、スティックが
   左から 152px まで来るので、残りの隙間は 38px しか無い。） */
.touch-pad {
  right: calc(var(--sa-r) + 22px);
  bottom: calc(var(--sa-b) + 22px);
  width: 208px;
  height: 220px;
}

.touch-btn {
  position: absolute;
  display: grid;
  place-items: center;
  padding: 0;
  border-radius: 50%;
  font-family: var(--font-jp);
  font-weight: 900;
  color: #fff5e2;
  border: 2px solid rgba(20, 10, 6, 0.55);
  text-shadow: 0 2px 3px rgba(0, 0, 0, 0.8);
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.08s ease, filter 0.08s ease, box-shadow 0.08s ease, opacity 0.08s ease;
  box-shadow:
    0 6px 0 rgba(0, 0, 0, 0.45),
    0 8px 18px rgba(0, 0, 0, 0.5),
    inset 0 3px 8px rgba(255, 255, 255, 0.32);
  /* ★ 休んでいる間は**半透明**（2026-08-11・オーナー報告「見にくい」の手当て①）。
   *   ボタンは字を読ませる面なので、スティックのように塗りだけ薄くはできない
   *   （塗りを薄くすると字の下地が消えて字が読めなくなる）。要素ごと 0.74 まで
   *   落として、押した瞬間に 1.0 へ戻す ＝ **押した手応えが1つ増える**。
   *   0.74 は「白抜きの字が土のコートでも客席でも読める」下限として実測画像で
   *   選んだ値（0.6 まで落とすと 走 の青が背景に沈む）。 */
  opacity: 0.74;
}
.touch-btn::after {
  /* glassy top highlight */
  content: "";
  position: absolute;
  left: 14%; right: 14%; top: 8%;
  height: 34%;
  border-radius: 50%;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.5), transparent);
  pointer-events: none;
}
.touch-btn.is-on {
  transform: translateY(5px) scale(0.95);
  filter: brightness(1.5);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.45), inset 0 3px 10px rgba(0, 0, 0, 0.5);
  opacity: 1;
}

.touch-btn--action {
  right: 6px;
  bottom: 42px;
  width: 96px;
  height: 96px;
  font-size: 2rem;
  background: radial-gradient(circle at 38% 28%, #ff8b96 0%, var(--team-a) 42%, #8c1120 100%);
}
.touch-btn--jump {
  right: 96px;
  bottom: 126px;
  width: 68px;
  height: 68px;
  font-size: 1.4rem;
  background: radial-gradient(circle at 38% 28%, #ffe49b 0%, var(--gold) 44%, #8a5a10 100%);
  color: #1a0d05;
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.45);
}
/* ⚠️ `.touch-btn--dash`（走）は 2026-08-11 のオーナー裁定で**要素ごと消えた**
 *    （core/input.js の SPEC §2 例外・4件目。指のダッシュはスティックのスライド幅で
 *    出る）。規則だけ残すと「消えた要素の見た目」を延々と保守することになるので、
 *    ここも一緒に落とした。当たり判定・寸法・置き場所の算数は下の縦持ち／横持ちの
 *    節にも走ぶんが無い。 */
/**
 * パス／しゃがみ。ラベルは core/input.js の `setPassContext()` が
 * 「パ」⇄「屈」に書き換える（押した結果が文脈で変わるので、固定にすると
 * 押せる方の半分が画面から消える）。色は1つ — 意味を運ぶのは字であって、
 * 状態ごとに色まで動かすと押すたびにパッドが騒がしくなる。
 *
 * ★ 置き場所は 投 の真上。他の3つを 1px も動かさずに 68px の円が入る唯一の
 *   隙間で、中心間距離は 投 と 92.5px（必要 82・余白 10.5）、跳 と 88.8px
 *   （必要 68・余白 20.8）。左へ回した案は 跳 と 53.9px しか取れず重なる。
 *   代償はパッドが 22px 高くなること（画面下端から 216 -> 238px）。詰めるには
 *   既存3つを動かすことになるので、手触りの話としてオーナーに預ける。
 */
.touch-btn--pass {
  right: 10px;
  bottom: 148px;
  width: 68px;
  height: 68px;
  font-size: 1.4rem;
  background: radial-gradient(circle at 38% 28%, #b6f2c4 0%, #43a960 44%, #10502a 100%);
}

/* ═══════════════════════════════════════════════════════════════════════════
   5. screens
   ═══════════════════════════════════════════════════════════════════════════ */

.screens {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* A live HUD behind a title card is visual noise. Pause is the exception: the
   readout is exactly what the player paused to look at. */
#ui-root.screen-open .hud { opacity: 0; transition: opacity 0.22s ease; }
#ui-root.screen-open--pause .hud { opacity: 0.32; }
/* There is nothing to steer behind a menu — and the pad was sitting on top of
   PRESS START. The ID selector outranks the pointer media query below. */
#ui-root.screen-open .touch-stick,
#ui-root.screen-open .touch-pad { display: none; }

.screen {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  pointer-events: auto;
  /* Wipe reveal: the whole screen is clipped in from the leading edge. */
  clip-path: inset(0 0 0 0);
}
.screen.is-active { display: flex; }
.screen.is-in  { animation: screenIn var(--ui-screen-in) cubic-bezier(.2,.9,.25,1) both; }
.screen.is-out { animation: screenOut var(--ui-screen-out) cubic-bezier(.6,0,.9,.3) both; }
@keyframes screenIn {
  0%   { opacity: 0; clip-path: inset(0 100% 0 0); }
  100% { opacity: 1; clip-path: inset(0 0 0 0); }
}
@keyframes screenOut {
  0%   { opacity: 1; clip-path: inset(0 0 0 0); }
  100% { opacity: 0; clip-path: inset(0 0 0 100%); }
}

/* full-bleed scrim shared by every screen */
.screen__scrim {
  position: absolute;
  inset: 0;
  /* The second gradient is the TEXT BELT. Everything under the logo — tagline,
     PRESS START, the key hints — lives between roughly 48% and 72% of the
     screen, and behind it sit the green net, a red shirt and the .rays wedges,
     which swing the background luminance INSIDE a single line of type. This
     nails that band to a known darkness so the small type has one background
     instead of a moving one. */
  background:
    radial-gradient(120% 40% at 50% 62%, rgba(0, 0, 0, 0.58) 0%, transparent 100%),
    radial-gradient(120% 90% at 50% 42%, rgba(24, 12, 40, 0.34) 0%, rgba(6, 4, 12, 0.60) 82%);
}
/* THE FIRST SCREEN HAS TO SHOW THE PRODUCT. 原作のタイトル (refs/ref-07.png) puts
   the dirt court, the green net, six 2.5頭身 characters and the white ball under
   its logo — the whole promise of the game is made before you press anything.
   Ours put a 0.9-alpha curtain over the 3D and showed a corner of the school
   building, i.e. it promised a logo. The outer alpha drops to 0.60 so the world
   comes through; the text belt above keeps its own darkness, because the small
   type still needs one background instead of a moving one.

   ⚠️ The camera is scenes.js/H's, so this side alone cannot put people under the
   logo — raised for the integration owner in INTEGRATION-NOTES.md (menu シーンの
   カメラを原作 ref-07 相当の構図に). */
/* scanlines: subtle, 1px, only on the screens (never over live gameplay) */
.screen__scrim::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0.17;
  background: repeating-linear-gradient(180deg,
    rgba(0, 0, 0, 0.45) 0 1px, transparent 1px 3px);
}

/* rotating sunburst — the single most "紅蓮" piece of furniture we own */
.rays {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 200vmax;
  height: 200vmax;
  margin: -100vmax 0 0 -100vmax;
  background: repeating-conic-gradient(from 0deg,
    rgba(255, 190, 90, 0.085) 0deg 5deg, transparent 5deg 13deg);
  /* THE WEDGES ARE THE LOGO'S HALO, NOT A FILTER OVER THE PICTURE.
     Careful with these percentages: the element is 200vmax square and a `circle`
     radial-gradient sizes to its farthest corner, so at 1600×900 100% ≈ 2262px —
     the old outer stop of 62% was a 1400px radius, i.e. the rays covered every
     pixel of the frame and laid a rotating stripe pattern over the world the
     title screen is meant to be selling. 23% ≈ 520px puts the outer edge just
     past the logo and its subtitle. */
  -webkit-mask: radial-gradient(circle, transparent 4%, #000 11%, #000 17%, transparent 23%);
  mask: radial-gradient(circle, transparent 4%, #000 11%, #000 17%, transparent 23%);
  animation: raySpin var(--ui-ray-period) linear infinite;
  animation-delay: var(--ui-anim-delay);   /* §0: capture scrubs, production runs */
  animation-play-state: var(--ui-anim-state);
  pointer-events: none;
}
@keyframes raySpin { to { transform: rotate(360deg); } }

.screen__inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(10px, 2.2vh, 26px);
  padding: clamp(16px, 4vw, 48px);
  padding-top: calc(var(--sa-t) + clamp(16px, 4vw, 48px));
  padding-bottom: calc(var(--sa-b) + clamp(16px, 4vw, 48px));
  max-width: min(1180px, 96vw);
  width: 100%;
  text-align: center;
}

/* ─── 5.1 title ─────────────────────────────────────────────────────────── */

.logo {
  position: relative;
  display: inline-block;
  isolation: isolate;
}
/* A soft dark bed so the logo owns its patch of screen instead of fighting
   whatever ray happens to be behind it this frame. */
.logo::before {
  content: "";
  position: absolute;
  inset: -26% -16%;
  z-index: -1;
  background: radial-gradient(58% 62% at 50% 48%,
    rgba(6, 3, 12, 0.92) 0%, rgba(6, 3, 12, 0.62) 52%, transparent 76%);
  filter: blur(10px);
}

/* PAINT-ORDER TRAP (measured, not guessed): with `-webkit-background-clip:text`
   the glyph colour is the element's BACKGROUND, and backgrounds paint before
   negative-z children. A `z-index:-1` extrusion pseudo therefore lands ON TOP
   of the gradient and buries it. So the element itself carries the dark
   extrusion (real text, real stroke) and the hot face is an ::after ON TOP. */
.logo__main {
  position: relative;
  font-family: var(--font-disp);
  font-weight: 900;
  font-size: clamp(3.2rem, 13vw, 9.5rem);
  line-height: 0.92;
  letter-spacing: 0.03em;
  transform: skewX(var(--lean)) rotate(-1.6deg);
  color: #14060f;
  -webkit-text-stroke: 0.045em #14060f;
  paint-order: stroke fill;
  text-shadow:
    0.014em 0.016em 0 #4a0c1e, 0.028em 0.032em 0 #430b1b,
    0.042em 0.048em 0 #3b0a18, 0.056em 0.064em 0 #330815,
    0.070em 0.080em 0 #2b0712, 0.084em 0.096em 0 #23050f,
    0.098em 0.112em 0.02em #1a040b;
  filter: drop-shadow(0 14px 22px rgba(0, 0, 0, 0.85));
  animation: logoIdle var(--ui-logo-idle) ease-in-out infinite alternate;
  animation-delay: var(--ui-anim-delay);   /* §0: capture scrubs, production runs */
  animation-play-state: var(--ui-anim-state);
}
/* the hot face, sitting on the extrusion */
.logo__main::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  color: transparent;
  -webkit-text-stroke: 0;
  text-shadow: none;
  background-image: linear-gradient(176deg,
    #fffdf4 0%, #ffe97e 28%, #ffb62a 56%, #ff5a24 82%, #d2172f 100%);
  -webkit-background-clip: text;
  background-clip: text;
}
/* bloom, genuinely behind (the element paints real text, so -1 works here) */
.logo__main::before {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  z-index: -1;
  color: #ff8a2a;
  -webkit-text-stroke: 0.06em #ff6a1a;
  text-shadow: none;
  paint-order: stroke fill;
  filter: blur(15px);
  opacity: 0.6;
}
@keyframes logoIdle {
  from { transform: skewX(var(--lean)) rotate(-1.6deg) translateY(0) scale(1); }
  to   { transform: skewX(var(--lean)) rotate(-1.1deg) translateY(-6px) scale(1.014); }
}

.logo__en {
  margin-top: 0.35em;
  font-family: var(--font-disp);
  font-weight: 900;
  font-size: clamp(0.78rem, 2.5vw, 1.7rem);
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  color: var(--cream);
  transform: skewX(var(--lean));
  -webkit-text-stroke: 0.035em #1a0810;
  paint-order: stroke fill;
  text-shadow: 0 3px 0 #2a0d18, 0 0 22px rgba(255, 190, 90, 0.45);
}

.logo__rule {
  margin: clamp(12px, 2vw, 30px) auto 0;
  width: min(74%, 520px);
  height: 3px;
  background: linear-gradient(90deg, transparent, var(--gold) 18%, #fff 50%, var(--gold) 82%, transparent);
  box-shadow: 0 0 12px rgba(255, 190, 90, 0.75);
  transform: skewX(var(--lean));
}

/* THE BAND IS NOT DECORATION. Measured on the shipped build, 「全国制覇への道」
   sat on the green net and a red shirt with nothing behind it, and the hint row
   sat on a character's head and torso — both with the .rays wedges alternating
   light and dark INSIDE one line of text. A soft-edged strip of ink gives every
   glyph the same floor without drawing a hard-edged box on the title screen. */
.title__tagline {
  padding: 3px clamp(14px, 3vw, 34px) 4px;
  background: linear-gradient(90deg,
    transparent 0%, rgba(8, 5, 14, 0.72) 12% 88%, transparent 100%);
  font-size: clamp(0.86rem, 1.8vw, 1.12rem);
  font-weight: 800;
  letter-spacing: 0.34em;
  text-indent: 0.34em;
  color: var(--gold-hot);
  text-shadow: 0 2px 0 #2a0d18;
}

.press {
  margin-top: clamp(6px, 2vh, 20px);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  animation: pressBlink var(--ui-press-period) steps(1, end) infinite;
  animation-delay: var(--ui-anim-delay);   /* §0: capture scrubs, production runs */
  animation-play-state: var(--ui-anim-state);
}
.press__main {
  font-family: var(--font-disp);
  font-weight: 900;
  font-size: clamp(1rem, 2.8vw, 1.85rem);
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  color: #fff;
  transform: skewX(var(--lean));
  -webkit-text-stroke: 0.045em #1a0810;
  paint-order: stroke fill;
  text-shadow: 0 3px 0 #48101f, 0 0 24px rgba(255, 255, 255, 0.55);
}
.press__sub { font-size: 0.78rem; letter-spacing: 0.2em; color: var(--cream-dim); }
@keyframes pressBlink { 0%, 72% { opacity: 1; } 73%, 100% { opacity: 0.2; } }

/* 12px of key hints on a photograph of a court is not a control legend, it is a
   watermark. Same ink strip as the tagline, and a size that survives a TV
   viewing distance (>=15px, and 20px on a 1080p screen). */
.title__hint {
  margin-top: 2px;
  display: flex;
  gap: clamp(8px, 2vw, 22px);
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  padding: 5px clamp(14px, 3vw, 34px) 6px;
  background: linear-gradient(90deg,
    transparent 0%, rgba(8, 5, 14, 0.72) 12% 88%, transparent 100%);
  font-size: clamp(15px, 1.1vw, 20px);
  line-height: 1.5;
  letter-spacing: 0.06em;
  color: var(--cream);
  opacity: 1;
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.8);
}
.title__hint kbd {
  font-family: var(--font-num);
  font-weight: 900;
  font-size: 0.92em;
  padding: 2px 7px;
  margin-right: 5px;
  border-radius: 3px;
  border: 1px solid rgba(255, 227, 176, 0.55);
  background: rgba(0, 0, 0, 0.72);
  color: var(--gold-hot);
  text-shadow: none;
}

/* ── タイトルから設定へ（2026-08-11・PRODUCT-PLAN E19）───────────────────────
   語彙は難易度の札（`.diff__chip`）と同じ — 「いま選ばれているものが金で光る」は
   このゲームの選択の言葉で、タイトルだけ別の言葉を持ち込む理由が無い。
   44px の床は実機のタップのため（`.diff__chip` / `.sound__btn` と同じ）。 */
.title__sound {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55em;
  min-height: 44px;
  padding: clamp(5px, 1vh, 10px) clamp(16px, 3vw, 26px);
  cursor: pointer;
  border: 1px solid rgba(255, 227, 176, 0.28);
  border-radius: 3px;
  background: linear-gradient(180deg, rgba(30, 20, 34, 0.82), rgba(10, 6, 14, 0.86));
  box-shadow: inset 0 1px 0 rgba(255, 245, 220, 0.1);
  color: var(--cream-dim);
  transition: transform 0.16s cubic-bezier(.2,1.5,.4,1), filter 0.16s ease, color 0.16s ease;
}
.title__sound-jp {
  font-size: clamp(0.82rem, 1.8vw, 1.05rem);
  font-weight: 900;
  letter-spacing: 0.1em;
  white-space: nowrap;
}
.title__sound.is-sel {
  color: #1a0c06;
  border-color: color-mix(in srgb, var(--gold-hot) 85%, #fff);
  background: linear-gradient(180deg, var(--gold-hot) 0%, var(--gold) 62%, #a87326 100%);
  box-shadow: 0 2px 0 rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 235, 0.7);
  transform: translateY(-2px);
}
/* 金地の上では副題も継承（`.menu__item.is-sel .sound__en` と同じ）。 */
.title__sound.is-sel .title__sound-en { color: inherit; opacity: 0.82; }
.title__sound:active { transform: scale(0.97); }
/* カーソルが「おと」に居る間は PRESS START を引っ込める（screens.js の
   `applyTitleMenu` の註 — 押すと始まらないものが点滅していたら画面が嘘をつく）。
   ⚠️ `opacity` では効かない: `.press` は `pressBlink` が opacity を毎周期
      書き替えるので、静的な opacity はアニメーションに負ける。動かしていない
      性質（filter）で落とす。 */
.title.is-sound .press { filter: brightness(0.42) saturate(0.35); }

/* ─── 5.2 team select ───────────────────────────────────────────────────── */

.select__title {
  font-size: clamp(1.4rem, 4.2vw, 2.8rem);
  font-weight: 900;
  letter-spacing: 0.2em;
  text-indent: 0.2em;
  transform: skewX(var(--lean));
  color: var(--cream);
  -webkit-text-stroke: 0.03em #1a0810;
  paint-order: stroke fill;
  text-shadow: 0 4px 0 #48101f;
}
.select__sub { margin-top: -0.3em; font-size: clamp(0.6rem, 1vw, 0.8rem); opacity: 1; color: var(--gold); }

/* ── ★ 6 校ぜんぶ（2026-08-28・PRODUCT-PLAN E25）─────────────────────────────
 * 1 行 6 枚。**カードの高さは 2 枚時代と変わらない** — この画面でいちばん背の高い
 * 部品である紋章は `clamp(58px, 8vw, 96px)` ＝ 列の幅ではなく画面の幅に従うので、
 * 列を 2 から 6 に増やしても縦は 1px も伸びない（縦の予算は E8 の実測どおり既に
 * ギリギリで、ここで伸ばすと下の札の行から順に切れる）。gap は 2 枚時代のまま。 */
.select__row {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: clamp(10px, 2.4vw, 30px);
  width: 100%;
  margin-top: clamp(6px, 2vh, 18px);
}
/* screens.js の applySelection() が .select__row.is-focus をトグルするが、予約
   フック・現状装飾なし。カードの選択は .card.is-sel の呼吸で既に表現済みで、
   ここに同じ印を足すと二重になるため（5.2b .diff.is-focus の注記と同じ理由）。 */

.card {
  --accent: var(--team-a);
  --accent-lo: var(--team-a-lo);
  position: relative;
  padding: clamp(8px, 1.1vw, 14px);
  --notch: 16px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  transform: translateY(0) scale(0.965);
  filter: saturate(0.55) brightness(0.7);
  transition: transform 0.22s cubic-bezier(.2,1.5,.4,1), filter 0.22s ease;
}
/* 2026-08-28（E25）: `.card--b` を消した。`buildTeamCard()` がインラインで
   `--accent` を書くので、この規則は 2 校時代から一度も絵に効いていない
   （理由の全文は screens.js `buildTeamCard()` の★註）。 */
.card.is-sel {
  transform: translateY(-8px) scale(1);
  filter: none;
  border-color: color-mix(in srgb, var(--accent) 80%, #fff);
  box-shadow:
    0 2px 0 rgba(0, 0, 0, 0.55),
    0 16px 40px rgba(0, 0, 0, 0.6),
    0 0 34px color-mix(in srgb, var(--accent) 60%, transparent),
    inset 0 1px 0 rgba(255, 245, 220, 0.28);
  animation: cardBreathe var(--ui-select-pulse) ease-in-out infinite alternate;
  animation-delay: var(--ui-anim-delay);   /* §0: capture scrubs, production runs */
  animation-play-state: var(--ui-anim-state);
}
@keyframes cardBreathe { from { filter: brightness(1); } to { filter: brightness(1.16); } }

/* ── ★ 6 枚ぶんの場所を作る（2026-08-28・PRODUCT-PLAN E25）───────────────────
 *
 * E2 / E8 / E17b と同じ等価交換を 4 度目にやる。実測（scratchpad/e25/ab.mjs・
 * `.screen__inner` の上端。負 ＝ 画面の外へ出た px）:
 *
 *              2 枚（〜2026-08-28）  6 枚にした代金  あいての行の代金
 *   1600x900        +64.5              -50.0            -96.6
 *   1280x720         -1.1              -50.0            -90.2
 *    900x600        -13.7              -61.0            -80.8
 *    844x390         +1.8              -38.6            -76.2
 *    375x667       +101.9             -236.8            -80.8
 *    390x844       +143.2             -256.8            -84.4
 *
 * ＝ 1280x720 は **ちょうど 1.1px で収まっていた**ところへ 140px を積んだ。
 * 幅 620px 以下の桁違いの代金（237〜257px）は 3 列 x 2 行になるぶんで、
 * カード 1 段まるごとが増えている。
 *
 * ★ **落とすのはカードの「余白と装飾の寸法」だけ**。名前も紋章も一言も背番号も
 *   ステータス棒も 1 つも消さない（消す取引は E2/E8 が 2 度使い切っていて、
 *   3 度目を 1280x720 級のノート PC でやらないと E17b が決めている）。
 *   紋章を一段小さくするのは削減であると同時に**構図の修正**でもある: 札 1 枚が
 *   156px（1280 幅・実測）しかないところに 96px の紋章は大きすぎた。
 * ★ 英字の校名（`.card__en`）だけは例外で、**6 列では必ず 2 行に折り返す**
 *   （実測 28.7px）。E8 が「この画面で最も情報量の少ない画素」として英字の副題を
 *   落としたのと同じ判断を、同じ英字に対してやる。漢字の校名は残る。 */
/* Crest: layered CSS shield in the team colours. */
.crest {
  position: relative;
  width: clamp(46px, 5.2vw, 78px);
  aspect-ratio: 1 / 1.14;
  clip-path: polygon(50% 0, 100% 18%, 100% 66%, 50% 100%, 0 66%, 0 18%);
  background: linear-gradient(170deg,
    color-mix(in srgb, var(--accent) 60%, #fff) 0%, var(--accent) 38%, var(--accent-lo) 100%);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.6);
  display: grid;
  place-items: center;
}
.crest::before {
  content: "";
  position: absolute;
  inset: 8%;
  clip-path: inherit;
  border: 2px solid rgba(255, 245, 220, 0.6);
}
.crest__mark {
  position: relative;
  font-family: var(--font-jp);
  font-weight: 900;
  font-size: clamp(1.5rem, 3.4vw, 2.6rem);
  color: var(--cream);
  transform: skewX(var(--lean));
  text-shadow: 0 3px 0 rgba(0, 0, 0, 0.55);
}

.card__jp {
  font-size: clamp(0.86rem, 1.7vw, 1.15rem);
  font-weight: 900;
  letter-spacing: 0.12em;
  text-shadow: 0 3px 0 rgba(0, 0, 0, 0.7);
  /* ── ★ 2 行ぶんを先に確保する（2026-08-28・PRODUCT-PLAN E25）────────────────
   * 6 枚並ぶと 1 枚は 156px（1280 幅・実測）になり、**「みどり台中学」だけが
   * 6 文字で 2 行に折り返す**。カードの高さはグリッドが最も高い 1 枚に揃える
   * ので、この 1 枚が折り返した時点で高さの代金は既に払っている — 折り返さない
   * 5 枚だけが 1 行ぶん上に浮いて、紋章の下から先（一言・背番号・棒）が横に
   * 揃わなくなる、という見え方だけの損になっていた。
   * だから**全部の札で 2 行ぶんを確保して中央に置く**。増える高さは 0 で、
   * 揃わない 5 枚が揃う。文字を小さくする案（6 文字が 1 行に入るまで縮める＝
   * 1.5rem → 1rem 相当）は採らなかった: この画面の主役の字を 3 分の 2 にする
   * 代金の方が、確保する 1 行より高い。 */
  min-height: 2.4em;
  display: grid;
  place-items: center;
}
/* 6 列では必ず 2 行に折り返す（上の★）。漢字の校名と紋章が残るので「どの高校か」
   は 1 つも失われない。 */
.card__en { display: none; }
/* `min-height` は `.card__jp` と同じ理由（2026-08-28・E25）— 一言の長さで
   折り返しの行数が変わると、その札だけ背番号と棒が下へずれる。 */
.card__note { font-size: 0.76rem; color: var(--cream-dim); letter-spacing: 0.06em; min-height: 2.4em; }

/* 6 人の背番号が 1 行に収まる寸法（6 列だと 2 行に折り返して 39.2px 使っていた）。 */
.card__numbers { display: flex; flex-wrap: wrap; gap: 3px; justify-content: center; }
.card__numbers span {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-weight: 900;
  font-size: 0.66rem;
  min-width: 1.5em;
  padding: 1px 0;
  border-radius: 2px;
  background: rgba(0, 0, 0, 0.5);
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--cream);
}

.card__stats { width: 100%; display: grid; gap: 5px; }
/* ── ★ 選んでいる学校のパラメータ（2026-08-29・スマホ用）────────────────────
 * 既定は**出さない**。デスクトップは札 1 枚が 156px あって `.card__stats` の棒が
 * 読める幅にあるので、同じものを 2 箇所に出す理由がない。出すのは縦持ちだけ
 * （下の `max-width: 620px` / `min-height: 521px` の節）。 */
.select__stats { display: none; }
/* 縦持ちのスマホでだけ出す。条件が 2 つあるのは、幅が狭いだけでは足りないから
   （背の低い横長はどの案も入らない・理由は `max-width: 620px` の節の★）。 */
@media (max-width: 620px) and (min-height: 521px) {
  .select__stats {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 3px clamp(8px, 3vw, 16px);
    width: 100%;
    max-width: 520px;
    margin: 0 auto;
    /* 選んでいる札の色で塗る（どの札のものかを色が言う）。`--accent` は
       applySelection() が毎回書く。 */
    --accent: var(--team-a);
    --accent-lo: var(--team-a-lo);
  }
  /* この幅ではラベルを 3.4em も取れない（棒が痩せる）。ラベルは 4 文字
     「キャッチ」「ど根性」が入る最小に詰める。 */
  .select__stats .stat { grid-template-columns: 4.2em 1fr; gap: 6px; }
  .select__stats .stat__label { font-size: 0.62rem; letter-spacing: 0.06em; }
  .select__stats .stat__bar { height: 8px; }
}
.stat { display: grid; grid-template-columns: 3.4em 1fr; align-items: center; gap: 8px; }
.stat__label { font-size: 0.66rem; letter-spacing: 0.14em; color: var(--cream-dim); text-align: left; }
.stat__bar {
  position: relative;
  height: 7px;
  border-radius: 2px;
  overflow: hidden;
  background: rgba(0, 0, 0, 0.6);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.9);
}
.stat__bar i {
  position: absolute;
  inset: 0;
  transform-origin: left center;
  background: linear-gradient(90deg, var(--accent-lo), var(--accent) 60%, #fff);
}

/* ─── 5.2b difficulty（2026-08-09）─────────────────────────────────────────
 *
 * チーム選択の 2 段目。選ばれている段だけ金で塗る。
 *
 * ★ 段の色に学校の accent を借りない。難易度はどちらの高校を選んでも同じ軸なので、
 *   accent を使うと「青嵐の むそう」と「紅蓮の むそう」が別物に見える。
 * ★ どの行に**カーソルが居るか**は `.diff.is-focus` が言う（キーボード／パッドの
 *   ための情報で、指で押す人には要らない）。カードの行に同じ印を足すと
 *   `.card.is-sel` の呼吸と二重になるので、カード側は既存の見た目のまま。
 * ★ 札の高さは 44px を割らない（実機のタップの床。触るものを増やすときの
 *   このリポの決まり — style.css §4 のタッチ furniture と同じ）。
 */
.diff {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(3px, 0.7vh, 8px);
}
.diff__head { display: flex; align-items: baseline; gap: 0.7em; }
.diff__title {
  font-size: clamp(0.78rem, 1.5vw, 1.05rem);
  font-weight: 900;
  letter-spacing: 0.16em;
  color: var(--cream);
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.7);
}
.diff__en { font-size: clamp(0.5rem, 0.9vw, 0.7rem); color: var(--gold); }

.diff__row {
  display: grid;
  /* 段数は AI.levels（DIFFS）依存で固定 4 ではない。auto-fit は札の無い
     列を 0 幅に潰す(min が 0 でも実測4/5段とも従来の等分レイアウトと同じ幅になる —
     scratchpad/grid-test.html で検証済み)ので、段が増減してもここは無改修でよい。 */
  grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
  gap: clamp(5px, 1.1vw, 14px);
  width: 100%;
  max-width: 760px;
}
.diff__chip {
  position: relative;
  min-height: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding: clamp(4px, 0.8vh, 9px) 4px;
  cursor: pointer;
  border: 1px solid rgba(255, 227, 176, 0.28);
  border-radius: 3px;
  background: linear-gradient(180deg, rgba(30, 20, 34, 0.82), rgba(10, 6, 14, 0.86));
  box-shadow: inset 0 1px 0 rgba(255, 245, 220, 0.1);
  color: var(--cream-dim);
  transition: transform 0.16s cubic-bezier(.2,1.5,.4,1), filter 0.16s ease, color 0.16s ease;
}
.diff__jp {
  font-size: clamp(0.7rem, 1.5vw, 1rem);
  font-weight: 900;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.diff__lv {
  font-family: var(--font-disp);
  font-weight: 900;
  font-size: clamp(0.4rem, 0.75vw, 0.6rem);
  letter-spacing: 0.18em;
  opacity: 0.78;
}
.diff__chip.is-sel {
  color: #1a0c06;
  border-color: color-mix(in srgb, var(--gold-hot) 85%, #fff);
  background: linear-gradient(180deg, var(--gold-hot) 0%, var(--gold) 62%, #a87326 100%);
  box-shadow: 0 2px 0 rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 235, 0.7);
  transform: translateY(-2px);
}
/* カーソルがこの行に居るときだけ、選ばれている札に光の輪を足す。 */
.diff.is-focus .diff__chip.is-sel {
  box-shadow:
    0 2px 0 rgba(0, 0, 0, 0.55),
    inset 0 1px 0 rgba(255, 255, 235, 0.7),
    0 0 0 2px rgba(255, 240, 200, 0.9),
    0 0 22px rgba(255, 190, 90, 0.55);
}
.diff.is-locked .diff__chip { cursor: default; filter: saturate(0.55); }
.diff__note {
  min-height: 1.15em;
  font-size: clamp(0.5rem, 0.9vw, 0.68rem);
  letter-spacing: 0.06em;
  color: var(--gold);
}

/* 空の注記は場所を取らない（2026-08-09・E8）。字が入るのは止まっている行だけ
   （`?ai=` ／ かちぬきせん）なので、**常に 2 行ぶん予約すると 25px（1600x900）が
   何も言っていない余白**になる。⚠️ 等価交換: 止めた瞬間に下の行が注記 1 行ぶん
   下がる（元の min-height はその跳ねを止めるためのものだった）。3 行に増えた
   この画面では、跳ねないことより**入り切ること**が先（1280x720 は札の行を足した
   だけで 30.4px はみ出す。実測 scratchpad/e8/probe-fit.mjs）。 */
.diff__note:empty { min-height: 0; }

/* ─── 5.2c しあいの しゅるい（2026-08-09・PRODUCT-PLAN E8）────────────────────
 *
 * 勝ち抜き戦を足したので選択画面に 3 行目が生えた。★ **見た目は難易度の行と同じ族**
 * （同じ `.diff` ブロックを付ける）— どちらも「札を 1 枚えらぶ」で、1 つの画面が
 * 選択の語彙を 2 つ持つ理由が無い。違うのは修飾子だけ:
 *     `.diff--level` … 4 段（`AI.levels`）
 *     `.diff--mode`  … 2 枚。4 枚ぶんの幅に 2 枚並べると札 1 枚が横長すぎるので、
 *                      行の幅だけ詰める（結果画面の 2 択と同じ 520px の考え方）。
 * ⚠️ 修飾子は**ゲートが行を名指しできるようにする**ためでもある（`.diff__chip` が
 *    2 行ぶんに増えたので、`tools/difficulty.mjs` は `.diff--level` で絞って読む）。 */
.diff--mode .diff__row { max-width: min(520px, 100%); }

/* ─── 5.2d コート（2026-08-10・PRODUCT-PLAN E17b）──────────────────────────────
 *
 * 特殊コート「こおり」を製品の入口に出したので 4 行目が生えた。★ 見た目は上の 2 行と
 * **同じ族**（同じ `.diff` ブロック・同じ 2 枚幅）。増えたのは修飾子だけで、
 * `tools/courtselect.mjs` はこの `.diff--court` で行を名指しする。 */
.diff--court .diff__row { max-width: min(520px, 100%); }

/* ── あいて（2026-08-28・PRODUCT-PLAN E25）──────────────────────────────────
 * 同じ `.diff` の族。ただし札が **7 枚**（おまかせ + 6 校）なので、`.diff--mode` /
 * `.diff--court` が 2 枚のために絞っている 520px は使わず、行の既定の 760px を
 * そのまま使い切る。1 枚 ≒108px で、`TEAMS[id].short` の最長「みどり台」（4 字）が
 * `white-space: nowrap` のまま収まる幅（config の `short` の註に実測）。 */
.diff--rival .diff__row { max-width: min(760px, 100%); }
/* 自チームと同じ学校の札。行ごと止まっているわけではないので `.diff.is-locked`
   とは別 — **1 枚だけが掴めない**ことを言う（キーもポインタも飛ばす側の実装は
   screens.js `rivalDisabled()`）。`.is-sel` は同時に付かない（applySelection が
   掴めない札を選んだままにしない）ので、金の札と喧嘩することはない。 */
.diff__chip.is-off {
  cursor: default;
  opacity: 0.34;
  filter: saturate(0.25);
}

/* 札の 2 行を 1 つの塊にまとめる箱（screens.js `picks` の註）。内側の間隔は
   `.screen__inner` の gap（1600x900 で 19.8px）より小さい固定値で足りる — 2 行は
   同じ「えらぶ」の族なので、他の塊と同じ距離で離す理由が無い。 */
.select__picks {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(2px, 0.6vh, 6px);
  width: 100%;
}

/* ── 4 行目を足しても縦が伸びない組み方（2026-08-10・E17b）─────────────────────
 *
 * コートの行を素直に下へ積むと、**この画面はどの横長でも入り切らなくなる**。
 * 実測（scratchpad/e17b/fit-after1.json・札を積んだ直後の `.screen__inner` の上端）:
 *
 *     1280x720   -25.0px（`?ai=` で -37.5）   932x430  -9.8px（-19.4）
 *      844x390   -19.0px（-28.2）             667x375  下で実測
 *
 * ＝ 上は見出しが切れ、下は札が画面の外へ出る。E2・E8 はここで**中身を落とす**
 * 等価交換（カードの数字とステータスバー／見出しの大きさと英字の副題）を取ったが、
 * 3 度目の同じ取引をすると 1280x720 級のノート PC からカードの中身が消える。
 *
 * ★ 代わりに**横を使う**。しゅるいとコートはどちらも 2 枚組（`max-width: 520px`）で、
 *   横長の画面には 2 つ並べる幅が余っている（1280 幅で 1 列 520px + 520px でも余る）。
 *   落とすものが 1 つも無く、増える高さも 0 — この画面で唯一「無から出てくる」場所が
 *   横方向の余白だった。難易度だけは 4 枚組なので上段を丸ごと使う。
 * ⚠️ 縦持ち（幅 640px 未満）では今までどおり縦に積む。2 枚組を 2 つ横に並べると
 *    1 枚が 80px 級になり、「かちぬきせん」が札から溢れる。縦持ちは縦に余裕がある
 *    （390x844 で 72.9px・360x640 は下の註）ので、積んで入る。
 * ⚠️ カーソルの行は 4 段のまま（screens.js `ROW_*`）。↓ が右へ動いて見えるのは
 *    この 1 箇所だけで、行の意味も並びも 1 つも動かない。 */
@media (min-width: 640px) {
  .select__picks {
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-items: start;
    column-gap: clamp(10px, 2vw, 28px);
  }
  /* 4 枚組の難易度は上段を丸ごと。しゅるい（左）とコート（右）が下段に並ぶ。 */
  .diff--level { grid-column: 1 / -1; }
  /* あいては 7 枚組（2026-08-28・E25）なので、難易度と同じく行を丸ごと使う。
     ⚠️ これを書き忘れると 2 列組みの左セルに落ちて、札が 63px まで潰れたうえ
        行ごと左へ寄る（実測・scratchpad/e25/select-1280x720.png の1枚目）。
     ★ **ここは高さを増やさない**: 相方になれる 2 枚組が余っていないので上段を
       増やすしかなく、その 1 行ぶんは下の `max-height` 区間で返す。 */
  .diff--rival { grid-column: 1 / -1; }
}

/* ── 背の低い縦持ち（2026-08-10・E17b）────────────────────────────────────────
 *
 * 幅 640px 未満は上の 2 列に入らない（2 枚組を 2 つ並べると 1 枚が 80px 級になり
 * 「かちぬきせん」が札から溢れる）ので 4 行を縦に積む。積むと背の低い電話だけが
 * 入り切らない。実測（同 fit-after2.json・`.screen__inner` の上端）:
 *
 *     360x640  -5.7px（`?ai=` で -14.9）    375x667  +6.8px（-2.4）
 *     390x844 は +72.9px（`?ai=` で +63.7）＝ ここは何も落とさない
 *
 * 落とすのは背の低い横長（`max-height: 520px`）とまったく同じ 2 つ — **英字の副題**と
 * **カードの内側の余白**。文字も紋章も札も 1 つも落とさない（44px の床も、どちらの
 * 高校かも、段の名前も残る）。 */
@media (max-width: 639px) and (max-height: 700px) {
  .select__sub { display: none; }
  .select__row { margin-top: 0; }
  .card { gap: 6px; padding: clamp(8px, 1.4vw, 14px); }
}

/* 背の高いデスクトップ以外は塊の間隔も詰める（2026-08-09・E8）。
 * 実測（scratchpad/e8/probe-fit2.mjs・`?ai=` で注記が 2 本とも出る最悪ケース）:
 *     1280x720  中身が画面より **18.8px 高い**（上で見出しが 9.4px 切れる）
 * `.screen__inner` の gap は 1280x720 で 15.8px × 3 箇所 ＝ 47.5px あり、ここを
 * 8.6px にすると 21.6px 返る（→ -2.8px で収まる）。★ 条件が **max-height: 800px**
 * なのは、承認済みの絵（1600x900）を 1px も動かさないため。 */
@media (max-height: 800px) {
  .select .screen__inner { gap: clamp(8px, 1.2vh, 14px); }
  /* ── ★ 6 枚 + あいての行のぶん（2026-08-28・E25）─────────────────────────────
   * E8 が背の低い横長（`max-height: 520px`）だけでやっていた交換を、**背の低い
   * 横長より一段広い区間まで**引き上げる。落とすものは 1 つも増えていない
   * （英字の副題と見出しの大きさ ＝ E8 が「この画面で最も情報量の少ない画素」と
   * 判定した 2 つ）。実測でここが要るのは 1280x720 と 900x600 で、承認済みの絵
   * （1600x900）は `max-height: 800px` の外なので 1px も動かない。 */
  .select__title { font-size: clamp(0.9rem, 2.6vw, 1.5rem); }
  .select__sub { display: none; }
  /* 行の上マージン（既定 clamp(6px,2vh,18px)）も返す。E8 が `max-height: 520px` で
     同じ 1 手を取っている（そちらの★の内訳の最後の 7.8px）。 */
  .select__row { margin-top: 0; }
  /* 札の塊の中の間隔も詰める（4 行になったので 3 箇所ぶん効く）。 */
  .select__picks { gap: 2px; }
  /* 行の見出しと、行の中の 3 段（見出し / 札 / 注記）の間隔も詰める。**落とすものは
     無い** — 行の名前も英字も注記も残り、44px の札の床にも触らない。4 行ぶんに
     効くので、ここが `?ai=` と `?court=` の注記が同時に出た最悪の回を救う。 */
  .diff__title { font-size: clamp(0.7rem, 1.2vw, 0.9rem); }
  .diff { gap: 2px; }
}

/* ─── 5.3 pause ─────────────────────────────────────────────────────────── */

.pause .screen__scrim { background: rgba(5, 3, 10, 0.72); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px); }

.pause__panel {
  padding: clamp(20px, 3vw, 40px) clamp(28px, 5vw, 68px);
  --notch: 18px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(10px, 2vh, 22px);
}
.pause__title {
  font-size: clamp(1.6rem, 5vw, 3rem);
  font-weight: 900;
  letter-spacing: 0.24em;
  text-indent: 0.24em;
  transform: skewX(var(--lean));
  -webkit-text-stroke: 0.03em #1a0810;
  paint-order: stroke fill;
  text-shadow: 0 4px 0 #48101f;
}
.menu { display: flex; flex-direction: column; gap: 8px; min-width: min(320px, 70vw); }
.menu__item {
  position: relative;
  /* ★ 44px はこのリポの決まり（実機のタップの床）。実測 43.2px で **0.8px 足りて
     いなかった** — 札は最初から `pointerdown` を持つ触るものなので、E4 で
     「おと」を足したときのゲート（tools/volumegate.mjs A 節）が落として気づいた。
     見た目の変化は 1 札あたり 0.8px。 */
  min-height: 44px;
  box-sizing: border-box;
  padding: 10px 22px 10px 2.6em;
  font-size: clamp(0.9rem, 2vw, 1.15rem);
  font-weight: 800;
  letter-spacing: 0.16em;
  color: var(--cream-dim);
  background: rgba(0, 0, 0, 0.4);
  border: 1px solid var(--line);
  clip-path: polygon(10px 0, 100% 0, calc(100% - 10px) 100%, 0 100%);
  cursor: pointer;
  transition: color 0.14s, background 0.14s, transform 0.14s cubic-bezier(.2,1.5,.4,1);
}
.menu__item.is-sel {
  color: #180a06;
  background: linear-gradient(90deg, var(--gold-hot), var(--gold));
  transform: translateX(8px);
  box-shadow: 0 0 22px rgba(255, 190, 90, 0.6);
}
.menu__item.is-sel::before {
  content: "▶";
  position: absolute;
  left: 0.85em;
  top: 50%;
  margin-top: -0.62em;
  color: #180a06;
  animation: cursorBob var(--ui-menu-bob) ease-in-out infinite alternate;
  animation-delay: var(--ui-anim-delay);   /* §0: capture scrubs, production runs */
  animation-play-state: var(--ui-anim-state);
}
/* ...which never existed. The rule above has named `cursorBob` since this file
   was written and no such keyframe was ever declared, so the pause menu's arrow
   has been sitting perfectly still while a line of CSS claimed it was moving.
   Found by the same sweep that pulled the hard-coded seconds out. */
@keyframes cursorBob {
  from { transform: translateX(0); }
  to   { transform: translateX(3px); }
}

/* ─── 5.3b 音量設定（2026-08-09・PRODUCT-PLAN E4）───────────────────────────
 *
 * ポーズの中の小部屋。器（`.pause__panel` / `.menu` / `.menu__item`）はポーズと
 * **同じものを使い回す** — 選択の語彙（選ばれている札が金で光り ▶ が付く）を
 * 画面ごとに作り直さない。ここに足すのは行の中身（名前・± ・メモリ・数字）だけ。
 *
 * ★ 触るものは 44px を割らない（実機のタップの床）。行そのものと ± の両方。
 * ★ メモリの本数は CSS で決めない（`SETTINGS.volumeSteps` から DOM が作られる）。
 *   ここは幅を等分するだけなので、段数を変えても崩れない。
 */
.sound__panel { min-width: min(430px, 88vw); }

.sound__list { width: 100%; min-width: 0; }

.sound__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(8px, 2vw, 18px);
  min-height: 44px;
  /* ポーズの札は左に ▶ のぶんの空きがある（padding-left 2.6em）。同じ器なので
     同じ空きを保つ — ここだけ詰めると、カーソルが行を跨いだときに字が横へ跳ねる。 */
  padding: 6px 14px 6px 2.6em;
}
.sound__label { display: flex; align-items: baseline; gap: 0.55em; min-width: 0; }
.sound__jp { font-size: clamp(0.82rem, 1.8vw, 1.05rem); letter-spacing: 0.1em; white-space: nowrap; }
.sound__en {
  font-family: var(--font-disp);
  font-size: clamp(0.44rem, 0.8vw, 0.62rem);
  letter-spacing: 0.16em;
  opacity: 0.8;
}
/* 選ばれている札は金地になる（`.menu__item.is-sel`）ので、副題の色は継承させる。 */
.menu__item.is-sel .sound__en { color: inherit; }

.sound__ctrl { display: flex; align-items: center; gap: clamp(5px, 1.2vw, 10px); }

.sound__btn {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.1rem;
  font-weight: 900;
  line-height: 1;
  color: var(--cream);
  background: rgba(0, 0, 0, 0.42);
  border: 1px solid var(--line);
  border-radius: 3px;
  cursor: pointer;
  user-select: none;
  transition: background 0.12s, transform 0.12s cubic-bezier(.2,1.5,.4,1);
}
.sound__btn:active { transform: scale(0.94); background: rgba(255, 190, 90, 0.28); }
.menu__item.is-sel .sound__btn { color: #180a06; border-color: rgba(24, 10, 6, 0.5); background: rgba(255, 255, 235, 0.5); }

.sound__meter {
  display: flex;
  gap: 2px;
  width: clamp(74px, 17vw, 132px);
}
.sound__tick {
  flex: 1 1 0;
  height: 14px;
  background: rgba(255, 240, 210, 0.16);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.45);
}
.sound__tick.is-lit { background: linear-gradient(180deg, var(--gold-hot), var(--gold)); }
/* 金地の札の上では、点いていない目盛が地に溶ける。暗い側で見せる。 */
.menu__item.is-sel .sound__tick { background: rgba(24, 10, 6, 0.28); }
.menu__item.is-sel .sound__tick.is-lit { background: #2a1206; }

.sound__value {
  min-width: 3.6em;
  text-align: right;
  font-size: clamp(0.6rem, 1.2vw, 0.8rem);
  letter-spacing: 0.06em;
  white-space: nowrap;
}
/* ミュートは字が長い（4 文字）ので、数字より一段小さく詰める。 */
.sound__row.is-mute .sound__value { font-size: clamp(0.5rem, 1vw, 0.68rem); }

.sound__row--back { justify-content: flex-start; }

.sound__hint {
  min-height: 1.15em;
  font-size: clamp(0.5rem, 0.9vw, 0.68rem);
  letter-spacing: 0.06em;
  color: var(--gold);
}

/* ─── 5.4 result ────────────────────────────────────────────────────────── */

.result__banner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.result__verdict {
  font-size: clamp(2.6rem, 10vw, 7rem);
  font-weight: 900;
  line-height: 0.92;
  letter-spacing: 0.06em;
  transform: skewX(var(--lean));
  color: transparent;
  background-image: linear-gradient(176deg, #fffdf2 0%, #ffe58c 30%, #ffab26 58%, #ff4a2c 100%);
  -webkit-background-clip: text;
  background-clip: text;
  filter: drop-shadow(0 5px 0 #58101f) drop-shadow(0 12px 20px rgba(0, 0, 0, 0.8));
}
.result.is-lose .result__verdict {
  background-image: linear-gradient(176deg, #e8ecff 0%, #9fb0d8 34%, #5a6a96 70%, #2b3552 100%);
  filter: drop-shadow(0 5px 0 #1a2036) drop-shadow(0 12px 20px rgba(0, 0, 0, 0.8));
}
.result__verdict-en {
  font-family: var(--font-disp);
  font-weight: 900;
  font-size: clamp(0.8rem, 2.4vw, 1.5rem);
  letter-spacing: 0.44em;
  text-indent: 0.44em;
  color: var(--gold-hot);
  transform: skewX(var(--lean));
  text-shadow: 0 2px 0 #2a0d18;
}

.result__score {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: clamp(10px, 2.4vw, 32px);
  width: 100%;
  max-width: 760px;
  padding: clamp(8px, 1.4vw, 16px) clamp(12px, 2vw, 26px);
  --notch: 14px;
}
.rscore { --accent: var(--team-a); display: flex; align-items: center; gap: 12px; }
.rscore--b { --accent: var(--team-b); flex-direction: row-reverse; }
.rscore__name { font-size: clamp(0.8rem, 1.8vw, 1.15rem); font-weight: 800; letter-spacing: 0.08em; white-space: nowrap; }
.rscore__n {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-weight: 900;
  font-size: clamp(2.2rem, 6vw, 4rem);
  line-height: 0.9;
  color: #fff;
  transform: skewX(var(--lean));
  text-shadow: 0 0 0.5em color-mix(in srgb, var(--accent) 85%, transparent), 0 0.05em 0 #1a0810;
}
.rscore--win .rscore__n { color: var(--gold-hot); }
.result__dash { font-family: var(--font-num); font-weight: 900; font-size: clamp(1.4rem, 3vw, 2.2rem); opacity: 0.55; }
.rscore__n.is-bump { animation: scoreBump var(--ui-score-bump) cubic-bezier(.2,1.7,.4,1); }

.mvp {
  display: flex;
  align-items: center;
  gap: clamp(10px, 2vw, 20px);
  padding: clamp(8px, 1.4vw, 14px) clamp(14px, 2.4vw, 26px);
  --notch: 12px;
  --accent: var(--team-a);
  --accent-lo: var(--team-a-lo);
}
.mvp__crown {
  font-family: var(--font-disp);
  font-weight: 900;
  font-size: clamp(0.7rem, 1.5vw, 1rem);
  letter-spacing: 0.3em;
  color: #180a06;
  background: linear-gradient(90deg, var(--gold-hot), var(--gold));
  padding: 3px 10px 3px 14px;
  clip-path: polygon(8px 0, 100% 0, calc(100% - 8px) 100%, 0 100%);
}
.mvp .mug { width: clamp(48px, 5vw, 68px); }
.mvp__meta { text-align: left; display: flex; flex-direction: column; gap: 3px; }
.mvp__name { font-size: clamp(0.95rem, 2vw, 1.3rem); font-weight: 900; letter-spacing: 0.06em; }
.mvp__line { display: flex; gap: 14px; }
.mvp__stat { display: flex; align-items: baseline; gap: 4px; }
.mvp__stat b {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: 1.25em;
  color: var(--gold-hot);
}
.mvp__stat span { font-size: 0.62rem; letter-spacing: 0.18em; color: var(--cream-dim); }

/* The stats table gets the SAME plate and the SAME 760px as the score above it.
   Before, the densest block on the screen was the only bare one: .rrow's
   rgba(0,0,0,0.34) measured (51,37,36) against a court of (59,34,34), so it was
   not a backing at all — every row's contrast swung with whatever white line or
   player was passing underneath. It was also 860px wide, i.e. WIDER than the
   plates above it, which inverted the hierarchy of the whole screen. */
.result__table {
  width: 100%;
  max-width: 760px;
  padding: clamp(10px, 1.6vw, 18px);
  --notch: 14px;
}
.result__rows {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(8px, 1.6vw, 22px);
  width: 100%;
  max-width: none;
}
.result__col { display: grid; gap: 4px; align-content: start; }
/* ── THE TALLY IS NOT A SPREADSHEET ──────────────────────────────────────────
   It used to be 26px rows of 14px type where HP was a bare integer (21/0/0/…)
   and life or death was a 極小の ○ / ×. Two failures in one block: the screen
   that closes the match used NONE of the visual language the rest of the game
   is built from (a countable segmented gauge), and it spent more area than the
   score plate above it at the smallest type size on screen, which inverts the
   hierarchy of the whole page. Now: number column, name left, the same gauge as
   the board, and the dead are drawn as what they became — 天使. */
.rrow {
  display: grid;
  /* Widths are budgeted so a 5-glyph Japanese name fits WITHOUT an ellipsis at
     1600px: the name is the identity of the row and is the last thing allowed
     to lose space. */
  grid-template-columns: 2em minmax(0, 1fr) auto 2.1em;
  gap: 6px;
  padding: 5px 9px;
  align-items: center;
  font-size: clamp(1.12rem, 1.25vw, 1.45rem);
  /* On a plate, the row backing's job is banding, not opacity — a light stripe
     over a dark plate is stable no matter what the court does. */
  background: rgba(255, 255, 255, 0.045);
  border-left: 3px solid var(--line);
  opacity: 0;
  transform: translateX(-18px);
}
.result.is-shown .rrow { animation: rowIn var(--ui-result-row-in) cubic-bezier(.2,1.3,.4,1) both; animation-delay: var(--d, 0s); }
@keyframes rowIn { to { opacity: 1; transform: translateX(0); } }
.rrow--head {
  background: rgba(255, 255, 255, 0.10);
  color: var(--cream);
  font-size: clamp(0.72rem, 0.86vw, 0.98rem);
  letter-spacing: 0.10em;
  font-weight: 800;
  opacity: 1;
  transform: none;
  animation: none !important;
}
/* 「状態」 is two glyphs and it must stay on one line — a wrapped header turns the
   last column into a two-storey label taller than the rows it heads. */
.rrow--head b, .rrow__no-head { white-space: nowrap; }
.rrow--a { border-left-color: var(--team-a); --accent: var(--team-a); --accent-lo: var(--team-a-lo); }
.rrow--b { border-left-color: var(--team-b); --accent: var(--team-b); --accent-lo: var(--team-b-lo); }
.rrow b { font-family: var(--font-num); font-variant-numeric: tabular-nums; text-align: right; }

/* Shirt number: fixed column, so the names below it all start on one edge. */
.rrow__no {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-weight: 900;
  font-size: 0.86em;
  text-align: right;
  color: var(--gold-hot);
}
.rrow__no::before { content: "#"; font-size: 0.72em; opacity: 0.7; margin-right: 0.1em; }
.rrow__name {
  text-align: left;
  font-weight: 800;
  letter-spacing: 0.02em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
/* The gauge is the readout; the integer is a footnote to it. */
.rrow__hp { display: flex; align-items: center; gap: 7px; }
.rrow__hpnum {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-weight: 900;
  font-size: 0.72em;
  min-width: 1.9em;
  text-align: right;
  color: var(--cream-dim);
}
.gauge--result { gap: 2px; }
.gauge--result .tick {
  --tick-w: clamp(3px, 0.28vw, 5px);
  --tick-h: clamp(10px, 0.86vw, 15px);
}
.gauge--result .tick:not(.is-lit) { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14); }

/* ── 状態 = 天使 ─────────────────────────────────────────────────────────────
   The dead used to be a single '×'. Three characters who grew wings and rose off
   the court thirty seconds earlier were filed as typographic errata. Alive is
   not drawn at all any more — the gauge already says it. */
.rstate { position: relative; width: 1.6em; height: 1.6em; justify-self: center; }
.rstate--down::before {
  content: "";
  position: absolute;
  left: 34%;
  top: 2%;
  width: 32%;
  height: 20%;
  border-radius: 50%;
  border: 2px solid var(--gold-hot);
  box-shadow: 0 0 6px rgba(255, 226, 170, 0.8);
}
.rstate--down::after {
  content: "";
  position: absolute;
  left: 4%;
  top: 28%;
  right: 4%;
  bottom: 8%;
  background: linear-gradient(180deg, #ffffff 0 46%, #dfe7ff 46% 100%);
  clip-path: polygon(50% 40%, 36% 10%, 14% 4%, 0 30%, 6% 66%, 30% 88%, 50% 66%,
    70% 88%, 94% 66%, 100% 30%, 86% 4%, 64% 10%);
  filter: drop-shadow(0 0 3px rgba(255, 246, 214, 0.9));
}
.rrow.is-down .rrow__name { color: var(--cream-dim); opacity: 0.72; }
.rrow.is-down .rrow__no { color: var(--dead); }

/* ── 5.4b リザルトの 2 択（2026-08-09・PRODUCT-PLAN E3）──────────────────────
 *
 * ここは PRESS START の 1 行だった（押すと必ずタイトルへ）。2 択にしたので、
 * 選択の語彙は難易度の札（5.2b）と揃える — 選ばれている方が金で光り、光の輪が
 * カーソルの居場所を言う。結果画面だけ別の言い方をする理由が無い。
 *
 * ★ 横並び。縦に積むと `max-height: 520px`（背の低い横長）で下が画面外へ出る
 *   — その画面は既に .result__table を落としてまで縦を作っている区画なので、
 *   新しい行を縦に足すのは同じ問題をもう一度作ることになる。
 * ★ 44px を割らない（実機のタップの床。触るものを増やすときのこのリポの決まり）。 */

/* 札と注記で 1 つの塊にする。`.screen__inner` の gap は縦を最も食う値
   （1600x900 で 26px）なので、別々に置くと塊 1 つぶん余計に取る — 実測でこの
   1 手が縦持ち 390x844 で 18.6px を返した。
   ⚠️ `.story__actions`/`.story__menu`/`.story__item*`/`.story__note`
   （2026-08-13・PRODUCT-PLAN E20）は**見た目だけ**をここへ合流させたもの。
   ストーリー画面（`buildStory`）の 2 択は result 画面と同じ語彙で見せたいが、
   クラスまで `.result__*` に揃えると `document.querySelectorAll('.result__item')`
   （`tools/rematch.mjs` ほか複数の既存ゲート）が非表示の story 画面ぶんまで
   拾ってしまう（全画面が最初から同じ DOM に居るこの実装の性質）。クラスは
   画面ごとに分け、見た目はセレクタを束ねて共有する。 */
.result__actions, .story__actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 100%;
}
.result__menu, .story__menu {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(8px, 1.6vw, 18px);
  width: 100%;
  max-width: min(520px, 90vw);
}
.result__item, .story__item {
  position: relative;
  min-height: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding: clamp(5px, 0.9vh, 10px) 6px;
  cursor: pointer;
  border: 1px solid rgba(255, 227, 176, 0.28);
  border-radius: 3px;
  background: linear-gradient(180deg, rgba(30, 20, 34, 0.82), rgba(10, 6, 14, 0.86));
  box-shadow: inset 0 1px 0 rgba(255, 245, 220, 0.1);
  color: var(--cream-dim);
  transition: transform 0.16s cubic-bezier(.2,1.5,.4,1), filter 0.16s ease, color 0.16s ease;
}
.result__item-jp, .story__item-jp {
  font-size: clamp(0.82rem, 1.7vw, 1.1rem);
  font-weight: 900;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.result__item-en, .story__item-en {
  font-family: var(--font-disp);
  font-weight: 900;
  font-size: clamp(0.42rem, 0.78vw, 0.62rem);
  letter-spacing: 0.18em;
  opacity: 0.78;
}
/* カーソルはこの行にしか居ないので、選ばれている札は常に光の輪を持つ
   （難易度は 2 段あるので .diff.is-focus で条件付きだった。ここは 1 行きり）。 */
.result__item.is-sel, .story__item.is-sel {
  color: #1a0c06;
  border-color: color-mix(in srgb, var(--gold-hot) 85%, #fff);
  background: linear-gradient(180deg, var(--gold-hot) 0%, var(--gold) 62%, #a87326 100%);
  box-shadow:
    0 2px 0 rgba(0, 0, 0, 0.55),
    inset 0 1px 0 rgba(255, 255, 235, 0.7),
    0 0 0 2px rgba(255, 240, 200, 0.9),
    0 0 22px rgba(255, 190, 90, 0.55);
  transform: translateY(-2px);
}
/* ⚠️ 幅から決める clamp をそのまま持ってこない。`.diff__note` と同じ
   `clamp(0.5rem, 0.9vw, 0.68rem)` にすると縦持ち 390px で **8px** になる — この
   リポは同じ失敗を `.title__hint` で既にやっている（「12px の操作説明はコートの
   写真に貼った透かしであって legend ではない」）。この一行は毎回の結果画面で
   全員が読む説明なので、床を 11px に置く。
   ★ 墨の帯も同じ理由（`.title__tagline` の註）。金の小さな字は明るいコートの上で
     文字ごとに背景が変わる — 帯を敷くと全グリフが同じ床を持つ。 */
.result__note, .story__note {
  min-height: 1.15em;
  padding: 2px clamp(10px, 2vw, 22px) 3px;
  background: linear-gradient(90deg,
    transparent 0%, rgba(8, 5, 14, 0.72) 12% 88%, transparent 100%);
  font-size: clamp(11px, 1.1vw, 14px);
  letter-spacing: 0.06em;
  color: var(--gold);
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.8);
}

/* ── 勝ち抜き戦の一行（2026-08-09・PRODUCT-PLAN E8）───────────────────────────
 *
 * 「つぎの あいて: つよい」／「ぜんこく せいは！ 4にん ぬいた」／「かちぬきせん
 * おわり ・ 2にん ぬいた」の 1 行。**通常の試合では字が入らず `is-hidden` で
 * display:none** ＝ 連戦を遊ばない人の結果画面は 1px も動かない。
 *
 * ★ 注記（`.result__note`）より 1 段強い字にする。あちらは「押すと何が起きるか」の
 *   説明で、こちらは**試合の結末そのもの**（読み飛ばされると連戦が進んでいることに
 *   気づかない）。墨の帯・11px の床は同じ理由で共通（`.result__note` の註）。
 * ★ 演出はこれだけ。凝った制覇演出はオーナー判断（PRODUCT-PLAN E8「演出は最小限」）。 */
.result__gauntlet {
  padding: 2px clamp(10px, 2vw, 22px) 3px;
  background: linear-gradient(90deg,
    transparent 0%, rgba(8, 5, 14, 0.78) 12% 88%, transparent 100%);
  font-size: clamp(13px, 1.5vw, 19px);
  font-weight: 900;
  letter-spacing: 0.1em;
  color: var(--gold-hot);
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.85);
}
.result__gauntlet.is-hidden { display: none; }
/* 制覇の回だけ一段大きく。⚠️ **アニメーションは足さない** — この画面の動くものは
   `--ui-anim-delay` / `--ui-anim-state` の撮影スクラブ契約（§0）に全部乗っており、
   そこに乗らない動きを 1 つ足すと `?capture=1` の絵が撮る時刻で変わる（uiclock）。 */
.result.is-clear .result__gauntlet {
  font-size: clamp(15px, 2vw, 24px);
  color: #fff2cf;
}

/* ── ストーリーの一行（2026-08-13・PRODUCT-PLAN E20）───────────────────────────
 *
 * `.result__gauntlet` と**同じ見た目**（読む人にとっては同じ「進行の一行」なので
 * 別の語彙を持ち込まない）。ノードだけ分けてある理由は `applyStoryLine()` の註。 */
.result__story {
  padding: 2px clamp(10px, 2vw, 22px) 3px;
  background: linear-gradient(90deg,
    transparent 0%, rgba(8, 5, 14, 0.78) 12% 88%, transparent 100%);
  font-size: clamp(13px, 1.5vw, 19px);
  font-weight: 900;
  letter-spacing: 0.1em;
  color: var(--gold-hot);
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.85);
}
.result__story.is-hidden { display: none; }
.result.is-story-clear .result__story {
  font-size: clamp(15px, 2vw, 24px);
  color: #fff2cf;
}

/* ── かちぬきタイム（2026-08-09・PRODUCT-PLAN E12）─────────────────────────────
 *
 * 「きろく 3:24 じこベスト！」の 1 行。**制覇した回だけ**出る（`is-hidden` ＝
 * display:none なので、それ以外の結果画面は 1px も動かない）。
 *
 * ★ 上の一行（`.result__gauntlet`）より**弱い**字にする。制覇したことを言うのは
 *   あちらで、こちらはその添え物 — 同じ大きさで 2 行並べると、どちらが結末なのか
 *   一目で読めなくなる。だから帯は共通・字は `.result__note` 側の階調に置く。
 * ★ 数字だけ 1.25em（`.record__n` と同じ手）。遠目で「何分何秒か」が字ではなく数で
 *   読める。⚠️ アニメーションは足さない（`.result.is-clear` の註と同じ・撮影の
 *   スクラブ契約に乗らない動きを作らない）。 */
.result__time {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 0 0.5em;
  padding: 1px clamp(10px, 2vw, 22px) 2px;
  background: linear-gradient(90deg,
    transparent 0%, rgba(8, 5, 14, 0.72) 12% 88%, transparent 100%);
  font-size: clamp(11px, 1.1vw, 14px);
  font-weight: 800;
  letter-spacing: 0.06em;
  color: var(--cream-dim);
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.8);
}
.result__time.is-hidden, .result__time-best.is-hidden { display: none; }
.result__time-n {
  font-size: 1.25em;
  line-height: 1;
  color: var(--gold-hot);
}
/* 自己ベストの一言だけは金で光らせる（この行でいちばん嬉しい報せなので、
   数字と同じ強さを持たせる）。 */
.result__time-best { color: var(--gold-hot); font-weight: 900; }

/* タイトルの通算に足す自己ベスト（中黒ごと 1 つの塊。`buildClearCell` の註）。 */
.record__clear { display: inline-flex; align-items: baseline; gap: 0 0.55em; }
.record__clear.is-hidden { display: none; }

/* ★ 一行を足したぶんの場所を作る（背の低いデスクトップだけ）。
 *
 * 実測（tools/gauntletgate.mjs C 節・1280x720）: 一行が出ると 2 択の下端が
 * **747.3px ＝ 画面より 27.3px 下**へ出る ＝ 指で「つぎへ」が押せない。落とすのは
 * 選手一覧 — `max-width: 620px` と `max-height: 520px` が既に取っているのと**同じ
 * 等価交換**（この画面でいちばん密で、いちばん一目で読めない塊が落ち、勝敗・スコア・
 * MVP・2 択は残る）。
 * ⚠️ 条件を `.result.is-gauntlet`（＝勝ち抜きの一行が出ている回）に絞ってあるので、
 *    **通常の試合の結果画面は 1px も動かない**（resultmenu / recordsgate が測って
 *    いる絵はそのまま）。1600x900 は一行が出ても収まるので落とさない。
 * ⚠️ `.result.is-story`（2026-08-13・PRODUCT-PLAN E20）も同じ理由で足す —
 *    ストーリーの一行も `.result__actions` の中に居る同じ高さの塊なので、
 *    背の低いデスクトップでは同じだけ 2 択を画面外へ押し出す。 */
@media (max-height: 800px) {
  .result.is-gauntlet .result__table, .result.is-story .result__table { display: none; }
}

/* ── 5.4c 通算成績（2026-08-09・PRODUCT-PLAN E5）──────────────────────────────
 *
 * 結果画面（いま戦った段の通算＋いまの連勝）とタイトル（全段の合計＋最長連勝）で
 * **同じ 1 つの見た目**。読む人にとっては同じ「積み上がった数」なので、画面ごとに
 * 別の語彙を持ち込まない（2 択を難易度の札と同じ族にしたのと同じ判断）。
 *
 * ★ 字の床は 11px。`.result__note` と同じ理由（`.title__hint` の 12px を「コートの
 *   写真に貼った透かし」と呼んだのはこのリポ自身）。数字だけ 1.25em で持ち上げて、
 *   遠目でも「何勝何敗か」が字ではなく数で読めるようにする。
 * ★ `flex-wrap: wrap` は逃げ道。3 桁になった人（100しょう）や幅の狭い実機で、
 *   横へはみ出す代わりに 2 行になる — はみ出すと画面の外へ出て**読めなくなる**が、
 *   折り返せば縦に余っている画面（縦持ちは 520px 余る）が吸収する。
 * ★ 触るものではない（タップ先ではない）ので 44px の床は掛からない。 */
.record {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 0 0.55em;
  font-size: clamp(11px, 1.1vw, 14px);
  letter-spacing: 0.06em;
  color: var(--cream-dim);
}
.record.is-hidden { display: none; }
.record__diff { color: var(--gold-hot); font-weight: 800; }
.record__tag { opacity: 0.78; }
.record__sep { opacity: 0.45; }
/* `record__clear` の tail に glue した中黒（`buildClearCell` の註・検品②）用。
   制覇していないうちは `record__clear` と揃えて隠す（`applyTitleRecord`）。 */
.record__sep.is-hidden { display: none; }
.record__cell { display: inline-flex; align-items: baseline; gap: 0.2em; }
.record__unit { opacity: 0.86; }
.record__n {
  font-size: 1.25em;
  line-height: 1;
  color: var(--gold-hot);
}

/* 墨の帯。金の小さな字は明るいコートの上で文字ごとに背景が変わる
   （`.result__note` と `.title__tagline` の註と同じ）。 */
.result__record, .title__record {
  padding: 2px clamp(10px, 2vw, 22px) 3px;
  background: linear-gradient(90deg,
    transparent 0%, rgba(8, 5, 14, 0.72) 12% 88%, transparent 100%);
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.8);
}
/* タイトルは標語の直下。標語との間隔は `.screen__inner` の gap がすでに持って
   いるので、ここでは何も足さない（足すと 1 行ぶん縦が増える）。 */
.title__record { max-width: 100%; }

/* ─── 5.5 story（トーナメント表・2026-08-13・PRODUCT-PLAN E20）────────────────────
 *
 * 器は `.pause__panel plate`（設定画面と同じ使い回し）。5 行の表 ＋ 相手紹介 ＋
 * 2 択（`.story__actions`/`.story__menu`/`.story__item` — 見た目は結果画面の
 * 2 択と共有するがクラスは分ける。理由は 5.4b 節冒頭の ⚠️）を 1 枚に収める必要が
 * あるので、行 1 つあたりの高さを詰める — 表の行は**タップ先ではない**
 * （済／いま／まだ を見るだけの行）ので 44px の床は掛からない。
 */
.story__panel {
  min-width: min(560px, 92vw);
  max-width: min(640px, 94vw);
  gap: clamp(8px, 1.6vh, 16px);
}

.story__list {
  display: flex;
  flex-direction: column;
  gap: 5px;
  width: 100%;
}
.story__row {
  display: flex;
  align-items: center;
  gap: clamp(6px, 1.4vw, 14px);
  min-height: 30px;
  box-sizing: border-box;
  padding: 4px clamp(8px, 1.6vw, 16px);
  border: 1px solid var(--line);
  border-radius: 3px;
  background: rgba(0, 0, 0, 0.34);
  color: var(--cream-dim);
  text-align: left;
  transition: background 0.14s, border-color 0.14s, opacity 0.14s;
}
/* 済んだ戦は薄く沈める（一覧のいちばん読みたい行＝いま、から目を逸らさない）。 */
.story__row.is-done { opacity: 0.5; }
/* いま挑む戦は金の縁取りで浮かせる（`.diff__chip.is-sel` と同じ語彙・帯は使わない
   — この行はタップ先ではないので選択の金地までは持ち込まない）。 */
.story__row.is-now {
  border-color: var(--gold);
  background: rgba(90, 56, 10, 0.30);
  color: var(--cream);
  box-shadow: 0 0 14px rgba(255, 190, 90, 0.22);
}
.story__status {
  flex: 0 0 auto;
  min-width: 3.4em;
  font-size: clamp(0.56rem, 1.1vw, 0.72rem);
  font-weight: 800;
  letter-spacing: 0.06em;
  opacity: 0.85;
}
.story__row.is-now .story__status { color: var(--gold-hot); opacity: 1; }
.story__body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.story__event {
  font-size: clamp(0.68rem, 1.4vw, 0.9rem);
  font-weight: 800;
  letter-spacing: 0.02em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.story__tags { display: flex; gap: 6px; }
.story__tag {
  font-size: clamp(0.5rem, 0.95vw, 0.62rem);
  letter-spacing: 0.04em;
  opacity: 0.78;
}
.story__tag:not(:last-child)::after { content: "・"; margin-left: 6px; opacity: 0.6; }

/* いま挑む戦の相手紹介（2〜3 行）。攻略のヒントを兼ねる — PRODUCT-PLAN E20 節。 */
.story__rival {
  width: 100%;
  box-sizing: border-box;
  padding: 8px clamp(10px, 2vw, 20px) 9px;
  background: linear-gradient(90deg,
    transparent 0%, rgba(8, 5, 14, 0.72) 8% 92%, transparent 100%);
  text-align: left;
}
.story__rival-head {
  font-size: clamp(0.6rem, 1.15vw, 0.78rem);
  font-weight: 800;
  letter-spacing: 0.08em;
  color: var(--gold-hot);
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.8);
}
.story__rival-lines { margin-top: 3px; }
.story__rival-line {
  font-size: clamp(0.58rem, 1.05vw, 0.74rem);
  line-height: 1.4;
  color: var(--cream-dim);
}
/* 特訓の予告（2026-08-13・PRODUCT-PLAN E21④）。相手紹介の下に足す3〜4行目。
   語彙は同じ `.story__rival-line`（別の色・字を持ち込まない）、区切りだけ付ける。 */
.story__rival-line--training { margin-top: 6px; }

/* ─── 5.5b 物語の地の文だけの画面（2026-08-13・PRODUCT-PLAN E21②③⑤）───────────
 *
 * 導入（`storyintro`）・試合の間の出来事（`storybeat`）・結末（`storyend`）の
 * 3枚。器は `.pause__panel plate`（story/settings と同じ使い回し）。中身は
 * 段落（`.story-note__para`）の積み重ねだけで、2択もカーソルも無い —
 * 「どこを触っても進む」1枚（タイトルの入り方と同じ語彙）。
 */
.story-note__panel {
  min-width: min(480px, 90vw);
  max-width: min(560px, 92vw);
  gap: clamp(10px, 2vh, 20px);
}
.story-note__body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  text-align: left;
}
.story-note__para { display: flex; flex-direction: column; gap: 2px; }
/* 段落の間だけ余白を空ける（オーナー承認済みの本文にある空行の表現）。 */
.story-note__para.has-gap { margin-top: clamp(10px, 2vh, 18px); }
.story-note__line {
  font-size: clamp(0.72rem, 1.7vw, 0.92rem);
  line-height: 1.6;
  letter-spacing: 0.02em;
  color: var(--cream-dim);
}
.story-note__hint {
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--cream-dim);
  opacity: 0.7;
  text-align: center;
}
/* パネル全体がタップ対象に見えるように（`.title` の「どこを触っても始まる」と
   同じ約束）。 */
.storyintro, .storybeat, .storyend { cursor: pointer; }

/* ═══════════════════════════════════════════════════════════════════════════
   6. responsive
   ═══════════════════════════════════════════════════════════════════════════ */

/* Tablet / small laptop: shed the English subtitles and tighten the top band. */
@media (max-width: 900px) {
  .side__en { display: none; }
  .hud__top {
    gap: 6px;
    padding: calc(var(--sa-t) + 6px) calc(var(--sa-r) + 9px) 16px calc(var(--sa-l) + 9px);
  }
  .row__name { width: 4.9em; }
  .pc { left: calc(var(--sa-l) + 10px); bottom: calc(var(--sa-b) + 10px); min-width: 190px; }
  .title__hint { display: none; }
}

/* Phone: pips shrink to bars-only, the player card loses its mugshot detail. */
@media (max-width: 620px) {
  .hud { font-size: 12px; }
  .side__alive-label { display: none; }
  .side__jp { font-size: 0.7rem; letter-spacing: 0.02em; }
  .side__score { font-size: 1.15rem; min-width: 1.1em; }
  .side__head { padding: 4px 8px 4px 5px; gap: 5px; }
  .side--b .side__head { padding: 4px 5px 4px 8px; }
  .clock { min-width: 78px; padding: 3px 10px 5px; }
  .clock__time { font-size: 1.6rem; }
  /* The names stay — they ARE the board. The bench is what goes. */
  .row__name { width: 4.4em; font-size: 0.78rem; }
  .tick { --tick-w: 5px; --tick-h: 12px; }
  .gauge { gap: 2px; }
  .side__benchwrap { display: none; }
  .pc { min-width: 0; width: min(58vw, 260px); padding-right: 12px; }
  .pc__role { display: none; }
  /* 6 枚は 3 列 x 2 行へ折り返す（2026-08-28・E25）。この区間は下の行で背番号・
     ステータス棒・一言を落としているので、1 枚が「紋章 + 校名 + 役割語」だけに
     なる ＝ 2 段でも 2026-08-28 まで（1 列 x 2 枚の背の高い札）と同じ高さに
     収まる（実測: 375x667 で上端 +42.0px・360x640 で +28.5px）。 */
  .select__row { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .result__rows { grid-template-columns: 1fr; }
  .card__stats, .card__numbers { display: none; }
  /* ── ★ 代わりに「選んでいる学校」の 4 項目を札の外に 1 枚出す（2026-08-29）───
   *
   * 札の中の棒を戻す案は**測って捨てた**: 1 枚 107px（390 幅）の中でラベルが
   * 幅を食い、棒が **27px**（360 幅では 17.5px）の切れ端になる。出しても比べ
   * られないので、要望（チームごとのパラメータが分かる）を満たさない。
   * 外に 1 枚出すと同じ縦の代金で棒が **89〜104px** になる（約 4 倍・実測）。
   *
   * 2 x 2 に畳むのは 4 段より安いから。実測（`?ai=`+`?court=` で注記が 3 本出る
   * 最悪の回・`.screen__inner` の上端）:
   *     4 段  390x844 +37.0 / 375x667 +14.0 / 360x640 **+0.5**
   *     2 x 2 390x844 +53.7 / 375x667 +30.6 / 360x640 **+17.1**
   * 4 段のほうが棒は 251〜280px と読みやすいが、360x640 が +0.5px ＝ 次に何か
   * 足したら壊れる。2 x 2 なら 3 サイズとも余裕を持って収まる。
   *
   * ⚠️ 代金は**役割語（PLAYER / RIVAL）**で払う。札 6 枚 x 2 段ぶんの行が消える。
   *    「どれが自分か」は選ばれた札が光って浮くこと（`.card.is-sel`）と、真下の
   *    パネルがその札の色で塗られることが言う。`.card__note` の textContent は
   *    `display: none` でも残るので `tools/teamselect.mjs` の読み方は変わらない。
   * ⚠️ `min-height: 521px` の条件は**背の低い横長を外すため**。あちらは
   *    どの案も入らない（現状ですら 667x375 は最悪の回に -0.2px はみ出す）。 */
  .card__note { display: none; }
  .diff--rival .diff__lv { display: none; }
  /* （旧註）あいての札の英字を落とす（2026-08-28・E25）。この幅では 7 枚の札が 1 枚
     44.6px（実測）になり、`SHIRATORI` が札からはみ出して隣とくっついて見える。
     漢字の校名は残るので「どの学校か」は 1 つも失われない。 */
  /* 一言は落とす（2026-08-28・E25）。役割語も 2026-08-29 に落とした（上の★）。
     3 列 x 2 行になるこの区間では一言が 2 行に折り返して 1 枚あたり 28〜42px
     使う（実測）。「どちらが自分か」は役割語とカーソルの光り方が言うので、
     落として構図が壊れるのは一言の方だけ。
     ⚠️ 一言を落としたら `min-height` も一緒に外す。あれは「一言の長さで行数が
        変わっても札どうしがずれない」ための予約で、一言が無ければ全札とも
        1 行に揃う ＝ 予約は 1 行ぶんの無駄になる（実測: 390x844 でカード 2 段
        ぶん 29.6px・844x390 で 14.8px を余計に食っていた）。 */
  .card__flavour { display: none; }
  .card__note { min-height: 0; }
  .card.is-sel { transform: translateY(-3px) scale(1); }

  /* ── 結果の表を落とす（2026-08-09・PRODUCT-PLAN E3）─────────────────────────
   *
   * 幅の狭い画面では上の行で `.result__rows` が **1 列**になるので、14 行の表が
   * この画面でいちばん縦を食う。実測（scratchpad/e3-shots.mjs・390x844 縦持ち）:
   *
   *     2 択を足す前   中身が画面より **75.0px 高い**（見出しが上で 37.5px 切れる）
   *     2 択を足した後  **93.2px**（さらに札が下端で切れ、押せる場所が画面の外）
   *
   * 前者は前からある崩れだが、後者は**触れるものが画面の外に出る** ＝ 指しか
   * 持っていない人が再戦できない、で意味が違う。背の低い横長（max-height: 520px）が
   * 既に取っているのと同じ等価交換をここでも取る: 勝敗・スコア・MVP・2 択は残り、
   * 選手一覧の表が落ちる（この画面でいちばん密で、いちばん一目で読めない塊）。 */
  .result__table { display: none; }
}

/* Portrait phone: the touch furniture owns the bottom, so the card goes above. */
@media (orientation: portrait) and (pointer: coarse) {
  .pc { bottom: calc(var(--sa-b) + 200px); }

  /* ═══ 親指の furniture を縮めて画面の隅へ寄せる ══════════════════════════════
   *     （2026-08-11・オーナー実機報告「スマホの縦表示だと画面上にボタンが
   *      あって見にくいです」＝ PRODUCT-PLAN O11 に裁定が下りた）
   *
   * ── 何が起きていたか（実測・390x844・ノッチ込み）───────────────────────────
   * 縦持ちの内野は y 392.7（遠エンドライン）〜 729（近エンドライン）に写り、その帯は
   * **画面の幅いっぱい**を占める。親指の furniture は y 596〜788 に居たので、
   * 帯の下 133px ぶんに丸ごと乗っていた ＝ **見えているコートの 23,481px²**
   * （360x640 で 21,783px²・framecheck の `overlapThumbPx2`）。オーナーの
   * 「見にくい」はこの数字そのもの。
   *
   * ── なぜ「コートの外に出す」で解けないか（算数・緩めた言い訳ではない）────────
   * 近エンドライン 729 から下は、セーフエリアを引くと **81px** しか無い
   * （844 − 34(sa-b) − 729）。スティック単体で 130px・パッドは 196px なので、
   * 4 つを縦に積んだままでは**入らない**。横へ逃げる余地も無い（幅 390 に対し
   * スティック 130 ＋ パッド 178 ＝ 308 で、残りは 82px）。
   * ⚠️ 「コートを上げて下に操作帯を作る」は**却下**。2026-08-05 にオーナーが
   *    「縦持ちの画面が小さくて見にくい。元の縮尺に戻して欲しい」で縮尺を優先すると
   *    決めた要件そのもので、カメラは SPEC §カメラ構図の確定値（不可侵）。
   * ⚠️ 「4 つを横 1 列にして 81px の帯へ出す」も**却下**。幅は足りる（4 個 ×52 ＝ 208）が、
   *    親指が描く弧から外れる**操作系の作り直し**で、手触りはオーナーの領分。
   *
   * ── 取った手（乗る面積を減らす）─────────────────────────────────────────────
   *   ② 縮める … スティック 130 -> 104 ／ 投 84 -> 74 ／ 跳走パ 60 -> 52
   *   ③ 隅へ寄せる … スティック 左22/下26 -> 左12/下10 ／ パッド 右22/下22 -> 右12/下10
   *   （①「濃さを落とす」は §4 の `.touch-stick` / `.touch-btn` が持つ）
   * ⚠️ **44px のタップの床は割らない**。いちばん小さいボタンで 52px、スティックは
   *    ドラッグ領域なので 104px。`tools/thumbcover.mjs` が毎回**実測の矩形で**
   *    数えていて、44px を割った瞬間に赤になる（`--break=tiny` がその証明）。
   *
   * ★ 2026-08-11 の裁定で「走」が消えて **3 つ**になった（core/input.js の SPEC §2
   *   例外・4件目）。残る 3 つは 1px も動かしていない — 走が居たのはパッドの最下段で、
   *   他の 3 つはそこを起点に置かれていないため。
   *
   * ── ボタン同士の中心間距離（単位 px・必要値は半径の和）────────────────────────
   *     投-跳 71.4 / 63    投-パ 69.6 / 63    跳-パ 70.9 / 52
   *   （走が居たときのいちばんの詰まりは 跳-走 の 59.2 / 52 ＝ 余裕 7.2px だった。
   *    消えたので、いま詰まっているのは 投-パ の 6.6px）
   * ── パッドの外接 = 幅 **120px** × 高さ 136px（`width`/`height` はこの外接そのもの）
   *   内訳: 幅 = 跳 の right 68 + 52（走が居たときは 走 の 80 + 52 = 132）
   *        高さ = パ の bottom 84 + 52。
   *   ⚠️ この箱は `pointer-events: auto` なので、**空いている所も指を吸う**。走のぶんを
   *      詰めないと、消したボタンの跡が「押しても何も起きない帯」として残る。
   *
   * ⚠️ ★ **上の 3 行（寸法・置き場所・中心間距離）はここまでが 2026-08-11 の姿**で、
   *    ボタンの側は 2026-08-12 の並び替え（この下の ★）が**丸ごと置き換えている**
   *    （投 74→96 ／ 跳・パ を投の左へ縦 1 列 ／ パッドの外接 120x136 → 172x112）。
   *    スティックの側（104px・左12下10・帯）は 1px も動いていない。前の姿は
   *    `?pad=corner` でそのまま出る。
   */
  /* ★ 2026-08-11（同日・オーナー実機報告「全体的に移動ボタンが押しにくい」）:
   *   円盤の**寸法も濃さも位置も 1px / 1bit も戻していない**（＝ 上のコートの被覆の
   *   話は 1px² も悪くしない）。足したのは `--stick-zone-*` ＝ 透明な当たり判定の
   *   帯だけで、そこに置かれた指の下へ円盤が移る（`src/ui/touchstick.js`）。
   *   帯の実測（390x844・ノッチ込み）: x 2〜190・y 618〜842 ＝ **42,112px²**。
   *   右へ 76px にしているのはパッドの左端まで空けるため（2026-08-12 にパッドの
   *   外接が x=258 → **x=200** へ広がったので、いまの隙間は 8px。ここが 0 になると
   *   `tools/stickreach.mjs` の A/B 節が鳴る）。 */
  .touch-stick {
    --stick-l: 12px;
    --stick-b: 10px;
    --stick-zone-up: 80px;
    --stick-zone-right: 76px;
    width: 104px;
    height: 104px;
  }
  .touch-stick__knob { width: 48px; height: 48px; margin-left: -24px; margin-top: -24px; }
  /* ★ 2026-08-12 に**外接ごと置き直した**（下の並び替えの ★）。
   *
   * 寸法は 5 つの custom property から**導出**する（べた書きしない）。狭い画面で
   * 縮めるときに触る値が 1 か所に集まり、外接と 3 つの位置が必ず整合するため。
   *   width  = 投 ＋ 緩衝 ＋ 縦 1 列          （390x844 で 96+24+52 = 172）
   *   height = 縦 1 列 2 つ ＋ その隙間        （52*2+8 = 112 ＝ コートの近エンドライン
   *            y=698 からセーフエリアの下端 y=810 までの帯ちょうど）
   * ＝ 箱は 390x844（ノッチ込み）で **x 200〜372・y 698〜810**。
   * ⚠️ この箱は `pointer-events: auto` なので**空いている所も指を吸う**。だから外接は
   *    3 つが実際に占める範囲ちょうどにしてある（余らせると「押しても何も起きない帯」）。
   * ⚠️ 左端 200px は、スティックの透明な帯の右端（x=192）から 8px 空けた値。ここを
   *    割ると `tools/stickreach.mjs` の A/B 節（帯の実面積・帯の中に別の当たり判定が
   *    居ないこと）が鳴る。**幅 360px の画面ではこの余裕が無くなる**ので、下の
   *    `max-width: 380px` の節が 投 と緩衝と右余白を詰める（それでも 84px ＝
   *    2026-08-12 の 74px より大きい）。 */
  .touch-pad {
    --pad-main: 96px;        /* 投 の直径 */
    --pad-side: 52px;        /* 跳 ／ パ・屈 の直径 */
    --pad-stack-gap: 8px;    /* 縦 1 列の 2 つの隙間 */
    --pad-gap: 24px;         /* 投 と縦 1 列の緩衝（誤爆の緩衝帯。★の註） */
    --pad-right: 18px;       /* 画面の右端からの逃げ */
    right: calc(var(--sa-r) + var(--pad-right));
    bottom: var(--sa-b);
    width: calc(var(--pad-main) + var(--pad-gap) + var(--pad-side));
    height: calc(var(--pad-side) * 2 + var(--pad-stack-gap));
  }
  /* ═══ ★ 2026-08-12・オーナー実機報告 ═══════════════════════════════════════════
   *   「**右のボタンが押しにくいので、押しやすいレイアウトにして欲しい。
   *     特に投げるボタン。**」
   *
   * ── 何が押しにくかったか（実測・390x844・ノッチ込み。上の節が置いた姿）─────────
   *   · 投 は **74px**（97ca8a1 で 84 → 74 に縮んだ）。押す面は円なので、指の腹
   *     （実測で 45x60px 前後）に対して的の余裕がほとんど無い。
   *   · 隣との**縁の間隔が 6.6px しかない**（投-パ。投-跳 でも 8.4px）。
   *     ＝ 投 を狙って少し外すと パ か 跳 が出る。
   *   · **右下の隅に埋まっている**（右端まで 12px・下端まで 48px）。親指は右下から
   *     入ってくるので、外すときは**隅の側**へ落ちる — その側が全部「何も無い」。
   *     ＝ いちばん起きやすい外し方が、いちばん確実に空振りになる形だった。
   *
   * ── 場所の算数（この並びは幾何から決まっていて、好みで置いていない）─────────────
   *   コートの近エンドラインは 390x844（ノッチ込み）で **y=698**（`tools/thumbcover.mjs`
   *   が毎回投影して出す内野の四隅の実測値）。セーフエリアの下端は y=810。
   *   ＝ **y 698〜810 の 112px の帯は、置いても 1px² もコートを覆わない**。
   *   幅は、スティックの透明な帯（x<=192）を避けて x 196〜390 の 194px が空いている。
   *   この 194x112 に 3 つを入れる並びは、投 を 96px にすると**ほぼ一意に決まる**:
   *     · 投 96px は帯の高さ 112px のほぼ全部を使う ＝ その上下にはもう何も積めない
   *     · よって残る 2 つは**投 の左に縦 1 列**（52 + 8 + 52 = 112 ＝ 帯ちょうど）
   *   跳（上）と パ／屈（下）の順は意味と一致する — 上へ跳ぶ、下へしゃがむ。
   *
   * ── 却下した案（実測してから落としたもの）───────────────────────────────────
   *   ✗ 三角のまま大きくする（跳 を左上・パ を右上・投 を右下のまま 96/58/58 へ）
   *     … 96px の 投 を帯に置くと、その上に載る 2 つは**必ず y=698 より上**へ出る。
   *       見積もり **cover +2,400px² 前後**（跳 と パ の円の大半がコートに乗る）＝
   *       2026-08-11 にオーナーが「見にくい」と言った量を作り直すことになる。
   *   ✗ 弧の上に 3 つ等間隔（親指の回転軸から等距離）… 96px の 投 を含めると
   *     必要な弧の間隔が 74px 以上になり、半径 100px では 3 つで 90〜100° 要る ＝
   *     端の 1 つが画面の下端の外／もう 1 つが右端の外へ出る。390px 幅では入らない。
   *   ✗ 投 をさらに大きく（104px 以上）… 帯 112px に対して上下の余白が 4px しか
   *     残らず、影のにじみがコートに乗り始める（cover が増える）。
   *
   * ── 前後（390x844・ノッチ込み。実測は `tools/padreach.mjs` と `tools/thumbcover.mjs`
   *    が毎回出す）─────────────────────────────────────────────────────────────
   *     投 の直径          74 → **96px**（円の面積 4,301 → 7,238px² ＝ 1.68 倍）
   *     隣との縁の間隔     6.6 → **28.5px**
   *     隅からの逃げ       右端まで 12 → 18px
   *     指が届く実面積     4,436 → **14,820px²（3.34 倍）**（下の `::before` 込み・掃引の実測）
   *     コートの被覆 cover  1,354 → **46.3px²（-96.6%）**
   *     総インク ink       11,370 → 13,704px²（+20.5%）← 投 が太ったぶんの代償
   *   ⚠️ `cover`（見えているコートに落ちた量）が桁で減るのは、3 つとも近エンドライン
   *      （y=698）より下に居るため。増えるのは `ink`（＝ 画面のどこに居ようが落ちる
   *      総量）だけで、`thumbcover` の床はそのぶんを**上げてある**（あちらの表の ★）。
   */
  .touch-btn--action {
    width: var(--pad-main);
    height: var(--pad-main);
    font-size: 2rem;
    right: 0;
    /* 帯（外接の高さ）の中で縦に揃える ＝ 上下の余白が必ず等しい。 */
    bottom: calc((var(--pad-side) * 2 + var(--pad-stack-gap) - var(--pad-main)) / 2);
  }
  /**
   * ★ 隅までの**透明な当たり判定**（＝ スティックの `.is-float::after` と同じ手）。
   *
   * 投 の箱から**画面の右端と下端まで**を 投 の一部にする。完全に透明なので
   * `tools/thumbcover.mjs` が数える α 加重の被覆（cover / ink）には **1px² も乗らない**
   * — 増えるのは指を受ける面積だけ。親指が下から入って**手前に外した**指が、
   * 空振りではなく 投 になる（上の「いちばん起きやすい外し方」への直接の手当て）。
   *
   * ⚠️ 伸ばすのは**右と下だけ**。左（跳／パ の側）へ伸ばすと、跳 を狙って外した指が
   *    投 になる ＝ 跳びたいのに投げる、といういちばん高くつく誤爆を作る。
   *    24px の隙間はその緩衝で、意図的に空けてある。
   * ⚠️ 擬似要素は事象の的にならないので、ここに置かれた指は `.touch-btn` 自身の
   *    pointerdown として届く（`core/input.js` の結線に 1 行も足していない）。
   * ⚠️ `position: absolute` にしてあるのは、`.touch-btn` が `display: grid` だから
   *    （素の擬似要素はグリッドの子になり、中の字を押し出す）。`::after`（艶）と同じ。
   * ⚠️ 縦持ちの中だけに置く。横持ちの並びでは 跳 が 投 の**箱の縁より下**に食い込んで
   *    いて（実測 4x12px）、同じ矩形を当てると 跳 の当たり判定を奪う。横持ちの並びを
   *    見直す日に一緒に足すこと。
   */
  .touch-btn--action::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    /* 箱の外へ: 画面の右端／下端まで（パッドの余白 ＋ セーフエリア ＋ 自分の下余白）。 */
    right: calc(-1 * (var(--sa-r) + var(--pad-right)));
    bottom: calc(-1 * (var(--sa-b)
      + (var(--pad-side) * 2 + var(--pad-stack-gap) - var(--pad-main)) / 2));
  }
  /* 跳（上）と パ・屈（下）は 投 の左に縦 1 列。上へ跳ぶ・下へしゃがむ、と並びが
     意味と一致する。横位置は「投 の直径 ＋ 緩衝」＝ 箱の中で必ず 投 の左隣になる。 */
  .touch-btn--jump {
    width: var(--pad-side);
    height: var(--pad-side);
    right: calc(var(--pad-main) + var(--pad-gap));
    bottom: calc(var(--pad-side) + var(--pad-stack-gap));
    font-size: 1.15rem;
  }
  .touch-btn--pass {
    width: var(--pad-side);
    height: var(--pad-side);
    right: calc(var(--pad-main) + var(--pad-gap));
    bottom: 0;
    font-size: 1.15rem;
  }

  /* ═══ ★ `?pad=corner` — 2026-08-12 までの姿（オーナーが実機で比べるための入口）════
   *
   * 上の並びを**丸ごと** 2026-08-12 の姿へ戻す（投 74px・右下の隅・跳 は左上・
   * パ は右上）。`UI.padLayout` の註のとおり、これは
   *   ① オーナーの実機 A/B の入口（`?pad=corner`）
   *   ② `tools/thumbcover.mjs --break=corner` / `tools/padreach.mjs --break=corner` の
   *      ネガティブテストの経路（新しい床が**本当に何かを守っているか**の証明）
   * の 2 つを兼ねる。⚠️ 透明な当たり判定も一緒に消す — 「74px の隅のボタンなのに
   * 判定だけ広い」は前の姿でも新しい姿でもない**3 つ目の状態**で、比較にならない。 */
  html[data-pad="corner"] .touch-pad { width: 120px; height: 136px; right: calc(var(--sa-r) + 12px); bottom: calc(var(--sa-b) + 10px); }
  html[data-pad="corner"] .touch-btn--action { width: 74px; height: 74px; font-size: 1.5rem; right: 0; bottom: 4px; }
  html[data-pad="corner"] .touch-btn--action::before { content: none; }
  html[data-pad="corner"] .touch-btn--jump { right: 68px; bottom: 58px; }
  html[data-pad="corner"] .touch-btn--pass { right: 2px; bottom: 84px; }
  /* Portrait has vertical room to spare, so the shout drops a little — but it
     still derives from the config value, which stays the one place the division
     of the frame is decided. */
  .ann { top: calc(var(--ui-ann-top, 0.22) * 100% + 6vh); }
  .ann.is-super { top: calc(var(--ui-ann-top-super, 0.16) * 100% + 6vh); }
}

/* ═══ 幅の狭い縦持ち（360〜375px）だけ、投 と余白を詰める ══════════════════════
 *
 * 2026-08-12 の並びは「スティックの透明な帯（x<=192）の右に 3 つを置く」なので、
 * 使える幅は **画面幅 − 192px** しかない。360px の端末ではそこが 168px で、
 * 390px の姿（右 18 ＋ 投 96 ＋ 緩衝 24 ＋ 列 52 = 190）が**入らない** —
 * そのまま出すとパッドの左端が x=170 になり、スティックの帯に 20px 食い込んで
 * `tools/stickreach.mjs` の A（帯の実面積 34,500px² の床）と B（帯の中に別の
 * 当たり判定が居ないこと）が両方鳴る（実測: 帯を 2,200px² 奪う）。
 *
 * 詰めるのは **投 の直径・緩衝・右余白の 3 つだけ**で、跳／パ の 52px と縦の
 * 積み方は動かさない（44px の床に最も近いのがそこなので、削る順序は最後）。
 *   360px: 右 12 ＋ 投 84 ＋ 緩衝 16 ＋ 列 52 = 164 → 左端 **x=196**（帯まで 4px）
 *   375px: 同じ値で 左端 x=211
 * ⚠️ 84px は 2026-08-12 までの 74px より**大きい**（＝ 狭い端末でも押しやすさは
 *    前進する）。44px の床は `tools/thumbcover.mjs` と `tools/padreach.mjs` が持つ。 */
@media (orientation: portrait) and (pointer: coarse) and (max-width: 380px) {
  .touch-pad {
    --pad-main: 84px;
    --pad-gap: 16px;
    --pad-right: 12px;
  }
}

/* ⚠️ ここには 2026-08-05 から「Landscape + touch: the stick owns the bottom-left
 *    corner, so the card slides right of it instead of hiding underneath.」
 *    （`.pc { left: sa-l+186px; bottom: sa-b+12px }`）が居た。2026-08-09 の検品で
 *    **背の高い指の横持ち（タブレット）でカードがコートに -47.9px 潜る**と実測された
 *    ため、下のドックが `pointer` を問わなくなり、この規則は**どの画面にも届かなく
 *    なった**（背の低い横持ちは `max-height: 520px` の節が top/bottom/left を丸ごと
 *    上書きし、背の高い横持ちは下のドックが上書きする）ので消した。 */

/* ═══ 横持ちのカードも上のスコアボードの真下へ（2026-08-09・オーナー裁定 O6=(a)）══
 *
 * 2026-08-05 に縦持ちが取った置き方（下のブロック）と**同じ**。横持ちだけ画面下に
 * 残っていたが、`tools/framecheck.mjs` を「カメラの取りうる姿勢の区間」で測る形に
 * 作り直したところ、下に置いたままでは**試合中にコートへ潜る**ことが実測で確定した:
 *
 *     1600x900   内野 近R が `.pc` に **-51.9px**（24 サンプルの窓では -1.4 にしか
 *                見えていなかった。「承認済みの絵の -2px」はその窓の値だった）
 *     800x500    -18.4px      844x390 / 932x430  -21.1px      667x375  -20.2px
 *
 * オーナー裁定:「試合中（カメラの区間の全域）もコートに一切被らない」を正解とする。
 * 下には置けない — 近エンドラインは区間の下端で画面の外（1600x900 で y=988 / 画面 900）
 * まで来るので、下の帯には**そもそも安全な場所が無い**。横にも逃げられない
 * （遠 R と遠 L の間の安全な帯は 667x375 で 159px しかなく、カードは 295px）。
 * 空いているのはコートの上・スコアボードの下の「空と客席の帯」だけで、そこが縦持ちの
 * 答えでもあった。
 *
 * ★ `top` は**上の帯の下端に追随させる**。⚠️ 1 機種で校正した固定 px にしては
 *   いけない — 帯の高さは `.hud` の `font-size: clamp(13px, 1.05vw, 17px)` と
 *   その中の `clamp(rem, Nvw, rem)` 群で決まる ＝ **幅比例**なのに、コートは
 *   **高さ比例**（遠エンドラインの区間の上端は実測でどの解像度でも 0.3954 × 画面高）。
 *   2026-08-09 の初版は 1600x900 の 1 機種だけで校正した `196px` で、検品の実測では
 *       1280x720  遠 R がカードに **-2.5px**（潜っている） ／ 1024x768  +6.1px
 *   だった（どちらも要 8px）。帯の下端の実測（`.hud__top .side` の下端・
 *   scratchpad/e1fix/probe-band.mjs）:
 *       1024x768 146.8  1194x834 156.5  1280x720 162.4
 *       1366x768 169.6  1600x900 188.6  1920x1080 189.8
 *   幅 1024〜1600 はほぼ直線（0.0725px/px）で、1600 より上は clamp が飽和して平ら。
 *   だから **`clamp(155px, 8vw + 68px, 198px)`**。下限 155px は幅 1092px 未満で帯の
 *   縮みが止まるぶん、上限 198px は帯が飽和する 1630px 以上のぶん。
 *   ⚠️ **1600x900 では 196px ちょうどを返す**（8×16 + 68）＝ オーナー承認済みの絵は
 *      1px も動かない。
 * ★ 1280x720 は**窓が 9.2px しか無い**（この式でいちばん厳しい形）。帯の下端 162.4 ＋
 *   カード 105.1 ＝ 267.5 に対し、コートの遠エンドラインの区間の上端は
 *   0.3954 × 720 = 284.7 で、上下に 8px ずつ空けると 283.5 ＝ 残りが 1.2px。
 *   だから式は「帯からの余白」より**コートからの余白**を厚く取ってある。
 *   内訳（この式での 帯との余白 / コートとの余白・px。前者は probe-band.mjs の実測、
 *   後者は framecheck の区間の実測）:
 *       1024x768  6.3 / 47.2      1194x834  5.1 / 64.4
 *       1280x720  6.1 / 11.1      1366x768  5.7 / 20.5
 *       1600x900  5.3 / 46.1      1920x1080 6.0 / 114.7
 * ★ `pointer` を問わない（2026-08-09 の検品）。初版は「指の台（タブレットの横持ち）は
 *   下に置いたまま」にしていたが、framecheck のケース一覧にその形が無かっただけで、
 *   実測すると**カードがコートに潜っていた**（1024x768 touch で近 R が -47.9px /
 *   1194x834 touch で -51.3px）。裁定 O6=(a) と同じ方針でドックへ移す。
 *   背の低い横持ち（電話の横）は下のブロックが別に面倒を見る。
 *   ⚠️ タッチの furniture（スティック / ボタン）は**動かしていない** — 背の高い指の
 *      横持ちでは近い角がスティックに -68.9px 潜るが、その再配置は試合画面の見た目の
 *      変更でオーナーの領分（PRODUCT-PLAN O11 と同じ）。framecheck が実測のすぐ下に
 *      床を置いて悪化だけを捕まえ、毎回名指しで印字する。
 * ⚠️ **背が低くて幅のある窓（例 900x600・1600x600 ＝ デスクトップのブラウザを縦に
 *   潰した形）は幾何的に成立しない。** 900x600 の実測: 帯の下端 141.5（幅 900px 以下は
 *   `max-width: 900px` の節で英字の副題が落ち padding も 9→6px になる）＋ カード 103.4 に
 *   対し、遠エンドラインの区間の上端は 0.3954 × 600 = 237.2 しかない。この式は下限
 *   155px でカードを可能な限り上へ寄せる（帯との余白 11.6px）が、それでも下端 256.5 が
 *   コートに約 19px 掛かる。詰めるにはカードの高さを削る判断が要る（844x390 / 667x375 と
 *   同じ話）ので、framecheck の横長ケースにも入れていない。 */
@media (min-height: 521px) and (orientation: landscape) {
  .pc { top: calc(var(--sa-t) + clamp(155px, 8vw + 68px, 198px)); bottom: auto; }
}

/* Short landscape (phone on its side): pull everything toward the edges. */
@media (max-height: 520px) {
  .hud__top { top: calc(var(--sa-t) + 4px); }
  .clock__label { display: none; }
  .screen__inner { gap: 8px; padding-top: calc(var(--sa-t) + 10px); padding-bottom: calc(var(--sa-b) + 10px); }
  .result__table { display: none; }
  .title__tagline { display: none; }

  /* ── 難易度の行を足した分の場所を作る（2026-08-09）─────────────────────────
   *
   * 背の低い横長（932x430 / 667x375）は `max-width: 620px` の分岐に入らないので、
   * カードが数字とステータスバーを付けたまま載る。実測: 667x375 で選択画面の
   * 中身が 433.8px = 画面より **59px 高く**、上は見出しが切れ、下は難易度の札が
   * 画面外へ出た（scratchpad/e2-shots.mjs）。
   *
   * 落とすのは max-width:620px の分岐と同じもの — 数字とステータスバー。名前と
   * 紋章は残る（「どちらの高校か」はカードの仕事なので）。札の 44px は削らない
   * （タップの床）。 */
  .card__stats, .card__numbers { display: none; }
  /* 一言だけ落とす（2026-08-28・E25）。理由（`min-height` も外すことも）は
     `max-width: 620px` 側と同じ。 */
  .card__flavour { display: none; }
  .card__note { min-height: 0; }
  /* 紋章もこの区間だけ一段小さく。ここはカードが 1 行 6 枚のまま載る唯一の背の
     低い区間で、`?ai=`/`?court=` の注記が 3 本とも出た回がいちばん厳しい。 */
  .crest { width: clamp(34px, 4.0vw, 56px); }
  .card.is-sel { transform: translateY(-3px) scale(1); }

  /* ── しあいの しゅるいの行を足したぶんの場所を作る（2026-08-09・E8）──────────
   *
   * 上の E2 の等価交換をもう一度やる。実測（scratchpad/e8/probe-fit.mjs・札の行を
   * 足した直後）: 中身が画面より **667x375 で 39.2px / 932x430 で 40.9px /
   * 844x390 で 56.8px** 高い ＝ 上は見出しが切れ、下は札が画面の外へ出る。
   *
   * 落とすのは**見出しの大きさと英字の副題**だけ。この画面で最も情報量の少ない
   * 画素で（何を選ぶ画面かは 3 行の見出しが言っている）、札とカードには一切
   * 触らない — 44px の床も、どちらの高校かも、段の名前も残る。
   * 内訳（844x390）: 副題 11px ＋ その gap 8px ／ 見出し 40.8→24.5px ／
   * 空の注記 9.2px×2（上の `:empty`）／ 塊の gap 4px = 計 58.9px。 */
  .select__title { font-size: clamp(0.9rem, 2.6vw, 1.5rem); }
  .select__sub { display: none; }
  /* ★ 見出しを削るだけでは**注記が出た回**が入り切らない（`?ai=` は 2 本とも出る:
   *   844x390 で +19.4px）。残りはカードの内側の余白から取る — 文字も紋章も
   *   1 つも落とさず、詰めるのは間隔だけ。実測 844x390: カードの上下 10.2px ＋
   *   3 つの gap 12px ＋ 行の上マージン 7.8px = 30.0px。 */
  .select__row { margin-top: 0; }
  /* gap は 6px → 4px（2026-08-28・E25。背の低い横長でカードが 1 行 6 枚のまま
     載る唯一の区間なので、文字も紋章も落とさず間隔だけあと 2px 詰める）。 */
  .card { gap: 4px; padding: clamp(8px, 1.4vw, 14px); }

  /* ── 上の帯を縦で組み直す（2026-08-05・オーナー報告「コート全体が見えないです」）
   *
   * この帯の寸法は `clamp(_, N vw, _)` で**横幅**から決まっていた。横長で背の低い
   * 端末（844x390）は「幅は広いが高さが無い」ので、ほぼデスクトップの寸法のまま
   * 高さ 390px の画面に載る。実測（scratchpad/framefit.mjs）:
   *
   *     1600x900  上の帯 188.5px = 画面の 20.9%
   *      844x390  上の帯 144.5px = 画面の **37.1%**   ← 同じ組版が1.8倍の面積
   *
   * コートの遠い側の四隅は画面の 42.5% にあるので、帯の下端との余白は 21px しか
   * 無い（デスクトップでは 194px）。「コート全体が見えない」の縦の取り分はここ。
   * ⚠️ 条件は max-height。`(pointer: coarse)` も `max-width` もこの事故を捕まえ
   * ない（幅は 844px あるので「広い画面」の分岐に入ってしまう）。
   *
   * 落とすのは外野の控え帯だけ（`max-width: 620px` の分岐と同じ判断 —
   * 「名前は残す、ベンチが落ちる」）。内野3人の名前とゲージは残る。 */
  .hud { font-size: 12px; }
  .side__benchwrap { display: none; }
  /* 英字の副題は `max-width: 900px` で落ちるが、932x430（iPhone 15 Pro Max の
     横持ち）は 932px あるのでその分岐に入らない ＝ 背の低い画面で2行目が生き残る。
     ここでも落とす。 */
  .side__en { display: none; }
  .side__head { padding: 3px 9px 3px 5px; gap: 5px; }
  .side--b .side__head { padding: 3px 5px 3px 9px; }
  .side__jp { font-size: 0.74rem; letter-spacing: 0.02em; }
  .side__score { font-size: 1.2rem; min-width: 1.1em; }
  .side__alive-label { display: none; }
  .row { padding: 0 3px; }
  .row__name { width: 4.6em; font-size: 0.8rem; }
  .tick { --tick-w: 6px; --tick-h: 12px; }
  .gauge { gap: 2px; }
  .clock { padding: 3px clamp(10px, 1.4vw, 18px) 4px; min-width: 84px; }
  .clock__time { font-size: 1.7rem; }

  /* ── タッチの furniture をコートの四隅からどける ─────────────────────────
   *
   * スティックと4つのボタンは**サイズが固定 px** で、背の低い画面でも 150px /
   * 96px のまま出る。実測（framefit.mjs・667x375 = ホームボタン世代の横持ち）:
   *
   *     内野 近L (469,321) … 走 ボタンの下に 3.6px 潜る
   *     内野 近R (148,321) … スティックの下に **26px** 潜る
   *     外周 遠L (481,160) … 跳 ボタンの下に 0.5px 潜る
   *
   * 「画面に入っている」と「見えている」は別物で、四隅は前者を満たしたまま後者を
   * 満たしていなかった。スティックは 150->118px・パッドは 208->176px に縮め、
   * どちらも画面の隅へ寄せる。⚠️ 縮めても**当たり判定は 44px を割らない**
   * （最小のボタンで 58px、スティックはドラッグ領域なので 118px）。
   *
   * ボタン同士の中心間距離（縮めた後・単位 px、必要値は半径の和）:
   *     投-跳 89.3 / 71   投-走 81.6 / 71   投-パ 77.5 / 71
   *     跳-走 86.6 / 58   跳-パ 78.1 / 58   走-パ 135.4 / 58
   * いちばん詰まるのは 投-パ の 6.5px。ここをさらに詰めるとボタンが重なる。 */
  /* ★ 直径を「コートの近い側の角」から幾何で決める（固定 px をやめる・2026-08-09）。
   *
   * この式は 2026-08-05 から**カードの左端**に使っていたもので、カードが上へ退いた
   * ので、同じ角の式が要るのは親指の furniture のほうになった。コートの近Rの角は
   *     x = 0.5 * 幅 − 0.495 * 高さ          （px。カメラ行列から実測）
   * に来る — 縦画角が固定なので、角と画面中心の距離は**幅ではなく高さ**に比例する。
   * 実測（framefit.mjs・角の x）:
   *     844x390 → 228.9   932x430 → 253.1   667x375 → 148.2   1600x900 → 355.6
   *     0.5W − x を H で割ると 0.4951 / 0.4951 / 0.4941 / 0.4938
   * だから固定 px は「ある1機種でだけ当たる」。実際 667 では角がスティックの下に
   * 26px 潜っていた。
   * 59.5dvh = 49.5（中立姿勢の角）+ 10.0（カメラの左振れ。CAMERAFX.centerRange と
   * 距離帯・寄りまで含めた区間の端を tools/framecheck.mjs で実測した値）。18px は
   * スティックの左余白 10px ＋ 要求余白 8px。
   * ⚠️ `min()` なので**縮むのは足りない画面だけ**: 844x390 / 932x430 は 118px のまま
   *    （計算値 125.0 / 145.2）、667x375 だけ 92.4px になる。ドラッグ領域なので
   *    44px のタップの床は割らない。
   * ⚠️ `dvh` が無い環境では calc が無効になり、`min()` ごと落ちて 118px に戻る。
   * ⚠️ **64px の床を必ず掛ける。** 16:9 より縦長で背も低い窓（例 600x500）ではこの式が
   *    負になり、`min()` だけだと幅が 0 に丸められて**スティックが消える**。角を避ける
   *    ことより「操作系が在ること」が先なので、そこは 64px（タップの床 44px の上）で
   *    止めて、角に潜るぶんは framecheck の `hudThumb` が数えて出す。 */
  /* ★ 帯（`--stick-zone-*`）は横持ちにも要る。上へ 64px にしているのは、背の低い
   *   横持ち（375〜430px）では画面の高さそのものが乏しく、これ以上伸ばすと
   *   スコアボードの帯まで届くため。円盤の寸法・位置はこの節でも 1px も動かして
   *   いないので、`tools/framecheck.mjs` の `hudThumb`（四隅からの距離）は同値。 */
  .touch-stick {
    --stick-l: 10px;
    --stick-b: 10px;
    --stick-zone-up: 64px;
    --stick-zone-right: 64px;
    width: max(64px, min(118px, calc(50vw - 59.5dvh - 18px - var(--sa-l))));
    height: max(64px, min(118px, calc(50vw - 59.5dvh - 18px - var(--sa-l))));
  }
  .touch-stick__knob { width: 50px; height: 50px; margin-left: -25px; margin-top: -25px; }
  .touch-pad {
    right: calc(var(--sa-r) + 12px);
    bottom: calc(var(--sa-b) + 12px);
    width: 176px;
    height: 190px;
  }
  .touch-btn--action { width: 84px; height: 84px; right: 4px; bottom: 34px; font-size: 1.7rem; }
  .touch-btn--jump { width: 58px; height: 58px; right: 84px; bottom: 106px; font-size: 1.2rem; }
  /* 走は 2026-08-11 の裁定で消えた（右 94/下 20 に居た）。箱の幅 176px は 投+跳 の
     並び（84+58 = 142）とパの逃げ場に効いているので据え置く。 */
  .touch-btn--pass { width: 58px; height: 58px; right: 8px; bottom: 124px; font-size: 1.2rem; }

  /* ── 操作キャラのカードを「1行の細帯」に畳む（2026-08-05）───────────────
   *
   * オーナー報告（実機・スマホ）:「操作キャラの顔とゲージのウィンドウが
   * コートと被っていてプレーしにくいです」「裏に選手が隠れている」。
   *
   * 目分量ではなく実測した（`scratchpad/hudfit.mjs` — コート面 y=0 の四隅を
   * カメラで画面へ投影し、その四角形と HUD の矩形の重なりを数える）:
   *
   *     844x390 (iPhone 横)   カード 314x96  → **見えているコートの 30.7%** を覆う
   *     932x430               カード 314x96  → 24.6%
   *     1600x900 (承認済みの絵) カード 387x108 →  4.6%
   *
   * 縦900では成立していた配置が、縦390では6倍の面積を食う。効いているのは
   * 横幅ではなく**高さ**なので条件は max-height — `@media (pointer: coarse)` は
   * 入力方式しか見ておらず、この事故を捕まえられない。デスクトップ(縦900)は
   * このブロックに入らないので 4.6% のまま1pxも動かない。
   *
   * 残す情報（要件）: 誰を操作しているか / HP / 必殺ゲージ。
   * 落とすのは顔・「内野」タグ・漢字名・SUPER の字だけで、どれも上のチーム表示
   * （自分の行に ▶ が付く）か文脈が既に言っている。顔を落とすのが最も効く —
   * カードの高さの実体はほぼ顔の一辺だった。
   *
   * ★ 畳み方そのもの（顔を落とす・1行にする）は縦持ちでも同じものが要る（2026-08-05
   *   同日のオーナー報告 2件目）。**この節に二度書くと片方だけ直す事故になる**ので、
   *   宣言はこの下の
   *       @media (max-height: 520px), (orientation: portrait) and (pointer: coarse)
   *   の 1 ブロックに集約した。ここに残すのは**横持ち固有の位置決めだけ**。
   *   ⚠️ 集約でこの節の計算値が変わらないことは実測で確認済み（844x390 / 932x430 で
   *      `.pc` と子孫の getComputedStyle が前後で完全一致・矩形も一致）。
   */
  /* ★ 2026-08-09（オーナー裁定 O6=(a)）: 帯を画面の下からスコアボードの直下へ移した。
   *   理由と実測は上の「横持ちのカードも上のスコアボードの真下へ」の註と同じ
   *   （下の帯には、カメラの取りうる姿勢の区間で見ると安全な場所が存在しない）。
   *   100px は上の帯の下端の実測（932x430 で 98.8 / 844x390・667x375・800x500 で
   *   95.7〜96.2）のすぐ下。
   *   ⚠️ **844x390 と 667x375 だけは 8px の余白を取り切れない**（実測 3.7px / -2.3px）。
   *      上の帯の下端と遠エンドラインの隙間が 58.3px / 52.3px しかなく、カードは演出
   *      ピーク込みで 51.4px あるため。詰めるにはカードの高さを削る＝可読性の判断が
   *      要るので、`tools/framecheck.mjs` はこの 2 機種だけ実測のすぐ下に床を置いて
   *      悪化を捕まえ、判断はオーナーへ上げてある（ゲートが毎回名指しで印字する）。 */
  .pc {
    top: calc(var(--sa-t) + 100px);
    bottom: auto;
    left: calc(var(--sa-l) + 10px);
  }
}

/* ═══ 操作キャラのカードを「1行の細帯」に畳む — 横持ちと縦持ちで共通 ═══════════
 *
 * 条件が 2 つあるのは、同じ「畳む」が別々の理由で要るから:
 *   ・max-height: 520px            … 背の低い横持ち。縦900で成立していた 96px の
 *                                    カードが縦390では見えているコートの 30.7% を覆う。
 *   ・portrait かつ pointer:coarse … 縦持ち。カードは 226x92 のまま画面下部に載り、
 *                                    コート（実測 y 392〜728）のど真ん中に立っていた。
 * 畳み方は同じで良い（残すのは 誰を操作しているか / HP / 必殺ゲージ）。位置決めだけが
 * 向きごとに違うので、位置はそれぞれの節に置いてある。
 * ⚠️ 横持ちの見た目はオーナー確認済み。ここは**移設であって再設計ではない** — 宣言は
 *   1 行も足していない/変えていない（実測で計算値の一致を確認済み）。 */
@media (max-height: 520px), (orientation: portrait) and (pointer: coarse) {
  .pc {
    gap: 6px;
    padding: 3px 9px 3px 7px;
    min-width: 0;
    font-size: 11px;
  }
  .pc .gauge--card .tick { --tick-w: 8px; --tick-h: 11px; }
  .pc .super { height: 9px; }
  .pc .mug { display: none; }
  .pc__kanji, .pc__role, .pc__superlabel { display: none; }
  /* 名前 / HP目盛 / HP数値 / SUPER を横1列に。`.pc__hprow` と `.pc__superrow` は
     display:contents なので、中身はそのまま `.pc__body` のグリッド項目になる。 */
  .pc__body {
    grid-template-columns: repeat(4, max-content);
    align-items: center;
    column-gap: 7px;
    row-gap: 2px;
  }
  .pc__nameline { grid-column: auto; }
  /* `.pc` で限定する。`.super` は今のところカードにしか生えていないが、
     セレクタでそれを保証しておかないと後から生えた瞬間に黙って巻き込む。 */
  .pc .super { min-width: 58px; }
  /* 代行の帯は残す（「今は CPU が動かしている」は握り直しの合図なので消せない）
     が、副題は落とす。帯が横に伸びると畳んだ意味が消える。 */
  .pc__autosub { display: none; }
}

/* ═══ 縦持ちのカードは上のスコアボードの真下に付ける（下には置けない）══════════
 *
 * 2026-08-05 オーナー実機:「プロフィールのウィンドウの位置を見直したい」。
 *
 * 縦持ちで置ける場所は実測すると 1 つしかない（390x844・zoom=1 ＝ 縮尺を戻した後）:
 *     上の帯          y   0 〜 101（`.hud__top` の箱は 117。下 16px は余白）
 *     ★空きの帯      y 101 〜 393     ← 空と客席の書き割り。ここだけが空いている
 *     コート          y 393 〜 729
 *     タッチの操作系  y 592 〜 788（スティック 654-784 / パッド 592-788）
 * 下は**物理的に無理**: 近エンドラインは画面の 86.4%（=729px）に来るのに対し、
 * スティックだけで 130px あり 844−729=115px には収まらない。だから上へ動かす。
 * 元は `bottom: sa-b + 200px`（= y 518〜610）で、コートのど真ん中に立っていた。
 *
 * ★ 空きの帯のどこに置くかは絵で比べて決めた（scratchpad/cardplace/{float,dock}.png）。
 *   空の中ほどに浮かせると宙ぶらりんに見えるので、スコアボードの直下に付ける。
 *
 * ★ `top` の値は「パネルの下端」の実測。縦画角と違ってここは font-size で決まるので
 *   割合では書けない。実測（scratchpad/portraitband.mjs・縦持ち 8 サイズ）:
 *       幅 ≤620px（電話）      パネル下端 96.1〜101.7   （hud の font-size 12px）
 *       幅 621〜1024px（タブレット）      143.5〜149.8   （13px ＋ 外野の控え帯が出る）
 *   境目が 620px なのは、そこで `.side__benchwrap` が落ちる**既存の**分岐だから。
 *   108 / 156 は上の最大値に 6px 足した値。
 * ★ 掛け声（`.ann`・上端が画面高の 28%）と重ならないことも確認済み: 帯の下端は
 *   108+44.5=152.5 なので、28% > 152.5 ⟺ 画面高 > 545px。360x640 で 27px 空く。
 *   ⚠️ 高さ 545px を切る縦持ち（320x480 世代）だけは掛け声と重なる。現行機に無いので
 *      割り切っている（framecheck の縦持ちは 360x640 以上を測る）。 */
@media (orientation: portrait) and (pointer: coarse) {
  .pc {
    top: calc(var(--sa-t) + 108px);
    /* ⚠️ 上のブロックの `bottom` を必ず解除する。絶対配置で top と bottom が両方
       効くと高さが引き伸ばされ、1行の帯が縦に伸びる。 */
    bottom: auto;
    /* 620px 以下の節が `width: min(58vw, 260px)` を掛けている。畳んだ帯は中身の
       幅で決まるべきなので外す（226px に押し込めると HP 目盛と SUPER が潰れる）。 */
    width: auto;
    max-width: calc(100vw - var(--sa-l) - var(--sa-r) - 20px);
  }
}
@media (orientation: portrait) and (pointer: coarse) and (min-width: 621px) {
  /* 外野の控え帯が出るぶん上の帯が 43px 背が高い（上の実測表）。 */
  .pc { top: calc(var(--sa-t) + 156px); }
}

/* Very wide desktop: cap the top band so the panels do not drift apart. */
@media (min-width: 1700px) {
  .hud__top { padding-left: max(calc(var(--sa-l) + 18px), calc((100vw - 1640px) / 2)); padding-right: max(calc(var(--sa-r) + 18px), calc((100vw - 1640px) / 2)); }
}

/* Respect the OS setting — the HUD still updates, it just stops dancing. */
@media (prefers-reduced-motion: reduce) {
  .rays, .press, .clock.is-urgent .clock__time, .clock.is-urgent::after,
  .super.is-full, .super.is-full .super__fill, .row.is-active .row__cursor,
  .logo__main, .card.is-sel, .menu__item.is-sel::before, .row.is-critical .tick.is-lit,
  .pc.is-hurt .mug::after {
    animation: none !important;
  }
  /* The cinematic retreat keeps its dimming (that is information: the readout
     is standing back), but it stops sliding. */
  .hud.is-cinematic .hud__top, .hud.is-cinematic .pc { transform: none; }
}
