/* ============================================================
   INKVILLE — rhythm.css
   Ink-Put!, Dance Dance Ink, Ink Tar Hero, the shared setlist and game top bar

   Part of the game.css split. Loaded as <link> tags from index.html in
   THIS ORDER — the cascade depends on it. Do not reorder the links.
   ============================================================ */
/* ===== INK-PUT! — standalone, now on a THEATER STAGE (v2.99 → v3.04) ===== */
#scr-inkput{background:radial-gradient(130% 100% at 50% 22%, #43263a 0%, #2a1626 46%, #150a13 78%, #0a050a 100%);}   /* a dim hall, not a black void */
.ip-hall{position:absolute; inset:0; z-index:1; overflow:hidden; pointer-events:none;}
/* ⭐ v3.68 — THE CAST RAINS DOWN. Every card's artwork tumbles behind the arrows; the longer your combo,
   the heavier and faster the fall. Sits at z-index 2 — above the hall backdrop (1), below the arrows (5). */
#ipFall{position:absolute; inset:0; z-index:2; overflow:hidden; pointer-events:none;}
.ipfp{position:absolute; top:-16%; will-change:transform; animation:ipFallA linear forwards;}
.ipfp svg{width:100%; height:100%; display:block; image-rendering:pixelated;}
@keyframes ipFallA{
  from{transform:translateY(0) rotate(var(--r0,0deg));}
  to  {transform:translateY(128vh) rotate(var(--r1,0deg));}
}
/* the bar SEALS — the first bar banks you (green SAFE!), every bar after it is gold */
/* ⚠ the seal must NEVER widen the bar off-screen: a full row of 8 arrows already fills the folded Fold
   (318 of 375px), so the seal is compact, the row's gap tightens to make room, and the row is allowed to
   wrap the seal onto a second line rather than overflow. */
#iputGrid .seq-r{flex-wrap:wrap; max-width:100%; gap:6px;}
/* ⭐ v3.88 — locked rows: arrows themselves go gold, the reward word rides the right edge */
.seq-r.locked-gold{border-color:var(--gold); background:#f4c54212; box-shadow:0 0 12px #f4c54266;}
.seq-r.locked-gold .arrowfill{filter:drop-shadow(0 0 4px #f4c542);}
.seq-tag{font-family:var(--fontD); font-style:normal; font-size:10px; letter-spacing:1px; color:var(--gold); margin-left:6px;
  text-shadow:1px 1px 0 #1b1510; animation:seqTagPop .4s var(--pop) both; white-space:nowrap;}
@keyframes seqTagPop{0%{opacity:0; transform:scale(2)}100%{opacity:1; transform:scale(1)}}
/* ⭐ v0.384 — THE BREATHING ROW. Every arrow still waiting on the player swells and settles, about
   once every 1.6s — slow enough to read as calm, obvious enough that no instruction line is needed.
   The one you must hit NEXT breathes bigger, around its existing 1.3x lift. Landed arrows and
   locked-gold rows hold perfectly still, so "alive vs done" reads at a glance.
   ⚠ .seq-cur already sets a transform; the keyframes must carry that scale or the arrow snaps small. */
@keyframes seqBreathe{0%,100%{transform:scale(1)}50%{transform:scale(1.16)}}
@keyframes seqBreatheCur{0%,100%{transform:scale(1.24)}50%{transform:scale(1.44)}}
.seq-breath{animation:seqBreathe 1.6s ease-in-out infinite;}
.seq-breath-cur{animation:seqBreatheCur 1.05s ease-in-out infinite;}
@media (prefers-reduced-motion:reduce){ .seq-breath,.seq-breath-cur{animation:none;} }
.seq-seal{display:inline-flex; align-items:center; justify-content:center; margin-left:6px; padding:2px 6px;
  font-family:var(--fontD); font-size:9px; letter-spacing:1px; white-space:nowrap; border:2px solid var(--ink);
  box-shadow:2px 2px 0 #0006; animation:sealPop .34s var(--pop) backwards; vertical-align:middle;}
@media (min-width:640px){ #iputGrid .seq-r{gap:12px;} .seq-seal{font-size:11px; padding:3px 9px; margin-left:10px;} }
.seq-seal.safe{background:#2e9e4f; color:#fdf6e3;}                 /* BAR 1 — you're safe to move on */
.seq-seal.gold{background:var(--gold); color:var(--ink);}          /* BARS 2, 3, 4 — pure profit */
@keyframes sealPop{0%{transform:scale(.4) rotate(-8deg); opacity:0}60%{transform:scale(1.15) rotate(3deg); opacity:1}100%{transform:scale(1) rotate(0); opacity:1}}
.seq-r.safe-r{border-color:#2e9e4f; background:#2e9e4f18; box-shadow:0 0 14px #2e9e4f88;}
.seq-r.safe-r .arrowfill{filter:drop-shadow(0 0 4px #2e9e4f);}
.seq-r.safe-r::after{background:linear-gradient(105deg,transparent,#bfffd0aa,transparent);}
.ip-valance{position:absolute; top:0; left:0; right:0; height:52px; background:linear-gradient(#b5202f,#7a1420 70%,#5c0f18);
  -webkit-mask:radial-gradient(circle 15px at 15px 100%,#0000 96%,#000) repeat-x bottom/30px 16px; mask:radial-gradient(circle 15px at 15px 100%,#0000 96%,#000) repeat-x bottom/30px 16px;
  box-shadow:0 3px 8px #0007;}
.ip-curtain{position:absolute; top:0; bottom:0; width:15%; background:repeating-linear-gradient(90deg,#5e0f18 0,#8a1826 9px,#b5202f 15px,#8a1826 21px,#5e0f18 30px); box-shadow:inset -8px 0 14px #0009, inset 8px 0 14px #0009; z-index:2;}
.ip-curtain.l{left:0;} .ip-curtain.r{right:0; transform:scaleX(-1);}
.ip-stage{position:absolute; left:0; right:0; bottom:0; height:34%;
  background:linear-gradient(#4a3520,#2e2013 60%,#20160d); border-top:3px solid #6b4d2a;
  box-shadow:0 -6px 20px #0008;}
.ip-stage::before{content:''; position:absolute; inset:0; background:repeating-linear-gradient(90deg,#00000022 0 34px,transparent 34px 68px);}   /* stage planks */
.ip-spot{position:absolute; top:44px; left:50%; transform:translateX(-50%); width:74%; height:60%;
  background:radial-gradient(ellipse at 50% 0,#fff5cf3a,transparent 70%); pointer-events:none;}
.ip-star{position:absolute; left:50%; bottom:calc(34% - 8px); transform:translateX(-50%); width:56px; height:64px; z-index:3;
  filter:drop-shadow(0 0 10px #f4c54266); animation:ipFloat 2.2s ease-in-out infinite;}
.ip-star svg{width:100%; height:100%;}
@keyframes ipFloat{0%,100%{transform:translateX(-50%) translateY(0) rotate(-2deg)}50%{transform:translateX(-50%) translateY(-8px) rotate(2deg)}}
.ip-foot{position:absolute; left:0; right:0; bottom:calc(34% - 5px); height:10px; z-index:3;
  background:repeating-linear-gradient(90deg,#f4c542 0 6px,transparent 6px 22px);
  filter:drop-shadow(0 -4px 8px #f4c54288); opacity:.85;}
#iputWrap{flex:1; min-height:0; position:relative; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:12px; padding:10px 10px calc(236px + var(--padny,0px)); z-index:5; color:#fdf6e3;}
.seq-r.shiny{border-color:var(--gold); background:#f4c54216; box-shadow:0 0 14px #f4c54288; position:relative; overflow:hidden;}
.seq-r.seq-r8{gap:8px; padding:3px 8px;}
.seq-r.seq-r8 .arrowfill{width:24px; height:24px;}
/* ⚠⚠ v0.834 — THE GLEAM IS A MOVING BACKGROUND, NOT A MOVING BOX. It used to be a 36px bar swept
   across on `left`, which is a layout property and re-solved the page every frame for as long as a
   shiny row was on screen. ⚠ It could not simply become `translate` like the other sweeps here:
   the travel was written in MIXED UNITS (`left:-40px` -> `110%`), and a transform percentage
   resolves against the element's own 36px box, so there is no single figure that means the same
   thing. Painting it as a `background-position` sweep is the pattern `hudFilmRoll` and `logoscan`
   already use in this project, it is paint-only, and it needs no knowledge of the row's width. */
.seq-r.shiny::after{content:''; position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(105deg,transparent 42%,#ffffff88 50%,transparent 58%);
  background-size:260% 100%; background-repeat:no-repeat;
  animation:rowGleam 1.6s linear infinite;}
@keyframes rowGleam{from{background-position:180% 0}to{background-position:-90% 0}}
.seq-r.shiny .arrowfill{filter:drop-shadow(0 0 4px #f4c542);}
.arrowfill.seq-lock{opacity:.16;}   /* a locked slot — a faint dot until its group of four unlocks */
#iputTimer{position:absolute; top:0; left:0; right:0; height:6px; background:#00000066; overflow:hidden;}
#iputTimerF{height:100%; width:100%; clip-path:inset(0 0 0 0); background:linear-gradient(90deg,#ff5c5c,#ffb84d,#ffe14d,#7ee87e,#5cc8ff,#b18cff,#ff5c5c); background-size:200% 100%; animation:rgbLine 2.2s linear infinite;}   /* ⚠ v0.834 — emptied by clip-path from JS, never by `width`: the rainbow must not be squashed */
#iputRound{font-family:var(--fontD); font-size:22px; letter-spacing:4px; text-shadow:2px 2px 0 #1b1510;}
/* v0.384 — the bottom line is EMPTY during normal play (only the once-a-run 24-BIT award ever
   writes here), so it must take no room at all when it has nothing to say. */
#iputMsg{font-family:var(--fontD); font-size:11px; min-height:0; letter-spacing:2px; color:#f4c542;}
#iputMsg:empty{display:none;}
/* v0.384 — INK-PUT's wordmark sits at the VERY TOP, centred in the space beside the inkling,
   instead of tucked in the right corner. The arrows then read as the middle of the screen. */
#scr-inkput .gtitle{top:calc(4px + env(safe-area-inset-top,0px)); left:92px; right:8px; width:auto; justify-content:center;}
/* v0.384 — and now that the card rain actually falls DURING play, the stage tag has artwork
   behind it for the first time. crReadable caught it at 1.5:1 on a cream card. Ring it in black,
   the house technique, so it reads on any backdrop. */
#scr-inkput #ipStageTR{top:calc(30px + env(safe-area-inset-top,0px)); color:#fdf6e3;
  text-shadow:1px 1px 0 #1b1510,-1px 1px 0 #1b1510,1px -1px 0 #1b1510,-1px -1px 0 #1b1510;}
#scr-inkput #ipStageTR b{text-shadow:1px 1px 0 #1b1510,-1px 1px 0 #1b1510,1px -1px 0 #1b1510,-1px -1px 0 #1b1510;}
/* v0.384 — BREATHING ROOM, literally. A scaled-up arrow still counts toward the row's scrollable
   width, so the swell on the outermost arrow pushed #iputGrid 10px past its own box on the
   unfolded screen and tripped the clipped-text check. Reserve the swell instead of clipping it. */
#iputBank{font-family:var(--fontD); font-size:11px; opacity:.8;}
#scr-inkput .seq-r{gap:10px; padding:6px 24px;}   /* v0.384 - wide enough that a BREATHING arrow swells inside the row instead of past its edge */
#scr-inkput .seq-r .arrowfill{width:28px; height:28px;}
#iputWrap.miss{animation:iputShake .22s;}
@keyframes iputShake{25%{transform:translateX(-7px)}75%{transform:translateX(7px)}}
/* ===== DANCE DANCE INK — standalone, four film-reel lanes (v3.05) ===== */
#scr-ddance{background:radial-gradient(120% 95% at 50% 12%,#3a1e50,#1e1236 52%,#0c0818);}
/* ⚠ margin, NOT padding: every child of this stage is absolutely positioned, and an absolute child
   resolves its `top` against the PADDING box — so padding here would move nothing at all. */
#ddiStage{flex:1; min-height:0; position:relative; z-index:5; overflow:hidden; margin-top:var(--hudtop-extra,0px);}
#ddiCombo{position:absolute; top:6px; left:50%; transform:translateX(-50%); z-index:7; font-family:var(--fontD); font-size:15px; color:#f4c542; letter-spacing:2px; text-shadow:0 0 10px #f4c542; min-height:16px; transition:transform .08s;}
#ddiCombo.pop{transform:translateX(-50%) scale(1.35);}
#ddiField{position:absolute; left:50%; transform:translateX(-50%); top:30px; bottom:calc(var(--padclear,238px) + 16px); width:min(94%,344px); display:flex; gap:6px; z-index:3;}   /* the receptors sit clear above the dual D-pads */
.ddi-lane{flex:1; position:relative; overflow:hidden; background:linear-gradient(#1a1512,#241b14 60%,#1a1512); box-shadow:inset 0 0 0 2px #0007;}
.ddi-lane::before,.ddi-lane::after{content:''; position:absolute; top:-24px; bottom:-24px; width:7px; background:repeating-linear-gradient(180deg,#e9d8b2 0 8px,transparent 8px 22px); opacity:.5; animation:ddiRun .5s linear infinite;}
.ddi-lane::before{left:2px;} .ddi-lane::after{right:2px;}
@keyframes ddiRun{from{transform:translateY(0)}to{transform:translateY(-22px)}}   /* the film runs UP now — the arrows climb */
.ddi-recept{position:absolute; left:0; right:0; top:0; height:52px; display:flex; align-items:center; justify-content:center; z-index:2; border-bottom:3px solid #ffffff44;}   /* receptors ride the TOP — press as the arrows reach them */
.ddi-recept svg{width:56%; max-width:40px; opacity:.34; transition:opacity .1s, transform .1s;}
.ddi-lane.lit .ddi-recept svg{opacity:1; transform:scale(1.15); filter:drop-shadow(0 0 8px #fff);}
.ddi-note{position:absolute; left:50%; top:0; transform:translate(-50%,0); z-index:3; will-change:transform;}
.ddi-note .ddi-head{display:block; width:100%; height:100%; position:relative; z-index:2;}
.ddi-note svg{width:100%; height:100%; display:block; filter:drop-shadow(0 2px 3px #000a);}
/* ⭐ THE FREEZE ARROW. The tail hangs BELOW the head, because the tail is the part of the note
   that arrives later — it is the same note still going on. It is drawn as a lit rail so it
   reads as something to keep standing on rather than as a second arrow. */
.ddi-note.hold .ddi-tail{position:absolute; left:50%; top:50%; transform:translateX(-50%); width:46%;
  background:linear-gradient(180deg,#fff9 0 3px,#f4c54288 3px,#c98a1e66); border-left:2px solid #ffffff66;
  border-right:2px solid #ffffff66; z-index:1; border-radius:0 0 4px 4px;}
.ddi-note.held .ddi-tail{background:linear-gradient(180deg,#fff 0 3px,#6bd0f0cc 3px,#2f8fbf99);
  box-shadow:0 0 10px #6bd0f0aa;}
.ddi-note.held svg{filter:drop-shadow(0 0 9px #6bd0f0) drop-shadow(0 2px 3px #000a);}
/* ⚠ the arrows are SHINY on purpose (Odaroo): white / dark / white contour with a multi-coloured
   inside. The three rings are three stacked strokes drawn in js/rhythm.js — do not try to fake
   them here with a single CSS outline, it cannot stack. */
.rg-arw{overflow:visible;}
/* the count-in — big, centred, and gone the moment the needle drops */
.rg-count{position:absolute; left:50%; top:44%; transform:translate(-50%,-50%); z-index:12;
  font-family:var(--fontD); font-size:clamp(30px,11vw,58px); color:#fdf6e3; letter-spacing:3px;
  text-shadow:3px 3px 0 #1b1510,0 0 24px #f4c542aa; pointer-events:none; text-align:center;
  width:min(90%,320px);}
.rg-row.locked{opacity:.5; cursor:default;}
.rg-row.locked .rg-stars{font-size:10px; letter-spacing:0;}
.ddi-judge{position:absolute; top:58px; left:50%; transform:translateX(-50%); z-index:8; font-family:var(--fontD); font-size:16px; letter-spacing:1px; pointer-events:none; animation:ddiJudge .5s ease-out forwards;}   /* the call sits just UNDER the receptor arrow */
@keyframes ddiJudge{0%{opacity:1; transform:translateX(-50%) scale(1.2)}100%{opacity:0; transform:translateX(-50%) translateY(14px) scale(1)}}
.ddi-milestone{position:absolute; left:50%; top:44%; transform:translate(-50%,-50%); z-index:9; text-align:center; pointer-events:none; animation:dmPop 1.1s ease-out forwards;}
.ddi-milestone .dm-n{font-family:var(--fontD); font-size:clamp(44px,16vw,72px); color:#f4c542; line-height:1; text-shadow:3px 3px 0 #1b1510,0 0 22px #f4c542;}
.ddi-milestone .dm-t{font-family:var(--fontD); font-size:clamp(13px,4vw,18px); color:#fff; letter-spacing:3px; margin-top:4px; text-shadow:2px 2px 0 #c93a22;}
@keyframes dmPop{0%{opacity:0; transform:translate(-50%,-50%) scale(.4) rotate(-4deg)}18%{opacity:1; transform:translate(-50%,-50%) scale(1.25)}36%{transform:translate(-50%,-50%) scale(1)}100%{opacity:0; transform:translate(-50%,-50%) scale(1.15)}}
/* ===== DANCE DANCE INK v3.10 — theater curtain, framed inkling, HP line, judgments ON TOP of the reel ===== */
#scr-ddance .ip-curtain{width:11%;}
.gink-ddi{top:4px; left:6px; width:70px;}
.ddi-stats .rg-hud-nm{color:#5fd0c8; text-shadow:0 0 8px #5fd0c888;}
.ddi-stats #ddiStarRow{position:static; transform:none; font-size:11px; letter-spacing:2px; color:#3a3348; margin-top:2px;}
.ddi-stats #ddiStarRow .on{color:#f4c542; text-shadow:0 0 8px #f4c542;}
#ddiCombo{top:56px;}
#ddiHp{position:absolute; left:50%; transform:translateX(-50%); top:40px; width:min(86%,300px); height:7px; background:#00000066; z-index:6; border:1px solid #ffffff22; overflow:hidden;}   /* v3.57 — rides just under the stars now */
#ddiHpF{display:block; height:100%; width:100%; background:linear-gradient(90deg,#c93a22,#f4c542,#6bbf59); transition:width .18s;}
#ddiHpF.low{background:#ff3b3b; animation:ddiHpLow .5s ease-in-out infinite;}
@keyframes ddiHpLow{50%{opacity:.5}}
#ddiField{top:56px; bottom:calc(var(--padclear,238px) + 16px); width:min(86%,300px);}   /* v3.57 — bar gone: the reel starts higher */
#ddiJudges{position:absolute; left:50%; transform:translateX(-50%); top:140px; width:min(86%,300px); display:flex; gap:6px; z-index:8; pointer-events:none;}
.ddi-jslot{flex:1; text-align:center; font-family:var(--fontD); font-size:13px; letter-spacing:1px; min-height:18px; opacity:0; text-shadow:0 2px 3px #000a;}
.ddi-jslot.show{animation:ddiJShow .5s ease-out;}
@keyframes ddiJShow{0%{opacity:1; transform:translateY(0) scale(1.25)}100%{opacity:0; transform:translateY(-12px) scale(1)}}
/* ===== INK TAR HERO — standalone guitar hero w/ song roulette, chords & holds (v3.05) ===== */
#scr-inktar{background:radial-gradient(120% 80% at 50% 0%,#241748 0%,#140f26 55%,#0a0a14 100%);}
/* ⚠ `inset:0` then `top:` — the shorthand first, the override second, or source order undoes it. */
#ithStage{position:absolute; inset:0; top:var(--hudtop-extra,0px); overflow:hidden; font-family:var(--fontD);}
#ithTitle{position:absolute; top:6px; left:50%; transform:translateX(-50%); font-size:11px; color:#e86a9a; letter-spacing:1px; z-index:6; text-shadow:0 0 10px #e86a9a99; white-space:nowrap; max-width:94%; overflow:hidden; text-overflow:ellipsis;}
#ithStarRow{position:absolute; top:24px; left:50%; transform:translateX(-50%); z-index:6; font-size:12px; letter-spacing:3px; color:#3a3348;}
#ithStarRow .on{color:#f4c542; text-shadow:0 0 8px #f4c542;}
#ithCombo{position:absolute; top:68px; right:12px; left:auto; transform:none; z-index:6; font-size:14px; color:#49c6c0; letter-spacing:2px; min-height:16px; text-shadow:0 0 10px #49c6c0; transition:transform .08s; text-align:right;}   /* v3.114 — top-RIGHT, stacked under the logo and STAGE tag, clear of the inkling */
#ithCombo.pop{transform:scale(1.35); transform-origin:right center;} #ithCombo.hot{color:#f4c542; text-shadow:0 0 12px #f4c542,0 0 4px #fff;}
.ith-hud{position:absolute; top:6px; z-index:6; text-align:center; color:#f2ead6;}
.ith-hud .lab{font-size:7px; letter-spacing:1px; opacity:.65;} .ith-hud .val{font-size:16px; color:#f4c542;}
#ithScore{left:8px;} #ithHi{right:8px;}
#ithHwy{position:absolute; left:50%; transform:translateX(-50%); top:58px; bottom:176px; width:min(94%,340px); display:flex; gap:5px; z-index:3; border-bottom:4px solid #fff9; box-shadow:0 4px 16px #fff6; background:linear-gradient(#ffffff05,#ffffff12);}
.ith-lane{flex:1; position:relative; overflow:hidden; border-left:1px solid #ffffff10; border-right:1px solid #ffffff10;}
.ith-lane::after{content:''; position:absolute; left:0; right:0; bottom:0; height:40%; background:linear-gradient(transparent,var(--lc,#fff2)); opacity:.16; pointer-events:none;}
.ith-note{position:absolute; left:8%; right:8%; height:20px; border-radius:6px; border:2px solid #00000077; background:var(--lc); box-shadow:0 0 10px var(--lc);}
.ith-note.hold{border-radius:10px;}
.ith-tail{position:absolute; left:26%; right:26%; border-radius:5px; background:var(--lc); opacity:.5; box-shadow:0 0 8px var(--lc); z-index:2;}
#ithFrets{position:absolute; left:50%; transform:translateX(-50%); bottom:92px; width:min(94%,340px); display:flex; gap:5px; z-index:6; align-items:flex-end;}
.ith-fret{flex:1; height:54px; border-radius:30px 30px 7px 7px; border:3px solid #00000088; background:var(--lc); box-shadow:0 5px 0 #00000066, inset 0 4px 8px #ffffff66, inset 0 -4px 8px #00000044; cursor:pointer; transition:transform .05s, filter .05s;}
.ith-fret.press{transform:translateY(6px) scale(.96); filter:brightness(1.75); box-shadow:0 0 16px var(--lc);}
#ithMeter{position:absolute; bottom:74px; left:50%; transform:translateX(-50%); width:min(94%,340px); height:6px; background:#00000066; z-index:6; border:1px solid #ffffff22;}
#ithMeter i{display:block; height:100%; width:50%; background:linear-gradient(90deg,#c93a22,#f4c542,#6bbf59); transition:width .2s;}
.ith-guitarist{position:absolute; bottom:16px; width:60px; z-index:5; animation:wjBob 1.4s ease-in-out infinite;}
.ith-guitarist.v{right:0; animation-delay:.3s;} .ith-guitarist.p{left:0;}
.ith-guitarist svg{width:100%; height:auto; display:block;}
.ith-guitarist .wj-gtr{position:absolute; bottom:2px; width:56px; z-index:6;}
.ith-guitarist.p .wj-gtr{right:-14px; transform:rotate(12deg);} .ith-guitarist.v .wj-gtr{left:-14px; transform:scaleX(-1) rotate(12deg);}
.ith-guitarist.v > svg:not(.wj-gtr){filter:hue-rotate(210deg) saturate(1.4);}
.ith-burst{position:absolute; z-index:7; font-size:12px; pointer-events:none; animation:wjBurst .4s ease-out forwards; color:#f4c542; text-shadow:0 0 6px #f4c542;}
/* the song roulette */
#ithRoul{position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:9; background:#0a0a14ee;}
#ithRoulName{font-family:var(--fontD); font-size:18px; color:#f4c542; letter-spacing:1px; min-height:24px; text-align:center; padding:0 12px; text-shadow:0 0 12px #f4c54288;}
@media (max-height:680px){ #ithHwy{top:52px; bottom:164px;} #ithFrets{bottom:84px;} .ith-fret{height:46px;} #ithMeter{bottom:70px;} .ith-guitarist{width:54px;}
  #ddiField{bottom:calc(var(--padclear,224px) + 16px);} }
/* ===== INK TAR HERO v3.10 — D-pad stays visible & centered; field lifted above it, framed inkling, fire-holds, setlist ===== */
#ithHwy{top:64px; bottom:324px; width:min(88%,318px);}      /* v3.57 — the letterbox bar is gone: the highway climbs 32px higher */
.rg-songlbl{position:absolute; left:50%; bottom:6px; transform:translateX(-50%); z-index:7;
  font-family:var(--fontD); font-size:clamp(8px,2.4vw,10px); letter-spacing:1px; color:#ffffffcc;
  text-shadow:1px 1px 0 #1b1018cc; white-space:nowrap; pointer-events:none; opacity:.85;}   /* v3.109 — the playing track, inside the game frame */
.rg-starsTR{position:absolute; top:8px; right:12px; z-index:8; font-family:var(--fontD); font-size:13px; letter-spacing:2px; color:#3a3348;}   /* the five stars alone, top-right — no bar behind them */
.rg-starsTR .on{color:#f4c542; text-shadow:0 0 8px #f4c542;}
#ithFrets{bottom:250px; width:min(88%,318px);} .ith-fret{height:50px;}
#ithMeter{bottom:234px; width:min(88%,318px);}
#ithCombo{top:46px;}
/* the FEATURED INKLING — the real player drop in its worn frame, top-left, NO box, reacts to combos */
/* ⭐ v3.64 — THE SHARED MINIGAME TITLE SHOWCASE (gameTitle()): wordmark + checkerboard flags, centred at
   the top. It sits BETWEEN the corner inkling (left ~86px) and the stage tag (right ~90px) and is
   pointer-transparent, so it can never take a tap or cover the field on either fold. */
/* Bounded to a CENTRE BAND rather than a raw 50% centre: the left inset clears the corner inkling and the
   right inset clears the STAGE tag, so on the narrow fold the title can never run into either corner. */
/* ⭐ v3.68 — the wordmark now lives top-RIGHT (bigger, racing-flag framed) with the STAGE tag stacked
   directly beneath it. Right-anchored with a width cap so gtitleFit() can shrink long names on the folded Fold. */
.gtitle{position:absolute; top:calc(12px + env(safe-area-inset-top,0px)); right:10px; left:auto; width:min(60vw,250px); z-index:8; display:flex; align-items:center;   /* v3.108 - a safe cushion: phone browser chrome / the camera cutout can never clip the wordmark */
  justify-content:flex-end; gap:7px; pointer-events:none; overflow:hidden;}
.gt-txt{display:flex; flex-direction:column; align-items:flex-end; line-height:1.05; text-align:right; min-width:0;}
.gt-txt b{font-family:var(--fontD); font-size:17px; letter-spacing:1px; color:#f4c542; font-weight:normal;
  text-shadow:2px 2px 0 #1b1018, 0 0 10px #f4c54255; white-space:nowrap;}
/* ⚠ v0.380 — RINGED, like every other word in this game that has to sit over artwork. The pale
   lilac was picked for the dark game screens and measured 1.5:1 against the rink's cream paper
   (caught by rkSelfTest before it shipped, not in a screenshot afterwards). The EN line above has
   always carried an ink shadow, which is what makes it legible on any backdrop; the JP line now
   carries the same one, so it reads on dark stages and pale ones alike. */
.gt-txt i{font-family:var(--fontJ,var(--fontD)); font-style:normal; font-size:9px; letter-spacing:2px; color:#e6dcf0; opacity:1; white-space:nowrap;
  text-shadow:1px 1px 0 #1b1018, -1px 1px 0 #1b1018, 1px -1px 0 #1b1018, -1px -1px 0 #1b1018;}
.gt-sub{font-family:var(--fontD); text-decoration:none; font-size:9px; letter-spacing:1px; color:#f4c542; opacity:.95; white-space:nowrap; margin-top:2px;}   /* v3.88 — STAGE 1-N line under the title */
/* ⭐ v0.424 — WHAT IS LEFT OF THE BAND. gameTitle no longer draws a title; when a game passes a
   third line ("STAGE 1-3") it comes out as this small corner tag on its own. It is live information
   about the run you are in, not the name of the game, so it stays. */
.gstage-tag{position:absolute; top:calc(6px + env(safe-area-inset-top,0px)); right:calc(8px * var(--gs,1));
  z-index:8; pointer-events:none; font-family:var(--fontD); font-size:calc(9px * var(--gs,1));
  letter-spacing:1px; color:#fdf6e3; background:#1b1510cc; border:calc(2px * var(--gs,1)) solid #fdf6e3;
  padding:calc(3px * var(--gs,1)) calc(6px * var(--gs,1)); white-space:nowrap;}
.gt-flag{flex:0 0 auto; width:19px; height:15px; background:repeating-conic-gradient(#fff 0 25%,#1b1018 0 50%) 0 0/7px 7px;
  border:1px solid #1b1018; animation:nimFlagWave 1.1s ease-in-out infinite;}
.gt-flag.r{animation-delay:.3s;}
/* ⭐ v0.404 — THE "?" ON EVERY GAME. It rides inside the wordmark band, hard against its right
   edge, so it inherits the band's safe-area cushion on every screen instead of needing eleven
   separate placements. flex:none so it never squeezes the title, and a real thumb-sized target. */
.gt-help{flex:0 0 auto; margin-left:calc(4px * var(--gs,1));
  width:calc(22px * var(--gs,1)); height:calc(22px * var(--gs,1)); padding:0;
  display:flex; align-items:center; justify-content:center; cursor:pointer;
  font-family:var(--fontD); font-size:calc(11px * var(--gs,1)); line-height:1;
  color:var(--cream); background:#1b1510; border:calc(2px * var(--gs,1)) solid #fdf6e3;
  border-radius:50%; box-shadow:2px 2px 0 #1b151088;}
.gt-help:active{transform:translateY(1px);}
.tut-how{text-align:left; margin:10px 4px 12px; line-height:1.75; font-size:10px;}
.tut-how p{margin:0 0 8px; padding-left:10px; text-indent:-10px;}
.tut-how p:before{content:'· '; opacity:.6;}
#scr-inktar .gtitle{top:calc(7px + env(safe-area-inset-top,0px));}   /* ITH's star row rides lower than the other games' — lift the title clear of it */
@media (max-width:430px){ .gt-txt b{font-size:14px; letter-spacing:.5px;} .gt-txt i{font-size:8px;} .gt-flag{width:15px; height:12px;} .gtitle{gap:5px; width:min(64vw,220px);} }
/* ⚠⚠ THE CONTACT SHADOW IS A `filter`, AND `--gink-drop` (below) IS THE ONE COPY OF IT. Any keyframe
   that writes `filter:` on a .gink must restate this var first or it deletes the shadow for the
   length of its animation — that was the v0.787 flicker. */
.gink{position:absolute; z-index:7; pointer-events:none; filter:var(--gink-drop);}
.gink svg{width:100%; height:auto; display:block; overflow:visible;}
.gink-ith{top:6px; left:6px; width:84px;}
.gink{animation:ginkIn .34s ease-out backwards;}   /* clean entrance — the inkling rises in instead of flashing in mid-rebuild */
@keyframes ginkIn{from{opacity:0; transform:translateY(8px) scale(.85)}to{opacity:1; transform:none}}
.gink.hype{animation:ginkBob .34s ease-in-out;}
.gink.sing{animation:ginkSing .5s ease-in-out infinite;}
.gink.glitchy{animation:ginkGlitch .62s steps(1,end) 1;}
@keyframes ginkBob{50%{transform:translateY(-8px) scale(1.06)}}
@keyframes ginkSing{0%,100%{transform:translateY(0) rotate(-3deg)}50%{transform:translateY(-6px) rotate(3deg)}}
/* ⭐⭐⭐ v0.787 — THE GLITCH IS A GLITCH NOW, NOT A FLICKER. (Odaroo filmed it: "the glitch effect is
   not really a glitch, it's more like it's flickering and just looks a little bit weird.")
   Three things were making it read as a broken flash rather than a digital fault:
   1. ⚠⚠⚠ THE KEYFRAMES WROTE `filter:` — AND `.gink` CARRIES ITS DROP SHADOW IN `filter`
      (line 252). So on the 25% and 50% poses the inklet's whole shadow was DELETED and then put
      back, twice a cycle. That, not the movement, is the flicker: the mascot was blinking its own
      contact shadow on and off nine times a second. Every filter below therefore restates
      `var(--gink-drop)` first — a filter list REPLACES, it never adds, exactly like the
      `animation` shorthand trap.
   2. ⚠⚠ `hue-rotate(90deg) → hue-rotate(220deg)` recolours the ENTIRE inklet — frame, gear and
      all — to green and then purple. A colour strobe is not chromatic aberration. A real digital
      glitch splits the channels APART and leaves the subject its own colour: two opposed
      `drop-shadow`s in red and cyan do exactly that, and cost nothing (no repaint of the SVG).
   3. ⚠⚠ `steps(2)` on a five-stop keyframe list chops EACH interval in half again, so the browser
      shows ten hard states in 0.18s — a 55Hz-ish buzz with no pose readable. It is now
      `steps(1,end)`: each pose SNAPS and then HOLDS, which is what makes a glitch look deliberate.
      Stepped is still right (see the crown.css note — this is a pixel cartoon, never an ease); the
      fault was the count, not the stepping.
   The shape is a BURST, not a strobe: hit, hold, settle, hit, gone. Roughly a third of the cycle
   is the inklet standing perfectly still, and that stillness is what the eye reads the glitch
   against. ⚠ Do not add frames "to make it busier" — a continuous fault is a flicker again. */
:root{ --gink-drop:drop-shadow(0 3px 6px #0007); }
@keyframes ginkGlitch{
  0%   {transform:none; filter:var(--gink-drop)}
  8%   {transform:translate(-3px,1px); filter:var(--gink-drop) drop-shadow(3px 0 0 #ff2f6dcc) drop-shadow(-3px 0 0 #17e8ffcc)}
  16%  {transform:translate(2px,0) skewX(-5deg); filter:var(--gink-drop) drop-shadow(-2px 0 0 #ff2f6dcc) drop-shadow(2px 0 0 #17e8ffcc) contrast(1.25)}
  24%  {transform:none; filter:var(--gink-drop)}
  46%  {transform:translate(4px,-1px) skewX(7deg); filter:var(--gink-drop) drop-shadow(-4px 0 0 #ff2f6dcc) drop-shadow(4px 0 0 #17e8ffcc)}
  54%  {transform:translate(-2px,1px); filter:var(--gink-drop) drop-shadow(2px 0 0 #ff2f6d99) drop-shadow(-2px 0 0 #17e8ff99) brightness(1.15)}
  62%  {transform:none; filter:var(--gink-drop)}
  84%  {transform:translate(-1px,0) skewX(-3deg); filter:var(--gink-drop) drop-shadow(1px 0 0 #ff2f6d88) drop-shadow(-1px 0 0 #17e8ff88)}
  92%,100%{transform:none; filter:var(--gink-drop)}
}
/* ⭐⭐ AND THE SIGNAL TEARS. The channel split alone still reads as "wobble"; what says CORRUPTED
   FRAME is a band of scanlines that slides down the picture once per burst. It is a sibling
   overlay, never a filter on the inklet itself — `.gink` is `position:absolute` already, so
   `inset:0` is free, and it must stay `pointer-events:none` (the whole portrait is).
   ⚠ It is drawn UNDER nothing and OVER the svg (z-index 2 inside a z-index-7 box), and it does not
   touch layout: adding a real element here would move the frame. */
.gink.glitchy::after, .gink.wild::after{
  content:''; position:absolute; inset:-2px; z-index:2; pointer-events:none; border-radius:inherit;
  background:repeating-linear-gradient(180deg,#0000 0 2px,#0b0b1466 2px 3px,#0000 3px 5px);
  mix-blend-mode:overlay; opacity:0; animation:ginkTear .62s steps(1,end) 1;}
.gink.wild::after{animation-duration:1.7s; animation-iteration-count:infinite;}
@keyframes ginkTear{
  0%,7%   {opacity:0; transform:translateY(-40%)}
  8%,23%  {opacity:1; transform:translateY(-14%)}
  24%,45% {opacity:0; transform:translateY(0)}
  46%,61% {opacity:1; transform:translateY(22%)}
  62%,83% {opacity:0; transform:translateY(40%)}
  84%,91% {opacity:.7; transform:translateY(58%)}
  92%,100%{opacity:0; transform:translateY(70%)}
}
/* stats stacked top-right */
.ith-stats{position:absolute; top:8px; right:8px; z-index:7; text-align:right; font-family:var(--fontD); color:#f2ead6; line-height:1.5;}
.ith-stats .rg-hud-nm{font-size:9px; color:#e86a9a; letter-spacing:1px; max-width:150px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; text-shadow:0 0 8px #e86a9a88;}
.ith-stats .lab{font-size:7px; opacity:.65; letter-spacing:1px;} .ith-stats b{font-size:14px; color:#f4c542;}
.ith-stats #ithStarRow{position:static; transform:none; font-size:11px; letter-spacing:2px; color:#3a3348; margin-top:2px;}
.ith-stats #ithStarRow .on{color:#f4c542; text-shadow:0 0 8px #f4c542;}
/* the FIRE-HOLD — the bar catches fire as you sustain, hotter with the multiplier; drop it and it dies */
.ith-note.fire{box-shadow:0 0 14px #ffae3c,0 0 26px #ff5c1c; animation:ithFire .18s linear infinite;}
.ith-fret.fire{box-shadow:0 0 20px #ffae3c,0 0 34px #ff5c1c; animation:ithFire .16s linear infinite;}
@keyframes ithFire{0%{filter:brightness(1.5) hue-rotate(0)}50%{filter:brightness(2.05) hue-rotate(-12deg)}100%{filter:brightness(1.5) hue-rotate(0)}}
/* ===== shared SETLIST menu (ITH + DDI) ===== */
.rg-menu{display:flex; flex-direction:column; align-items:center; padding:8px 10px calc(252px + var(--padny,0px)); overflow-y:auto;}
.rg-menu .wj-moon{top:14px; right:22px;}
.rg-jp{font-family:var(--fontD); font-size:12px; color:#e86a9a; letter-spacing:3px; margin-top:22px; text-shadow:0 0 10px #e86a9a88; z-index:6; text-align:center; width:100%; padding:0 10px; box-sizing:border-box;}
.rg-title{font-family:var(--fontD); font-size:clamp(17px,6vw,24px); color:#f4c542; letter-spacing:2px; text-shadow:3px 3px 0 #1b1018,0 0 18px #f4c54255; z-index:6; text-align:center; width:100%; max-width:94vw; padding:0 8px; box-sizing:border-box; line-height:1.35; overflow-wrap:break-word;}   /* v3.109 — always centred; wraps to more lines instead of clipping */
.rg-sub{font-family:var(--fontD); font-size:8px; color:#cbb8d8; letter-spacing:2px; margin:2px 0 10px; opacity:.85; z-index:6; text-align:center;}
.rg-list{width:min(94%,340px); display:flex; flex-direction:column; gap:6px; z-index:6;}
.rg-row{display:flex; justify-content:space-between; align-items:center; gap:8px; padding:9px 12px; background:#ffffff10; border:2px solid #ffffff22; border-radius:8px; cursor:pointer; transition:background .1s,transform .06s,border-color .1s; font-family:var(--fontD);}
.rg-row:not(.lock):hover,.rg-row:not(.lock):active{background:#f4c5421f; border-color:#f4c542; transform:translateY(-1px);}
.rg-row.lock{opacity:.5; cursor:default; border-style:dashed;}
.rg-nm{font-size:11px; color:#fdf6e3; letter-spacing:1px;}
.rg-stars{font-size:12px; letter-spacing:2px; color:#f4c542; white-space:nowrap;}
.rg-hint{font-size:7px; color:#cbb8d8; letter-spacing:1px; white-space:nowrap;}
.rg-flag{position:absolute; top:16px; width:34px; height:26px; z-index:6; background:repeating-conic-gradient(#fff 0 25%,#1b1018 0 50%) 0 0/13px 13px; border:1px solid #1b1018; animation:nimFlagWave 1.1s ease-in-out infinite;}
.rg-flag.l{left:16px;} .rg-flag.r{right:16px; animation-delay:.3s;}
@media (max-height:720px){ #ithHwy{top:74px; bottom:316px;} #ithFrets{bottom:246px;} .ith-fret{height:44px;} #ithMeter{bottom:230px;} .gink-ith{width:70px;} }
/* ===== RHYTHM one-line info bar (ITH + DDI) + inkling breathing room (v3.20) ===== */
.rg-topbar{position:absolute; top:0; left:0; right:0; height:26px; display:flex; align-items:center; gap:6px; padding:0 10px; z-index:9; background:linear-gradient(#0a0a14f2,#0a0a14bb); border-bottom:1px solid #ffffff22; font-family:var(--fontD); color:#f2ead6;}
.rg-topbar .rg-back{font-size:9px; letter-spacing:1px; color:#cbb8d8; cursor:pointer; white-space:nowrap;}
.rg-topbar .rg-song{flex:1; text-align:center; font-size:11px; letter-spacing:1px; color:#f4c542; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; text-shadow:0 0 8px #f4c54288;}
.rg-topbar .rg-right{display:flex; align-items:center; gap:8px; white-space:nowrap;}
.rg-topbar .rg-sc{font-size:10px; color:#fdf6e3;} .rg-topbar .rg-sc b{color:#f4c542; font-weight:normal;}
.rg-topbar .rg-stars span{font-size:11px; letter-spacing:1px; color:#3a3348;}
.rg-topbar .rg-stars span.on{color:#f4c542; text-shadow:0 0 6px #f4c542;}
.rg-topbar #ithStarRow, .rg-topbar #ddiStarRow{position:static; transform:none; top:auto; left:auto; margin:0;}   /* keep the stars IN the bar, not floating */
.gink-ith,.gink-ddi{top:32px !important; left:10px !important;}   /* below the bar, off the edge — room to breathe */
/* ===== UNIFIED GAME TOP BAR (every minigame): framed inkling top-left · EN name left · JP name right · live stats on the right ===== */
.gbar-flex{flex:0 0 auto; display:flex; align-items:center; gap:8px; padding:7px 12px; position:relative; z-index:9; background:linear-gradient(#0a0a14f4,#0a0a14c4); border-bottom:1px solid #ffffff22; font-family:var(--fontD);}
.gbar-flex .gb-back{font-size:9px; letter-spacing:1px; color:#cbb8d8; cursor:pointer; white-space:nowrap;}
.gbar-flex .gb-nm{font-size:13px; letter-spacing:1px; color:#f4c542; text-shadow:0 0 8px #f4c54288; white-space:nowrap;}
.gbar-flex .gb-jp{margin-left:auto; font-size:11px; letter-spacing:2px; color:#cbb8d8; opacity:.92; white-space:nowrap;}
.gink-game{top:calc(40px * var(--gs,1)) !important; left:calc(12px * var(--gs,1)) !important; width:calc(80px * var(--gs,1)); z-index:7;}   /* the featured framed inkling — top-left, same size/place as ITH/DDI */
.gbar-stats{position:absolute; top:42px; right:12px; z-index:7; text-align:right; font-family:var(--fontD); color:#f2ead6; line-height:1.35; pointer-events:none;}
.gbar-stats .gs{font-size:8px; letter-spacing:1px; opacity:.7; margin-bottom:3px; white-space:nowrap;}
.gbar-stats .gs b{display:block; font-size:15px; color:#f4c542; font-weight:normal; letter-spacing:0; opacity:1;}
@media (max-height:680px){ .gink-game{width:64px; top:36px !important;} .gbar-stats{top:38px;} }
/* ============================================================
   ⭐⭐ v0.453 — THE THREE RHYTHM HIGHWAYS OBEY THE MASTER STAGE.
   Ink-Tar Hero, Dance Dance Ink and Ink-Put! each pinned their playfield to a flat pixel width
   (318px / 300px / a row of 26px arrows). That is right on the folded Fold 6, where 318 of 375px is
   the whole screen — but it never moved on the unfolded one, so a 906px screen drew the same small
   highway with 500px of empty purple either side. It is the "unfolding zooms OUT" complaint, and
   the reason nobody caught it is that none of these three games had an INK_FIELD entry, so the
   master-stage rules were never run against them (v0.453 registers all of them).
   --inkwide is published by inkStage() and is the WIDTH ratio against the folded reference, floored
   at 1 — so unfolding can only ever make these wider, never narrower, and it changes only when the
   screen itself does. The % term still wins on any screen narrower than the cap, so the folded
   layout is untouched.
   ⚠ EVERY BOX IN A STACK MUST CARRY THE SAME WIDTH. The highway, the frets and the meter are three
   separate absolutely-positioned elements that only look like one instrument because their widths
   match; the same goes for DDI's field, HP bar and judge row. Widen one and you have built a
   staircase.
   ============================================================ */
#ithHwy, #ithFrets, #ithMeter{ width:min(88%, calc(318px * var(--inkwide,1))); }
#ddiField, #ddiHp, #ddiJudges{ width:min(86%, calc(300px * var(--inkwide,1))); }
/* ⭐⭐⭐ v0.780 — THE FRETS ARE TALLER, AND THIS IS THE ONE RULE THAT SETS THEIR HEIGHT.
   (Odaroo, 2026-08-10: the INK-TAR buttons should be taller and easier to press. v0.779 answered
   this by growing the GLOBAL A/B buttons, which was the wrong control on every other screen in the
   game; that is reverted and the height went here instead, where it belongs.)
   ⭐ v0.781 — AND THE NUMBER IS 96px FLAT (Odaroo, 2026-08-11: "only Ink-Tar's buttons are taller —
   make those 96"). It is deliberately NOT scaled by --inkwide: 96 is the size he asked for, and a
   fret that grew on the unfolded phone would be a different control on the two folds.
   ⚠⚠ THE FOUR `.ith-fret{height:…}` DECLARATIONS ABOVE THIS ONE ARE DEAD —
   same specificity, later rule wins — so editing one of them changes nothing and reads as a fix
   that did not work. They are left in place only because the #ithHwy / #ithFrets offsets they sit
   beside are very much alive.
   ⚠⚠ A TALLER FRET GROWS UPWARD (the row is bottom-anchored with no height of its own), straight
   into the neck above it. Every LIVE #ithHwy `bottom` was raised by the same 28px in the same pass
   (324→352, and 316→344 under max-height:720) — change one without the other and the notes are
   drawn behind the buttons. ⚠ The `bottom`s inside the max-height:680 block are dead for the same
   later-rule reason the heights are; the two that matter are the default and the ≤720 one. */
.ith-fret{ height:96px; }
#ithHwy{ bottom:352px; }
@media (max-height:720px){ #ithHwy{ bottom:344px; } }
/* INK-PUT!: a row of arrows, not an anchored highway — so the arrows themselves are what scales.
   Capped at 58px: past that a four-bar grid gets tall enough to reach for the controls. */
/* ⚠ #scr-inkput .seq-r .arrowfill already sets 28px and .seq-r.seq-r8 .arrowfill sets 24px — one id
   plus one class does NOT beat one id plus two, so the first version of this rule was outranked and
   silently did nothing while the two highways above it visibly worked. Match the deepest selector. */
#scr-inkput #iputGrid .seq-r .arrowfill, #scr-inkput #iputGrid .seq-r .seq-ab{
  width:min(calc(28px * var(--inkwide,1)), 58px); height:min(calc(28px * var(--inkwide,1)), 58px); }
#scr-inkput #iputGrid .seq-r .seq-ab{ font-size:min(calc(13px * var(--inkwide,1)), 27px); }
#scr-inkput #iputGrid .seq-r{ gap:min(calc(10px * var(--inkwide,1)), 22px); }
/* SUPERSTARS: the timing gauge is the playfield, and it was pinned to 320px like the highways were.
   (It lives in games.css, but the rule that governs it is this one, so it is written beside it.) */
#scr-super #supGaugeWrap{ width:min(88%, calc(320px * var(--inkwide,1))); }

/* ============================================================
   ⭐⭐⭐ INK-PUT! — THE INPUT CHALLENGE (v0.542 layout, Odaroo's own notes)

   THE TOP LINE IS A FIGHTING-GAME HEALTH ROW, and it is one line:
     [ my inkling · my name ] [ MY BAR → drains RIGHT to LEFT ] [ THEIR BAR → drains LEFT to RIGHT ] [ their inkling · their name ]
   Each bar empties TOWARD ITS OWNER, so the two portraits are what the damage is running at.
   ⚠⚠ NO FILM STRIP BETWEEN THE BARS. The shared vsNameBar() sprocket strip used to run down the
   middle of this row and Odaroo asked for it gone — the row carries its own names now, so Ink-Put
   deliberately does NOT call vsNameBar/vsFoeInk. Do not add them back.

   ⚠⚠ INSTRUCTIONS ARE NEVER A CENTRED POP-UP. What the current challenge wants sits BESIDE the
   inkling in a Crown-Deluxe-style panel (#ipInfo), under the health row on the left. The middle of
   the screen belongs to the arrows and nothing else.

   ⚠⚠ v0.613 REVERSED THE OLD RULE HERE, AND THE OLD WORDING WENT WITH IT. There is no marquee on
   this screen at all any more: nothing scrolls across the top, and #ipTicker is now a single
   rise-and-fade health figure over the playfield (see its own block below). Do not put a rolling
   line back — Odaroo asked for every scrolling caption gone by name.
   ============================================================ */
#scr-inkput .gbar-stats{display:none;}   /* the stats live under the health row now, not top-right */

/* ============================================================
   ⭐⭐⭐ v0.613 — THE HEALTH FLASH. THIS IS NOT A MARQUEE ANY MORE.
   Odaroo asked for every scrolling line across the top gone; what replaces it is one number that
   rises and fades over the middle of the playfield — `+20% HP` on a clear, `-8% HP` on a fluff.
   ⚠⚠ IT IS CENTRED AND IT DOES NOT SCROLL, which is what makes it impossible to clip: the old
   ticker was a `white-space:nowrap` line pinned between a fixed left edge and the screen edge, so a
   long line simply ran off the folded phone. This one is centred with its own translate and wraps
   nowhere near the edges.
   ⚠ THE POSITION MAY NOT DEPEND ON WHAT THE WORDS SAY (CLAUDE.md). It is anchored by transform on
   its own line rather than by a width that re-measures per message, so a two-character figure and a
   twelve-character one sit in exactly the same place.
   ============================================================ */
#ipTicker{position:absolute; left:0; right:0; top:38%; height:0; z-index:9; pointer-events:none; text-align:center;}
#ipTicker b{display:inline-block; white-space:nowrap; font-family:var(--fontD); font-size:clamp(15px,4.6vw,30px);
  letter-spacing:2px; color:#f4c542;
  text-shadow:2px 2px 0 #1b1510, -2px 0 0 #1b1510, 0 -2px 0 #1b1510, 2px 0 0 #1b1510, 0 2px 0 #1b1510;
  animation:ipPopUp 1.5s ease-out forwards;}
#ipTicker b.bad{color:#ff8b6b;}
@keyframes ipPopUp{
  0%{transform:translateY(10px) scale(.7); opacity:0}
  18%{transform:translateY(0) scale(1.1); opacity:1}
  30%{transform:translateY(0) scale(1); opacity:1}
  70%{transform:translateY(-14px) scale(1); opacity:1}
  100%{transform:translateY(-34px) scale(1); opacity:0}}
@media (prefers-reduced-motion:reduce){ #ipTicker b{animation:ipPopUp 1.5s steps(1,end) forwards;} }

/* ---- the health row ---- */
/* ⭐⭐⭐ v0.663 — THE PAD'S RESERVE MUST BE MULTIPLIED BY --gs, NOT JUST OFFSET BY --padny.
   #gpad is `240px * var(--gs) + var(--sab) + var(--padny)` (css/home.css) — its height SCALES. This
   reserve was a flat 250px plus the nudge, which is only ever right at --gs:1. Unfolded, --gs is
   about 1.13, so the real pad stands ~282px tall while 260px was being kept for it, and the arrow
   field's bottom landed at 577 against a pad starting at 551 — 26px of playfield underneath the
   controls, exactly the failure the old comment here was written to warn about.
   ⚠ `* var(--gs,1)` is the fix, and it is not new: #menuStack/.grid have reserved
   `248px * var(--gs,1) + var(--sab,0px) + var(--padny,0px)` since v0.381. --sab comes with it,
   because the pad's own height counts the safe-area inset too.
   ⚠ THE SAME FLAT-NUMBER RESERVE IS STILL WRITTEN ON OTHER SCREENS (236/246/248/252 in base.css,
   games.css, tetrink.css and #iputWrap's own base rule above). They are the same latent bug and
   should get the same treatment — deliberately not swept up here, so this change stays reviewable. */
#scr-inkput #iputWrap{justify-content:flex-start; align-items:stretch; padding:26px 8px calc(250px * var(--gs,1) + var(--sab,0px) + var(--padny,0px)); gap:6px;}
#ipVs{display:flex; align-items:center; gap:6px; width:100%; z-index:6; flex:none;}
/* ⭐⭐ v0.588 — THE NAME COLUMN GROWS ONLY AS FAR AS A LONG NAME NEEDS. A fixed 60px column with an
   ellipsis meant a 12-character name — the length this screen caps at — was CUT OFF on the folded
   phone at every size its own fitter could reach, floor included. It had been failing quietly
   because the default name is short; the pre-flight caught it the moment a worst-case name was on
   the save, which is exactly the v0.454 rule about testing the longest value a field can hold.
   ⚠⚠ IT IS , NOT A BIGGER FIXED WIDTH, AND THAT MATTERS IN AN ONLINE MATCH. This row is
   who · bar · bar · who when two people are playing, so 100px of hard width each would have taken
   80px off the two health bars whether the names needed it or not. Fluid, most names cost the row
   nothing and only a genuinely long one spends the slack.
   ⚠ MEASURED, NOT PICKED: the widest string the 12-char cap can produce ("WWWWWWWWWWWW") is 72px at
   5px, 96px at 7px and 108px at 8px in this face. 100px is the 7px fit with a pixel to spare, and
   7px is the floor ipNameFit now stops at — 5px was below legible and still did not fit. */
.ipvs-who{display:flex; flex-direction:column; align-items:center; gap:2px; width:auto; min-width:60px; max-width:100px; flex:none;}
/* ⭐ v0.640 — INK-PUT'S TWO FACES ARE THE HOUSE SIZE TOO. They were 46px folded / 66px unfolded —
   a third size for the same drawing, on the one VS screen that draws its own portraits instead of
   using the shared corners. --inkav (css/base.css) is the one number; both halves of the row read
   it, so the player and the rival are the same size as each other AND as the main menu's lens. */
/* ⭐⭐⭐ v0.719 — THE FACE SPILLS OVER ITS OWN FRAME, IT IS NEVER CUT OFF BY IT. (Odaroo,
   2026-08-06: the inklet "should start moving from within their frame, sometimes moving a little
   bit over the frame".) `overflow:hidden` sliced the top off the portrait on every ginkBob — the
   Ink-Put version of the same fault the corners had, where the head disappeared instead of hopping.
   The name sits BELOW this box, so an upward hop has nothing to swallow; z-index keeps the hop in
   front of the health bar beside it rather than under it. */
.ipvs-face{position:relative; z-index:2; width:var(--inkav,69px); height:var(--inkav,69px); border:2px solid #1b1510; background:#2a1c1c; border-radius:3px; overflow:visible;}
.ipvs-face svg{width:100%; height:100%; display:block;}
/* ⚠ THE SHARED .gink-game IS PINNED TOP-LEFT WITH !important — inside the health row it has to sit
   in the flow instead, or both portraits stack in the corner on top of each other. */
#scr-inkput .ipvs-face .gink-game{position:static !important; top:auto !important; left:auto !important;
  width:100% !important; height:100%; z-index:auto;}
#scr-inkput #iputInk{display:none;}   /* the ONE inkling is the health row's left portrait now */
.ipvs-nm{font-family:var(--fontD); font-size:8px; letter-spacing:1px; color:#fdf6e3; text-shadow:1px 1px 0 #1b1510;
  max-width:100px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:center;}
.ip-hpbar{position:relative; flex:1 1 0; min-width:0; height:24px; border:3px solid #1b1510;
  background:#2a1c1c; border-radius:3px; overflow:hidden; box-shadow:0 0 0 2px #fdf6e344;
  display:flex; align-items:stretch;}
/* ⚠ THE DIRECTION IS THE POINT. Mine hugs the RIGHT edge so it empties leftward into my own
   portrait; theirs hugs the LEFT so it empties rightward into theirs. */
.ip-hpbar.mine{justify-content:flex-end;}
.ip-hpbar.foe{justify-content:flex-start;}
/* ⚠⚠ v0.834 — drains on `scale`; `width` re-solved the page on every frame of every run.
   ⚠⚠⚠ THE ORIGIN IS THE EDGE THE BAR HUGS, AND THE TWO HUG OPPOSITE EDGES — read
   `.ip-hpbar.mine{justify-content:flex-end}` / `.foe{justify-content:flex-start}` twelve lines
   below before touching these. YOURS is pushed to the RIGHT of its track and so must recede
   rightwards; THEIRS sits at the LEFT and recedes leftwards. (The js comment above ipFoeHpPaint
   states this and the first version of this rule still got it backwards — with `width` the flex
   alignment did the mirroring for free, and the moment the fill became full-width it stopped
   doing anything and the origin became the only thing saying which way the bar empties.) */
#ipHpF,#ipFoeF{height:100%; width:100%; transition:scale .08s linear;}
.ip-hpbar.mine #ipHpF{transform-origin:right center;}
.ip-hpbar.foe  #ipFoeF{transform-origin:left center;}
#ipHpF{background:linear-gradient(270deg,#2e9e4f,#7ee87e);}
#ipFoeF{background:linear-gradient(90deg,#3b5e8c,#5cc8ff);}
#ipHpF.low,#ipFoeF.low{background:linear-gradient(90deg,#8a1826,#ff5c5c); animation:ipHpLow .5s steps(2,end) infinite;}
@keyframes ipHpLow{50%{filter:brightness(1.7)}}
.ip-hpbar>span{position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  font-family:var(--fontD); font-size:11px; color:#fdf6e3; text-shadow:1px 1px 0 #1b1510, -1px -1px 0 #1b1510; letter-spacing:1px;}
#scr-inkput.ip-solo .ipvs-who.foe, #scr-inkput.ip-solo .ip-hpbar.foe{display:none;}   /* no rival, no second bar */

/* ---- what the challenge wants: BESIDE the inkling, never over the playfield ---- */
#ipUnder{display:flex; align-items:flex-start; gap:8px; width:100%; z-index:6; flex:none;}
/* ⭐⭐⭐ v0.548 — THE INSTRUCTIONS ARE DRESSED AS A NAME. (Odaroo, 2026-08-03: "in-game instruction
   text uses the SAME font, SAME size and SAME spacing as the nicknames, on the same film-reel strip
   — instructions should be as clear as the names.")
   ⚠⚠ THE RECIPE IS .vsnm-c IN css/hub.css AND IT IS RESTATED HERE RATHER THAN INHERITED, for the
   same two reasons the rink's HUD restates it (css/rink.css): an id beats a bare class, so half of
   .inkfilm would lose to the rules that were already here; and the sprocket holes are a
   background-IMAGE, which any `background:` shorthand on this box silently wipes. If .vsnm-c's
   numbers change, change these with them — that is the whole point of matching.
   ⚠ 13px/16px is the NAME's size, and both lines carry it: the challenge title and the "what to
   press" line under it were 11px and 8px, which made the instruction the quietest text on a screen
   whose entire job is telling you what to press. */
#ipInfo{flex:1 1 auto; min-width:0; border-radius:0; padding:calc(6px * var(--gs,1)) calc(8px * var(--gs,1));
  background-color:#0d0a08;
  border-top:calc(2px * var(--gs,1)) solid #0d0a08; border-bottom:calc(2px * var(--gs,1)) solid #0d0a08;
  border-left:0; border-right:0;
  box-shadow:0 calc(2px * var(--gs,1)) 0 rgba(13,10,8,.55);
  /* v0.567 - the same holes, now CENTRED and MIRRORED. The recipe is --film-tile in css/base.css
     and it is pulled in by var rather than copied, so this strip can never drift from the reel. */
  background-image:linear-gradient(to right,
     transparent 0 calc((var(--film-per) - var(--film-w))/2 * var(--gs,1)),
     var(--film-hole) calc((var(--film-per) - var(--film-w))/2 * var(--gs,1)) calc((var(--film-per) + var(--film-w))/2 * var(--gs,1)),
     transparent calc((var(--film-per) + var(--film-w))/2 * var(--gs,1)) calc(var(--film-per) * var(--gs,1))),
    linear-gradient(to right,
     transparent 0 calc((var(--film-per) - var(--film-w))/2 * var(--gs,1)),
     var(--film-hole) calc((var(--film-per) - var(--film-w))/2 * var(--gs,1)) calc((var(--film-per) + var(--film-w))/2 * var(--gs,1)),
     transparent calc((var(--film-per) + var(--film-w))/2 * var(--gs,1)) calc(var(--film-per) * var(--gs,1)));
  background-size:calc(var(--film-per) * var(--gs,1)) calc(var(--film-h) * var(--gs,1)), calc(var(--film-per) * var(--gs,1)) calc(var(--film-h) * var(--gs,1));
  background-position:center calc(var(--film-in) * var(--gs,1)), center calc(100% - var(--film-in) * var(--gs,1));
  background-repeat:repeat-x, repeat-x;}
#ipInfo b{display:block; font-family:var(--fontD); font-size:calc(13px * var(--gs,1));
  letter-spacing:calc(.5px * var(--gs,1)); line-height:1.35;
  color:#f4c542; text-shadow:1px 1px 0 #1b1510;}
#ipInfo span{display:block; font-family:var(--fontD); font-size:calc(13px * var(--gs,1));
  letter-spacing:calc(.5px * var(--gs,1)); color:#fdf6e3; line-height:1.35; margin-top:calc(2px * var(--gs,1));}
@media (min-width:640px){
  #ipInfo b, #ipInfo span{font-size:calc(16px * var(--gs,1));} }
/* ⭐⭐⭐ v0.604 — AND SINCE THERE ARE NO INSTRUCTIONS, THE PANEL COLLAPSES. Odaroo asked for the
   instruction line gone: what is on the screen is what is happening. The element stays in the markup
   (the run stats sit beside it and the self-test watches it), so this is what stops an EMPTY box
   holding ~30px of the playfield open all run — every one of the properties that give it size is
   zeroed, not just the padding, or the borders and the film-hole background keep it visible.
   ⚠ It must come AFTER the #ipInfo rule above: same specificity plus :empty, so source order is
   what decides it. */
#ipInfo:empty{flex:0 0 0; padding:0; border:0; box-shadow:none; background-image:none; background-color:transparent; min-height:0;}
/* ⭐⭐ AND WITH THE PANEL GONE THE RUN STATS TAKE THE LEFT COLUMN, which is where the house style has
   always wanted them: "all stats stack UNDER the inkling, on the LEFT" (CLAUDE.md). They only sat on
   the right because the instruction panel owned the left half of this row; with nothing there,
   right-aligning them would leave the figures marooned against the far edge of an empty line. */
#ipUnder:has(#ipInfo:empty) #ipRun{text-align:left; margin-right:auto;}
#ipRun{flex:none; text-align:right; font-family:var(--fontD); color:#f2ead6; line-height:1.35;}
#ipRun .gs{font-size:8px; letter-spacing:1px; opacity:.7; margin-bottom:2px; white-space:nowrap;}
#ipRun .gs b{display:block; font-size:14px; color:#f4c542; font-weight:normal; letter-spacing:0; opacity:1;}

/* ---- the playfield ---- */
#ipField{position:relative; width:100%; flex:1 1 auto; min-height:180px; z-index:6;}
/* ⚠⚠ EXPLICIT z-index ON BOTH. The canvas is a positioned backdrop, so without a stacking order of
   its own it paints over the prompt layer — the same paint-order trap that made Ink Panik's whole
   side column unreadable. inkBackdropFaults() checks this directly on every game. */
#ipCanvas{position:absolute; inset:0; width:100%; height:100%; z-index:0;}
#ipPrompt{position:absolute; inset:0; z-index:2; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; pointer-events:none;}
/* ⭐⭐ THE SIZE IS SOLVED IN JS AGAINST #ipField's OWN BOX (see ipFit) and arrives as --ipa; the
   number of arrows per line arrives as --ipcols. A fixed pixel cap here is what made a four-arrow
   wave four small icons in a big empty box on the folded phone. */
.ip-row{display:grid; grid-template-columns:repeat(var(--ipcols,4), var(--ipa,44px));
  gap:calc(var(--ipa,44px) * .20); align-items:center; justify-items:center; justify-content:center; max-width:100%;}
.ip-arw{width:var(--ipa,44px); height:var(--ipa,44px); display:inline-block;}
/* ⭐⭐⭐ v0.732 — THE ROW PIPS. (Was the page dots.) An arrow wave is a GRID of up to 24 now, four to
   a line, so this strip counts ROWS rather than pages — six pips on a 24-BIT board. It is deliberately
   small and quiet: it is a count, not a second thing to read.
   ⚠ ON A **BLIND** WAVE IT IS THE ONLY THING ON SCREEN THAT SAYS HOW LONG THE WAVE IS, which is why
   it may never be hidden to save room. */
.ip-pages{display:flex; gap:7px; align-items:center; justify-content:center; margin-top:4px;}
/* ⭐ the 24-BIT board wears gold pips, so the rare wave is recognisable the moment it deals without
   a line of text announcing it (this game names nothing on its own playfield — see CLAUDE.md). */
.ip-pages.bit24 i{box-shadow:0 0 0 2px #1b1510cc, 0 0 6px #f4c54288;}
.ip-pages.bit24 i.done{background:#f4c542;}
.ip-pages i{width:9px; height:9px; border-radius:50%; background:#fdf6e340; box-shadow:0 0 0 2px #1b151088;}
.ip-pages i.done{background:#f4c542aa;}
.ip-pages i.now{background:#fdf6e3; transform:scale(1.25);}
.ip-arw.ip-cur{transform:scale(1.25); filter:drop-shadow(0 0 6px #fff);}
.ip-arw.ip-hit{opacity:.6; filter:drop-shadow(0 0 4px #f4c542);}
.ip-arw.ip-dim,.ip-arw.ip-hid{opacity:.28;}
.ip-arw.ip-flash{transform:scale(1.35); filter:drop-shadow(0 0 9px #f4c542);}
.ip-sub{font-family:var(--fontD); font-size:clamp(13px, calc(var(--ipa,44px) * .42), 34px); letter-spacing:3px; color:#f4c542; text-shadow:2px 2px 0 #1b1510;}
.ip-big{font-family:var(--fontD); font-size:clamp(40px, calc(var(--ipa,44px) * 1.5), 150px); line-height:1; color:#fdf6e3; text-shadow:3px 3px 0 #1b1510;}
/* ============================================================
   ⭐⭐⭐ v0.596 — Ⓐ AND Ⓑ INSIDE AN ARROW SEQUENCE, DRAWN AS THE PAD'S OWN BUTTONS.
   Odaroo: they must look EXACTLY like the A and B on the D-pad at the bottom of the screen. So the
   markup carries the pad's real classes (.gpb + .gpa/.gpbtn2) and inherits the pad's fill, keyline,
   colour and typeface for free — this rule only undoes the two things that cannot come across.
   ⚠⚠ POSITION IS THE ONE THAT MATTERS. .gpa and .gpbtn2 are `position:absolute`, pinned to the
   corners of the pad's own box; inherited into a grid row every button would stack on the first one
   and the sequence would look like it had a single Ⓐ in it. `position:static` is not cosmetic here,
   it is what makes the row a row.
   ⚠ AND THE SIZE COMES FROM --ipa like every other slot, not from the pad's fixed 72px — a
   fourteen-slot wave solves down to about 30px a slot and a 72px button would blow the row apart.
   ============================================================ */
/* ⚠⚠ BOX-SIZING FIRST. The pad's buttons do not set it, so the 3px keyline is drawn OUTSIDE the
   width — measured at 79px against arrows of 67px, which made every button slot fatter than every
   arrow slot and knocked the grid out of true (eight slots came out on two ragged rows instead of
   one). The border has to sit INSIDE --ipa for a button and an arrow to occupy the same square. */
.ip-arw.ip-btn{box-sizing:border-box!important;
  position:static!important; width:var(--ipa,44px)!important; height:var(--ipa,44px)!important;
  left:auto!important; right:auto!important; top:auto!important; bottom:auto!important;
  border-radius:50%!important; border-width:calc(var(--ipa,44px) * .08)!important;
  font-size:calc(var(--ipa,44px) * .42)!important; line-height:1;
  display:flex; align-items:center; justify-content:center; padding:0;
  box-shadow:0 calc(var(--ipa,44px) * .05) 0 #00000066;
  /* the same optical nudge the real buttons get — Press Start 2P carries its ink high and left */
  text-indent:calc(var(--ipa,44px) * -.03);}
.ip-arw.ip-btn > *{pointer-events:none;}
/* the cursor and the struck slots read the same on a button as they do on an arrow */
.ip-arw.ip-btn.ip-cur{filter:drop-shadow(0 0 6px #fff) brightness(1.15);}
.ip-arw.ip-btn.ip-hit{opacity:.6; background:#f4c542!important; color:#1b1510!important;}

@media (min-width:640px){
  #ipTicker b{font-size:30px;}
  /* ⚠⚠ v0.588 — THE UNFOLDED COLUMN HAD TO MOVE TOO, AND IT WOULD HAVE BROKEN SILENTLY IF IT HAD NOT.
     Raising ipNameFit s floor from 5px to 7px fixes the folded clip and, on its own, CREATES one
     here: 84px held the 12-character worst case only because the fitter was allowed to go down to
     5px (72px). At the new floor that same name wants 96px. So this column grows on the same fluid
     rule as the folded one, with 130px — the 9px fit — as the ceiling, since a wide screen can
     afford a bigger floor than a narrow one. */
  .ipvs-who{width:auto; min-width:84px; max-width:130px;} .ipvs-nm{font-size:11px; max-width:130px;}   /* v0.640 — the face size comes from --inkav now, not from this breakpoint */
  .ip-hpbar{height:32px;} .ip-hpbar>span{font-size:15px;}
  #ipRun .gs{font-size:11px;} #ipRun .gs b{font-size:19px;}
}

/* ============================================================================
   ⭐⭐⭐ v0.781 — THE RHYTHM DUEL'S DRESS IS THE HOUSE DRESS NOW.

   (Odaroo, 2026-08-11: "make sure the top information bar is completely removed… two inklets
   under both names, both names on film reels, the same design. Under the usernames an HP bar as
   large as the film reel, for both inklings being versed.")

   v0.698's `.rgvs` — a bespoke strip carrying two portraits, two names, two bars and a clock
   across the top of the playfield — is DELETED. Everything it did is now done by the shared VS
   HUD (`.vsnm` film reel + `.vsfoe` portrait + `.vsmid` clock, css/hub.css), which is what every
   other online game in Inkville wears. ⚠⚠ DO NOT REINTRODUCE A RHYTHM-ONLY TOP BAR: the whole
   point of the change is that a rhythm duel and a Tetr-Ink duel read as the same game's HUD.

   What is left here is the two pieces the shared HUD does not own:
     · `.rghp` — the health bar that hangs under each name cell. It is POSITIONED AND SIZED FROM
       JS (rgnHudPlace, js/online.js) because "as wide as the film reel" is a measurement of a
       shrink-to-fit cell, not a percentage anybody can write down.
     · `.rg-foeside` — the rival's figures, under the rival's inklet, mirroring my own column.
   ============================================================================ */
/* ⚠⚠ NO WIDTH, NO LEFT AND NO TOP HERE — all three are written inline off the reel cell every
   time the screen moves. A stylesheet default would be visible for one frame at the wrong size on
   every match, and would silently become the layout on any path where the placer does not run. */
.rghp{position:absolute; z-index:11; pointer-events:none; overflow:hidden;
  height:calc(11px * var(--gs,1)); background:#00000088;
  border:calc(2px * var(--gs,1)) solid #0d0a08;
  box-shadow:0 calc(2px * var(--gs,1)) 0 rgba(13,10,8,.55);}
.rghp i{display:block; height:100%; width:100%; transition:width .1s linear;}
/* ⚠ BOTH BARS FILL FROM THE LEFT AND DRAIN FROM THE RIGHT. They are deliberately NOT mirrored —
   that was asked for explicitly in v0.698 and nothing about this redesign changes it. */
.rghp.l i{background:linear-gradient(90deg,#2e9e4f,#7ee87e);}
.rghp.r i{background:linear-gradient(90deg,#3b5e8c,#5cc8ff);}
.rghp i.low{background:linear-gradient(90deg,#8a1826,#ff5c5c); animation:rgvsLow .5s steps(2,end) infinite;}
@keyframes rgvsLow{50%{filter:brightness(1.75)}}

/* ============================================================================
   ⭐⭐⭐ v0.733 — THE RHYTHM SIDE COLUMN (Tetr-Ink's spatial logic, both games, both modes).

   Odaroo: "one set of arrows as the main event. On the side you have all the stats… the enemy
   frets on the left-hand side."

   ⚠⚠ EVERY RULE HERE IS UNCONDITIONAL — it is NOT fenced behind `.ddi-vs` / `.ith-vs` any more,
   and that reversal is the whole point. The old dress halved your dance floor and shoved it
   off-centre the moment you went online, so the duel was a differently-shaped game to the one you
   had practised solo. The column is now part of BOTH; going online only fills its bottom third.
   ⚠⚠ `--rgside` IS THE ONE NUMBER. The column's width and the playfield's centre are both written
   from it, so the two can never disagree. Change it here and only here.
   ⚠ THE PLAYFIELD'S CENTRE IS `50% + --rgside/2`, not 50%: the space left over starts at the
   column's right edge and ends at the screen's, so its middle is half the column further across.
   Anything anchored on the old bare 50% would sit visibly left of the lanes it belongs to — which
   is exactly what happens if #ddiJudges or #ithFrets is ever added back without this offset.
   ============================================================================ */
#scr-ddance,#scr-inktar{--rgside:86px;}
/* ⚠⚠ THE COLUMN STARTS BELOW THE USERNAME STRIP, NOT AT THE TOP OF THE STAGE. The inklet is the
   first thing in it, and the standing rule (inkGinkFaults, js/inktest.js) is that the face sits
   BELOW its own name and may never grow up into it — and it does grow: `ginkBob`/`ginkSing` lift
   it ~14px every time you score, which a resting measurement cannot see. 44px cleared the strip
   standing still and covered it on every combo. */
.rg-side{position:absolute; left:6px; top:66px; width:calc(var(--rgside) - 10px); z-index:8;
  display:flex; flex-direction:column; gap:6px; align-items:stretch;
  bottom:calc(var(--padclear,238px) + 12px); pointer-events:none;}
/* ⚠ the game inkling is `position:absolute` wherever it is drawn (gameInk), so inside a flex
   column it has to be put back into the flow or it flies to the screen's top-left corner — the
   same trap ipFoePanel and the rhythm versus row both hit. */
.rg-side .gink{position:static !important; top:auto !important; left:auto !important;
  width:100% !important; max-width:56px; align-self:center; flex:none;}
.rg-stats{display:flex; flex-direction:column; gap:2px; font-family:var(--fontD); flex:none;}
/* ⚠⚠ LABEL ABOVE VALUE, NEVER SIDE BY SIDE. An 86px column cannot hold "SCORE" and "128,400" on
   one line at a readable size — that is the v0.446 narrow-column lesson, and stacking is what
   Tetr-Ink's own run box does for the same reason. */
/* ⚠⚠⚠ THE FIGURE IS SIZED FROM ITS OWN BOX, NOT FROM THE VIEWPORT. This is the v0.446
   Tetr-Ink TIME lesson verbatim, and it bit here within the hour of building the column: a flat
   13px read "128,…" the moment the score passed six digits — which it does in the first minute of
   every run — while a fresh run's "0" looked perfect in every screenshot. `container-type` +
   `cqw` makes the number shrink to the column it is in, so the widest value it can ever hold still
   fits, on both folds, with no second breakpoint to remember.
   ⚠ TEST IT AGAINST THE LONGEST VALUE, NEVER A FRESH RUN (the standing rule in CLAUDE.md). */
.rg-st{display:flex; flex-direction:column; line-height:1.1; background:#00000055;
  border:1px solid #ffffff1a; padding:2px 4px; min-width:0; container-type:inline-size;}
.rg-st span{font-size:7px; letter-spacing:1px; color:#cdbfa6; opacity:.85;}
.rg-st b{font-size:min(13px, 14cqw); font-weight:normal; color:#f4c542; text-shadow:1px 1px 0 #1b1510;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:block; letter-spacing:0;
  transition:transform .08s; transform-origin:left center;}
/* the column's COMBO is the one that pops now — see below */
.rg-st b.pop{transform:scale(1.3);}
/* ---------- the rival's block: their figures, under their own inklet, on the RIGHT ----------
   ⭐⭐⭐ v0.781 — THE MINIATURE BOARD IS DELETED. (Odaroo: "completely remove the actual little
   window that shows the screen of the other inkling — save space and feature the actual gameplay
   footage of the user.") `.rg-foe`, `.rg-foe-board`, `.rg-foe-nm` and the two ghost-note rules
   went with it; so did ddiGhostPaint / ithGhostPaint, which were the only things drawing into it.
   ⚠⚠ THIS BLOCK IS A MIRROR OF MY OWN COLUMN AND IT IS PARKED ON THE OTHER EDGE: same `.rg-st`
   boxes, same label-over-value, same width. That is what lets the two sides be compared at a
   glance. Its TOP is written inline (rgnHudPlace) from the rival's real portrait, because the
   portrait's height follows the art and the fold and cannot be written down here. */
/* ⚠⚠ ITS WIDTH IS `--rgfoe`, NOT `--rgside`. The playfield is centred in the room BETWEEN the two
   columns and that sum is written from --rgfoe, so a block that is actually --rgside wide (76 to
   the gutter's 60) sticks 16px further in than the arrows were told to expect — and the right-hand
   lane runs underneath the rival's figures. Measured: the field ended at 308 against a block
   starting at 293. A column and the space reserved for it are the same number or they are a bug. */
.rg-foeside{position:absolute; right:6px; width:calc(var(--rgfoe) - 6px); z-index:8;
  display:flex; flex-direction:column; gap:6px; pointer-events:none;}
.rg-foe-stats .rg-st b{color:#5cc8ff;}
.rg-foe-stats .rg-st.miss b{color:#ff5c5c;}

/* ---------- and the playfields move over to make room ---------- */
#ddiField,#ddiJudges,#ddiHp{left:calc(50% + var(--rgside)/2) !important; transform:translateX(-50%) !important;
  width:min(calc(100% - var(--rgside) - 12px), 300px) !important;}
#ithHwy,#ithFrets,#ithMeter{left:calc(50% + var(--rgside)/2) !important; transform:translateX(-50%) !important;
  width:min(calc(100% - var(--rgside) - 12px), 318px) !important;}
/* ⚠ AND SO DOES EVERYTHING THAT IS DRAWN OVER THE PLAYFIELD. The count-in, the combo call and the
   milestone burst all belong to the arrows, not to the screen — left on the bare 50% they read as
   sitting off to one side of the game they are commenting on. */
#scr-ddance .rg-count,#scr-inktar .rg-count,#ddiCombo,#scr-ddance .ddi-milestone{
  left:calc(50% + var(--rgside)/2) !important;}
/* ⚠⚠ AND ITS WIDTH HAD TO COME IN WITH IT. `.rg-count` is `width:min(90%,320px)` centred on its
   own left edge — perfectly safe on the bare 50% line, and 15px off the right of a folded screen
   once that line moves half a column across. Moving a centred box is never only a `left` change:
   whatever it is centred on has to fit in the space it was moved into. */
#scr-ddance .rg-count,#scr-inktar .rg-count{
  width:min(calc(100% - var(--rgside) - 16px), 320px) !important;}
/* ⭐ v0.734 — THE "LOADING RECORD…" PLACEHOLDER IS NOT A COUNT-IN DIGIT.
   It shares the count-in's element, and at the count-in's clamp(30px,11vw,58px) the single word
   RECORD… is wider than the 320px box on its own — so it overran on both rhythm games at 375px.
   It is dropped by JS the instant a real digit is written (see minigames.js), so nothing that is
   meant to be big is ever shrunk by this. */
#scr-ddance .rg-count.rg-load,#scr-inktar .rg-count.rg-load{
  font-size:clamp(11px,3.4vw,16px) !important; letter-spacing:2px;}
/* ⚠⚠⚠ AND THE BIG CENTRED "n COMBO!" CALL IS GONE FROM THE PLAYFIELD. It was pinned 56px down
   — which is exactly where the RECEPTORS now are — so it sat squarely on the four arrows you aim
   at, and the bigger the combo the more of the game it covered. The combo is a STAT and the stats
   live in the column (Odaroo: "the number of combos so far" on the side); the column's figure pops
   on every hit instead, and ddiMilestone still throws its full-screen burst every 20. */
#ddiCombo{display:none !important;}
/* ⚠ the two rhythm games' own `.gink-ddi` / `.gink-ith` corner rules pin a top and a left; inside
   the column those have to lose, or the portrait leaves the column it now lives in. */
.gink-ddi,.gink-ith{top:auto; left:auto;}
/* ⚠⚠ INK-TAR'S NECK NO LONGER STARTS BELOW A SECOND ONE. `.ith-vs #ithHwy{top:calc(21% + 126px)}`
   used to push it a fifth of the screen down to clear the rival's full-width highway. That board
   is gone from the playfield, so the neck opens at its solo height in a duel too — which is what
   "your neck never moves between solo and online" actually means. */
.ith-vs #ithHwy{top:64px;}
/* ⚠ THE ONE THING A DUEL STILL TAKES AWAY: the solo health bar and the star row. Not because the
   layout changed — it does not any more — but because the versus row at the top (rgnVsBar) IS your
   health in a duel, and two health bars for one player is a readout arguing with itself. The stars
   are a solo grading, and there is nothing to grade against in a race. ⚠ The INKLET is NOT hidden
   here: it lives in the column now, and it is the same portrait in both modes. */
.ddi-vs #ddiHp,.ddi-vs #ddiStarRow{display:none;}
.ith-vs #ithStarRow,.ith-vs #ithMeter{display:none;}
/* ⭐⭐⭐ v0.781 — A DUEL HAS TWO COLUMNS, SO THE PLAYFIELD IS CENTRED BETWEEN THEM.
   Solo has one column (mine, on the left) and the field is centred in the room to its right — that
   is `50% + --rgside/2` above, and it stays exactly as it was. Online there is a second, narrower
   column on the right carrying the rival's figures under the rival's inklet, so the middle of the
   free space moves back towards the centre by half the difference.
   ⚠⚠ THE RIVAL'S GUTTER IS DELIBERATELY NARROWER THAN MINE (`--rgfoe`). It holds three small
   figures and nothing else, while mine holds four plus my inklet — and every pixel not spent on it
   goes to the arrows, which are the main event (Odaroo: "just feature the actual gameplay footage
   of the user"). ⚠ It must still clear the rival's PORTRAIT, which is `inkPortW()` wide (69–78px)
   and hangs off the right edge: the gutter is narrower than that on purpose and the portrait is
   allowed to overhang it, which is why the playfield also starts BELOW the portrait (see next).
   ⚠⚠ AND THE FIELD STARTS UNDER THE WHOLE HUD, not under the reel alone. Solo's `top:56px` clears
   a bare name strip; a duel stacks reel + health bar + portrait, so the arrows begin below all of
   it or the receptors — the thing you are aiming at — are painted under the rival's face. */
#scr-ddance,#scr-inktar{--rgfoe:60px;}
.ddi-vs #ddiField,.ddi-vs #ddiJudges{
  left:calc(50% + (var(--rgside) - var(--rgfoe))/2) !important;
  width:min(calc(100% - var(--rgside) - var(--rgfoe) - 14px), 300px) !important;}
.ith-vs #ithHwy,.ith-vs #ithFrets{
  left:calc(50% + (var(--rgside) - var(--rgfoe))/2) !important;
  width:min(calc(100% - var(--rgside) - var(--rgfoe) - 14px), 318px) !important;}
/* ⚠ id+class BEATS the plain `#scr-ddance .rg-count` rule further up; a bare `.ddi-vs .rg-count`
   would lose to it and the count-in would stay on the solo centre line while the arrows moved. */
#scr-ddance .ddi-vs .rg-count,#scr-inktar .ith-vs .rg-count,#scr-ddance .ddi-vs .ddi-milestone{
  left:calc(50% + (var(--rgside) - var(--rgfoe))/2) !important;
  width:min(calc(100% - var(--rgside) - var(--rgfoe) - 18px), 320px) !important;}
.ddi-vs #ddiField{top:84px;}
.ith-vs #ithHwy{top:88px;}

@media (min-width:640px){
  .rghp{height:calc(14px * var(--gs,1));}
  #scr-ddance,#scr-inktar{--rgfoe:104px;}
  .ddi-vs #ddiField,.ddi-vs #ddiJudges{width:min(calc(100% - var(--rgside) - var(--rgfoe) - 14px), 380px) !important;}
  .ith-vs #ithHwy,.ith-vs #ithFrets{width:min(calc(100% - var(--rgside) - var(--rgfoe) - 14px), 400px) !important;}
  /* ⭐ v0.733 — a wide screen can afford a wider column, and the playfield still keeps its full
     318px because the room genuinely exists there (906 − 132 − 12 = 762). Same ceiling rule
     Tetr-Ink's own side column follows: raising this only spends space that is really free. */
  #scr-ddance,#scr-inktar{--rgside:132px;}
  /* ⭐ the unfolded screen has 760px of room and the folded caps were spending 300 of it. The
     lanes grow rather than the sides staying empty — rule 4 of the pre-flight. */
  #ddiField,#ddiJudges,#ddiHp{width:min(calc(100% - var(--rgside) - 12px), 380px) !important;}
  #ithHwy,#ithFrets,#ithMeter{width:min(calc(100% - var(--rgside) - 12px), 400px) !important;}
  .rg-side{top:80px;}
  .rg-side .gink{max-width:74px;}
  .rg-st span{font-size:9px;}
  .rg-st b{font-size:17px;}
}

/* ============================================================================
   ⭐⭐⭐ v0.844 — INK-TAR HERO TAKES THE WHOLE SCREEN BACK.

   Odaroo, 2026-08-22, on the folded phone: "look at the narrow screen, it looks very weird…
   feel free to remove the d-pad and actually make the buttons as large as on the wide screen,
   even on the actual narrow screen."

   ⭐⭐⭐ THE D-PAD IS GONE ON THIS SCREEN (padWanted, js/ui.js) AND THAT IS WHAT THIS BLOCK IS
   SPENDING. The cross was reserving ~250px at the foot of a 657px screen for a control that does
   nothing the five frets do not already do — so the neck was a sliver, the frets were squeezed
   under it, and the whole instrument read as an afterthought. With the pad hidden `--padclear`
   drops to 8px, and EVERY vertical anchor here is written from it rather than from a flat pixel
   number, so this layout follows the controls instead of guessing where they are.
   ⚠⚠ THIS IS WHY THE `bottom:352px` / `bottom:250px` / `bottom:234px` RULES ABOVE ARE DEAD.
   Same specificity, later rule wins. Do not "fix" a spacing problem by editing one of those — it
   will read as a change that did nothing. The three anchors below are the live ones, and they are
   ONE stack: frets sit on --padclear, the meter sits on the frets, the neck sits on the meter.
   Change the fret height and all three move together, because all three are written from --ithfh.

   ⭐⭐ AND THE NECK IS AS WIDE AS THE SCREEN ALLOWS. The old cap was a flat 318px, which on the
   416px folded Fold 8 left ~80px of empty stage and five narrow buttons. The side column is
   trimmed to what it actually holds (a 56px inklet and four small figures) and the cap is gone on
   the narrow fold, so the instrument grows to fill whatever is left.
   ⚠⚠ THE NECK AND THE FRETS ARE ONE WIDTH AND ONE LEFT EDGE, ALWAYS. A note falls down the lane
   that sits over its button; widen one of them alone and every note lands beside the fret it
   belongs to. The meter rides with them for the same reason it always has (v0.453).
   ============================================================================ */
#scr-inktar{ --ithfh: clamp(96px, 15vh, 120px); --rgside: 60px; }
#ithFrets{ bottom: calc(var(--padclear, 8px) + 10px) !important; gap:4px; }
#ithMeter{ bottom: calc(var(--padclear, 8px) + 10px + var(--ithfh) + 12px) !important; }
#ithHwy  { bottom: calc(var(--padclear, 8px) + 10px + var(--ithfh) + 26px) !important; top:58px; }
/* ⚠⚠ SOLO ONLY. A duel reserves a second gutter on the right for the rival's figures (`--rgfoe`,
   the `.ith-vs` rules above) and those are still the right numbers online — a bare `#ithHwy` rule
   down here would outrank them by source order and run the right-hand lane under the rival's
   readout, which is the exact fault the v0.781 note above describes. `:not(.ith-vs)` is also what
   keeps the two modes honest: the ONLINE neck is untouched by this pass. */
#ithStage:not(.ith-vs) #ithHwy,
#ithStage:not(.ith-vs) #ithFrets,
#ithStage:not(.ith-vs) #ithMeter{ width: calc(100% - var(--rgside) - 10px) !important; }
#ithFrets{ gap:5px; }
.ith-fret{ height: var(--ithfh); border-radius: 34px 34px 8px 8px; }
/* the column only ever holds a portrait and four small figures — it does not need 86px, and every
   pixel it gives back is a wider button. ⚠ It must still clear the frets it now sits above. */
#scr-inktar .rg-side{ bottom: calc(var(--padclear, 8px) + 22px + var(--ithfh)); }
#scr-inktar .rg-side .gink{ max-width:56px; }

/* ⭐⭐⭐ THE WHAMMY BAR, DRAWN. (Odaroo: "a note also should be like a weird effect as you're
   sustaining it and wiggling it directly.") `--wham` is 0…1, written per fret by ithWhamSkin.
   ⚠⚠ IT IS THE INDEPENDENT `rotate` / `translate` PROPERTIES, NEVER `transform` — `.ith-fret.press`
   already owns transform and `animation:ithFire` owns the filter on a held fret. A `transform`
   here would REPLACE the press dip, which is the shorthand trap this project has hit six times.
   ⚠ And the wobble is a rotation of a box that is already there, not a new layer: a keyframed
   `left`/`top` on a fret would re-lay the page out every frame (the v0.834 rule). */
.ith-fret.wham{ animation: ithWham calc(0.19s - 0.07s * var(--wham,0)) linear infinite; }
@keyframes ithWham{
  0%  { rotate: calc(-3.4deg * var(--wham,0)); translate: 0 calc(-2px * var(--wham,0)); }
  50% { rotate: calc( 3.4deg * var(--wham,0)); translate: 0 calc( 2px * var(--wham,0)); }
  100%{ rotate: calc(-3.4deg * var(--wham,0)); translate: 0 calc(-2px * var(--wham,0)); }
}
.ith-lane.wham .ith-note.hold.fire{ animation: ithWhamNote .17s linear infinite, ithFire .18s linear infinite; }
@keyframes ithWhamNote{ 0%{ translate:-3px 0 } 50%{ translate:3px 0 } 100%{ translate:-3px 0 } }
.ith-lane.wham::after{ opacity:.42; }
/* a lip along the top of each fret so a big button still reads as a button and not a slab */
.ith-fret-lip{ display:block; height:9px; margin:7px 12px 0; border-radius:6px;
  background:linear-gradient(#ffffffaa,#ffffff22); pointer-events:none;}

@media (min-width:640px){
  /* a wide screen can afford the column it always had; the cap comes back so the neck stays an
     instrument rather than becoming a table. ⚠ Same width on the frets, or see above. */
  #scr-inktar{ --rgside:132px; }
  #ithStage:not(.ith-vs) #ithHwy,
  #ithStage:not(.ith-vs) #ithFrets,
  #ithStage:not(.ith-vs) #ithMeter{ width: min(calc(100% - var(--rgside) - 12px), 400px) !important; }
}
/* ⭐⭐⭐ v0.844 — THE PAUSE CHIP. With the cross gone this is the only touch route to the pause
   menu (HOW TO PLAY, QUIT) on this screen, so it may never be decorative and it may never be
   covered. ⚠ It sits in the strip ABOVE the neck, on the row under the star row, so it cannot
   take a tap meant for a lane — and it is deliberately outside `#ithHwy` for the same reason. */
.ith-pause{position:absolute; top:28px; right:12px; z-index:10; cursor:pointer;
  font-family:var(--fontD); font-size:11px; letter-spacing:2px; line-height:1;
  color:#f2ead6; background:#0a0a14cc; border:2px solid #ffffff33; border-radius:6px;
  padding:6px 9px; min-width:34px; min-height:26px;}
.ith-pause:active{background:#f4c5422e; border-color:#f4c542; color:#f4c542;}
