/* ============================================================
   INKVILLE — games.css
   Superstars, Bullet Ballet, Lumen Ink, the carts gallery and INKAMP

   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.
   ============================================================ */
/* ===== SUPERSTARS — the 3-stage Megaton-Punch timing game (v3.12) ===== */
#scr-super{background:radial-gradient(120% 90% at 50% 10%,#2a1c48,#181030 55%,#0c0818);}   /* was display:flex — that overrode .screen{display:none} and leaked this dark bg over the HOME menu */
#superStage{flex:1; min-height:0; position:relative; overflow:hidden; font-family:var(--fontD);}
/* ===== COSMIC SKY — a starfield that escalates to hyperspace with your combo ===== */
#superSky{position:absolute; inset:0; z-index:0; overflow:hidden; pointer-events:none;
  --spd:14s; --hue:0deg; --sat:1; --bri:1;
  background:radial-gradient(120% 90% at 50% 8%, #2a1c48, #181030 55%, #0a0716);
  filter:hue-rotate(var(--hue)) saturate(var(--sat)) brightness(var(--bri)); transition:filter .5s ease;}
#superSky::before,#superSky::after{content:''; position:absolute; left:-50%; right:-50%; top:-60%; height:220%;
  background-image:
    radial-gradient(1.5px 1.5px at 12% 22%, #fff, transparent),
    radial-gradient(1.5px 1.5px at 68% 12%, #cfe6ff, transparent),
    radial-gradient(2px 2px at 84% 44%, #fff, transparent),
    radial-gradient(1.5px 1.5px at 34% 66%, #ffd9f2, transparent),
    radial-gradient(1px 1px at 52% 88%, #fff, transparent),
    radial-gradient(2px 2px at 22% 92%, #d8fff0, transparent),
    radial-gradient(1.5px 1.5px at 92% 74%, #fff, transparent);
  background-size:60% 50%; animation:supStars var(--spd) linear infinite;}
#superSky::after{background-size:40% 34%; opacity:.6; animation-duration:calc(var(--spd) * 1.7);}
@keyframes supStars{from{transform:translateY(-25%);} to{transform:translateY(0%);}}
/* combo levels — quicker drift, hotter hue as it climbs */
#superSky.lv0{--spd:16s;} #superSky.lv1{--spd:11s; --hue:20deg;} #superSky.lv2{--spd:7.5s; --hue:60deg; --sat:1.1;}
#superSky.lv3{--spd:4.5s; --hue:130deg; --sat:1.2; --bri:1.1;} #superSky.lv4{--spd:2.6s; --hue:220deg; --sat:1.3; --bri:1.15;}
#superSky.lv5{--spd:1.5s; --hue:300deg; --sat:1.5; --bri:1.25;}
/* hyperspace streaks kick in at combo ≥ 3 */
.sky-streaks{position:absolute; inset:0; opacity:0; transition:opacity .4s;
  background:repeating-conic-gradient(from 0deg at 50% 42%, transparent 0deg 4deg, #ffffff22 4deg 5deg);
  -webkit-mask:radial-gradient(circle at 50% 42%, transparent 8%, #000 60%); mask:radial-gradient(circle at 50% 42%, transparent 8%, #000 60%);}
#superSky.warp .sky-streaks{opacity:.55; animation:supWarp .8s linear infinite;}
#superSky.lv5 .sky-streaks{opacity:.8;}
@keyframes supWarp{from{transform:scale(1) rotate(0deg);} to{transform:scale(1.6) rotate(6deg);}}
#supTower,#supGaugeWrap,#supBtn{position:relative; z-index:2;}   /* flow content rides above the sky */
.gink-sup{top:6px; left:6px; width:80px;}
/* ===== BULLET BALLET — the beam charge bar over your inkling + the MEGA beam ===== */   /* v3.48 — no bar, corner seat */
#hbeamBar{position:absolute; top:150px; left:14px; width:80px; height:8px; z-index:9; background:#0a0a14; border:2px solid #1b1510; border-radius:4px; overflow:hidden; box-shadow:1px 2px 0 #0006;}   /* the beam bar sits WELL below the framed inkling — clear of tall hats, low-hanging gear and stacked frames */
#hbStageTR{position:absolute; top:44px; right:10px; z-index:9; font-family:var(--fontD); font-size:10px; letter-spacing:1px; color:#fdf6e3; text-align:right; text-shadow:1px 1px 0 #1b1510;}
#hbStageTR b{font-size:16px; color:#f4c542; margin-left:4px; text-shadow:1px 1px 0 #1b1510, 0 0 8px #f4c54288;}
#hbStageTR .hb-hearts{display:block; margin-top:5px;}
#hbStageTR .hb-hearts svg.hpi{width:20px; height:18px; margin-left:3px;}   /* BIG obvious hearts */
/* THE HEADLINER'S HP — a wide multi-coloured bar that visibly drains, like the rink clock */
#hbossHp{position:absolute; top:6px; left:96px; right:10px; max-width:420px; margin:0 auto; z-index:12; pointer-events:none; text-align:center;}   /* clear of the framed inkling on the left and sitting just above the STAGE tag */
#hbossHp .bhp-nm{font-family:var(--fontD); font-size:9px; letter-spacing:1px; color:#ffd66b; text-shadow:1px 1px 0 #1b1510; margin-bottom:3px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
#hbossHp .bhp-track{height:20px; background:#0a0a14; border:3px solid #1b1510; border-radius:7px; overflow:hidden; box-shadow:0 2px 0 #0007, inset 0 0 0 1px #ffffff22;}
#hbossHp .bhp-fill{display:block; height:100%; width:100%; border-radius:4px; transform-origin:left center; transition:transform .22s linear;
  background:linear-gradient(90deg,#7ee081,#f4c542 28%,#ff9a3c 55%,#ff5f6d 78%,#b06af0 100%); background-size:260px 100%;
  box-shadow:0 0 10px #ff6ba888; animation:bhpShine 1.6s linear infinite;}
@keyframes bhpShine{0%{filter:brightness(1)}50%{filter:brightness(1.35)}100%{filter:brightness(1)}}
#hbossHp.low .bhp-track{border-color:#c93a22;} #hbossHp.low .bhp-fill{animation:bhpShine .5s linear infinite;}
@media (max-width:420px){ #hbossHp .bhp-track{height:16px;} #hbossHp .bhp-nm{font-size:8px; margin-bottom:2px;} }
.hring-pop{position:absolute; z-index:40; transform:translate(-50%,-50%); font-family:var(--fontD); font-size:20px; color:#f4c542; letter-spacing:2px; pointer-events:none;
  text-shadow:2px 2px 0 #1b1510, 0 0 14px #f4c542; animation:hringPop .9s ease-out forwards;}
@keyframes hringPop{0%{opacity:0; transform:translate(-50%,-50%) scale(.4);}18%{opacity:1; transform:translate(-50%,-60%) scale(1.35);}45%{transform:translate(-50%,-80%) scale(1);}100%{opacity:0; transform:translate(-50%,-130%) scale(1);}}
#hbeamFill{display:block; height:100%; width:0%; background:linear-gradient(90deg,#ff6ba8,#f4c542); transition:width .18s;}
#hbeamBar.full{animation:hbeamPulse .7s ease-in-out infinite;}
@keyframes hbeamPulse{0%,100%{box-shadow:1px 2px 0 #0006}50%{box-shadow:0 0 12px #f4c542, 1px 2px 0 #0006}}
@media (max-height:680px){ #hbeamBar{width:64px; top:28px;} }
.hbeam.mega{width:74px; margin-left:-37px; background:linear-gradient(90deg,transparent,#fff8 18%,#fff 50%,#fff8 82%,transparent); box-shadow:0 0 26px 10px #ff6ba8cc, 0 0 60px 26px #f4c54277; animation:megaBeam .12s steps(2) infinite;}
@keyframes megaBeam{0%{filter:hue-rotate(0deg) brightness(1.15)}100%{filter:hue-rotate(80deg) brightness(1.5)}}
/* ⚠ v0.463 — BULLET BALLET'S SKY STARTS BELOW THE PLAYER HUD. Its stage runs edge to edge from the
   very top, so the new name-and-level label in the corner was sitting ON the bullet field — the
   pre-flight's HUD-overlap rule caught it (98x23 shared). Every other game already insets its
   playfield below the corner; this gives ballet the same clearance. The sky is a backdrop, so
   losing a strip off the top costs the player nothing. */
/* ⚠⚠⚠ v0.578 — AND THE CLEARANCE IS DERIVED FROM THE LABEL, NOT TYPED AS 30px.
   The 30 above was measured against the name label as it stood in v0.463. v0.578 raised --nmsz to
   17/21px (Odaroo: "go big with the username"), the label grew with it, and the pre-flight caught
   this screen again at 906 — "BULLET BALLET playfield is drawn under the top HUD, they share 141x5
   pixels". A constant that was correct when it was written is exactly the thing that goes quietly
   wrong when the number it was measured against moves, and this is the second time this screen has
   been the one to notice.
   So it now tracks the name size directly: whatever a username is, the sky starts below it. The
   1.6 covers the label's line box plus its padding and leader, and the 12px is the corner offset
   with a little air. Re-raising --nmsz can no longer put the HUD back on the bullet field.
   ⚠ The sky is a backdrop, so a slightly deeper inset costs the player nothing — that was already
   the reasoning for the original 30 and it still holds. */
/* the X button exists ONLY when the beam is charged */   /* glow only — no transform, so the button holds dead still */
/* big pixel stat icons — no font glyphs, no emoji */
.sup-stats{line-height:1.5;}   /* position now comes from .gbar-stats (below the bar, top-right) */
.sup-stats .rg-hud-nm{font-size:9px; color:#f4c542; letter-spacing:1px; text-shadow:0 0 8px #f4c54288;}
.sup-stats .lab{font-size:7px; opacity:.65; letter-spacing:1px;} .sup-stats b{font-size:14px; color:#f4c542;}
/* v3.45 — no banner: logo under the top-left inkling, STAGE 1-5 top-right, tower hidden until the finale */
#supLogo{position:absolute; top:150px; left:8px; z-index:8; font-family:var(--fontD); font-size:12px; letter-spacing:1px; color:#f4c542; text-shadow:2px 2px 0 #1b1018, 0 0 10px #f4c54266; line-height:1.4;}   /* v3.54 — English only, sitting clear BELOW the inkling, its frame and its gear */
#supStageTR,#ipStageTR{position:absolute; top:44px; right:10px; z-index:7; font-family:var(--fontD); font-size:10px; letter-spacing:1px; color:#cbb8d8; text-align:right;}
#supStageTR b,#ipStageTR b{font-size:16px; color:#f4c542; margin-left:4px; text-shadow:0 0 8px #f4c54288;}
#scr-inkput .gink-game{top:6px !important; left:6px !important;}   /* v3.45 — no banner to clear anymore: the inkling hugs the corner */
.sup-comet{position:absolute; left:-90px; top:var(--cy,30%); width:44px; height:48px; opacity:.85; z-index:1; pointer-events:none;
  filter:drop-shadow(-6px 4px 0 #f4c54233) drop-shadow(-12px 8px 0 #f4c54218);   /* the pixel comet tail */
  animation:supComet var(--cd,9s) linear infinite;}
.sup-comet svg{width:100%; height:100%;}
@keyframes supComet{0%{transform:translate(0,0) rotate(-14deg);}100%{transform:translate(130vw,-38vh) rotate(-14deg);}}
#supTower{position:absolute; right:auto; left:50%; transform:translateX(-50%); top:82px; bottom:300px; width:46px; z-index:5;}   /* the reveal lands CENTRE stage — clear of everything on both folds */
#supTower.reveal{animation:supTowerIn .4s steps(5) both;}
@keyframes supTowerIn{0%{opacity:0; transform:translateX(-50%) translateY(24px);}100%{opacity:1; transform:translateX(-50%);}}
#supBell{position:absolute; top:-8px; left:50%; transform:translateX(-50%); width:24px; height:24px;}
#supBell svg{width:100%; height:100%; display:block; image-rendering:pixelated;}
#supBell.rung{animation:supBellRing .4s ease-in-out 3;}
@keyframes supBellRing{50%{transform:translateX(-50%) scale(1.3) rotate(-10deg)}}
#supTowerBar{position:absolute; left:50%; transform:translateX(-50%); top:26px; bottom:6px; width:16px; background:#00000066; border:2px solid #ffffff33; overflow:hidden;}
#supTowerFill{position:absolute; left:0; right:0; bottom:0; height:0%; transition:height 1.3s steps(24,end);   /* the climb goes up in chunky PIXEL steps, not a smooth pour */
  background:repeating-linear-gradient(0deg,#0000 0 1px,#0003 1px 2px, #0000 2px 6px), linear-gradient(0deg,#c93a22,#f4c542 60%,#6bbf59);}
#supPuck{position:absolute; left:50%; transform:translateX(-50%); bottom:0; width:20px; height:8px; background:#e86a9a; border:2px solid #1b1018; z-index:6; box-shadow:0 0 8px #e86a9a; image-rendering:pixelated;}   /* a chunky pixel slab, not an oval */
#supPuck.launch{transition:bottom 1.3s steps(24,end);}
#supTower .sup-mark{position:absolute; right:-4px; transform:translateX(100%); font-size:9px; color:#cbb8d8; white-space:nowrap;}
#supGaugeWrap{position:absolute; left:50%; transform:translateX(-50%); bottom:300px; width:min(88%,320px); z-index:6; text-align:center;}
/* ⚠ v0.441 — THE CUE CARRIES THE STAGE NAME, and some of them are long (MEGATON PUNCH,
   SAMURAI INK). At a fixed 18px with two pixels of letter-spacing that runs off a narrow
   phone. Found by the headless harness on its first run — the Browser pane never caught it,
   because the pane does not emulate a phone the way a real device reports itself. It now
   scales with the screen and is allowed to wrap rather than walk off the edge. */
#supCue{font-family:var(--fontD); font-size:clamp(13px,4.6vw,18px); color:#f4c542; letter-spacing:2px;
  min-height:22px; margin:0 auto; line-height:1.15; overflow-wrap:anywhere;
  /* ⚠ THE POP SCALES THIS 1.3x, and getBoundingClientRect is right to count it: at 1.3x a
     300px box really is 390px on a 375px screen and really does hang over both edges for
     the length of the animation. The cap is therefore set so the SCALED width still fits —
     260 x 1.3 = 338, inside 375 with room either side. Cap the rest state for the pop, not
     for the rest state. */
  max-width:min(260px, calc(100% - 20px));
  text-shadow:0 0 10px #f4c54288; transition:transform .08s;}
#supCue.pop{transform:scale(1.3);}
#supGauge{position:relative; height:34px; margin:6px 0; background:#0c0818; border:2px solid #ffffff33; border-radius:6px; overflow:hidden;}
.sup-fill{position:absolute; left:0; top:0; bottom:0; width:0%; background:#f4c542;}
.sup-mkr{position:absolute; top:2px; bottom:2px; width:10px; margin-left:-5px; background:#5fd0c8; border:1px solid #0c0818; box-shadow:0 0 8px #5fd0c8;}
#supMkrB{background:#e86a9a; box-shadow:0 0 8px #e86a9a;}
.sup-target{position:absolute; left:calc(50% - 14px); top:0; bottom:0; width:28px; background:#6bbf5933; border-left:2px solid #6bbf59; border-right:2px solid #6bbf59; text-align:center; color:#eaffea; font-size:16px; line-height:30px;}
#supGauge.strike{background:#173a1d; border-color:#6bbf59; box-shadow:0 0 18px #6bbf59, inset 0 0 12px #6bbf5966;}   /* SAMURAI INK — the flash to strike on */
.qsup-gauge{position:relative; height:34px; width:min(90%,300px); margin:10px auto; background:#0c0818; border:2px solid #ffffff44; border-radius:6px; overflow:hidden;}   /* the SUPERSTARS showstopper gauge, centred in the no-box #qte */
#supHint{font-size:8px; color:#cbb8d8; letter-spacing:1px; margin-top:4px;}
/* ===== v3.54 — the new SUPERSTARS acts ===== */
#supSq{display:none; position:relative; height:118px; margin:6px 0;}   /* SQUARE DANCE — a frame to close on */
#supSq .sq-t{position:absolute; left:50%; top:50%; width:44px; height:44px; transform:translate(-50%,-50%); border:3px dashed #f4c542; box-shadow:0 0 10px #f4c54244; image-rendering:pixelated;}
#supSq .sq-m{position:absolute; left:50%; top:50%; width:44px; height:44px; transform:translate(-50%,-50%) scale(2.4); border:3px solid #7fd4ff; box-shadow:0 0 10px #7fd4ff55;}
.sup-target.suptring{left:calc(50% - 14px); width:28px; height:28px; top:50%; bottom:auto; transform:translateY(-50%); border:3px solid #f4c542; border-radius:50%; background:#f4c54222; box-shadow:0 0 12px #f4c54266;}   /* the RING dead centre — the watch's home */
.sup-mkr.watchface{width:20px; height:20px; top:50%; bottom:auto; transform:translateY(-50%); margin-left:-10px; border-radius:50%;   /* the POCKET WATCH itself swings by */
  background:radial-gradient(circle at 34% 30%, #ffe58a, #f4c542 55%, #d9a232); border:2px solid #1b1018; box-shadow:0 0 10px #f4c54288;}
.sup-mkr.watchface::after{content:''; position:absolute; left:50%; top:-6px; width:4px; height:5px; transform:translateX(-50%); background:#d9a232; border:1px solid #1b1018;}   /* the crown + chain stub */
#supBtn{position:absolute; left:50%; transform:translateX(-50%); bottom:250px; z-index:7; max-width:200px;}
.sup-start .ss-jp{color:#f4c542;} .sup-start .ss-en{color:#fff;}
/* v3.96 — the opening FIVE ACTS bar drops BELOW the HUD row: the SUPERSTARS wordmark and STAGE 1-5 sit
   clean above it, so the logo never lands on the flags or the promise line behind it. */
.ssOverlay.bsOv.sup-start{justify-content:flex-start; padding-top:84px;}
.sup-start .rg-flag{top:80px;} .sup-start .rg-flag.l{left:96px;}   /* the left flag clears the framed inkling */
#scr-super .gtitle{top:calc(7px + env(safe-area-inset-top,0px));}   /* the wordmark rides slightly higher than the other games — it shares its corner with the opening bar */

@media (max-height:720px){ #supTower{bottom:290px;} #supGaugeWrap{bottom:290px;} #supBtn{bottom:244px;} .gink-sup{width:66px;} }
/* ===== LUMEN INK — Lumines-style timeline block puzzle (v3.22) ===== */
#scr-lumen{background:radial-gradient(120% 90% at 50% 12%,#1a1030,#0c0818);}
/* ⚠ v0.431 — THE PADDING HERE IS A FALLBACK ONLY. lumSize() writes the whole shorthand every pass:
   the bottom is the pad's real top edge less the bite the board is allowed to take out of it, and
   the LEFT (or the TOP, in row-rail mode) is the inkling + NEXT column. Do not tune these numbers —
   fix lumPadRoom() / LUM_RAIL_W / LUM_RAIL_H. */
#lumWrap{flex:1; min-height:0; display:flex; align-items:center; justify-content:center; padding:8px 6px 242px 86px; position:relative; z-index:5;}
/* ⚠⚠ v0.815 — NO `max-width` AND NO `aspect-ratio` ONCE JS HAS MEASURED. The old `max-width:96%`
   silently clamped the board to 96% of the wrap, so every pixel lumSize() won back on the narrow
   screen was given away again on the way to the screen. The square-cell fit IS the constraint. */
#lumField{position:relative; height:100%; aspect-ratio:1/1; max-width:100%; background:#05030a; border:2px solid #ffffff33; box-shadow:0 0 22px #000a, inset 0 0 30px #0008;}
#lumGrid{position:absolute; inset:2px; z-index:2;}   /* ⭐ v3.56 — no CSS grid: it was HARDCODED 10×13 while the game plays 12×12, scattering every landed block into the wrong cell (the "completely broken" bug). Cells are now placed by the SAME percentage math as the falling block — pixel-identical, any board size. */
#lumBlock{z-index:3;}
.lum-cell{position:absolute; border-radius:0; image-rendering:pixelated;}
.lum-cell.fill{animation:lumBreathe 3.1s ease-in-out infinite;}   /* v3.58 — the blocks BREATHE, each on its own beat (delay set per cell) */
@keyframes lumBreathe{50%{filter:brightness(1.09);}}   /* v3.98 — brightness ONLY: the old scale made every block twitch a different size as it fell */
#lumBlock .lb{transition:left .07s linear, top .07s linear;}   /* v3.58 — the falling block GLIDES between cells instead of teleporting */
/* ===== ⭐ v3.98 — ONE BLOCK, EVERY LEVEL. A flat pixel slab with a hard 3px bevel: light on the top-left,
   shadow on the bottom-right, a solid ink keyline round the outside. No gradients, no soft blur, no cuts. ===== */
.ds-blk{border-radius:0; image-rendering:pixelated;
  box-shadow:inset 3px 3px 0 #ffffff4d, inset -3px -3px 0 #00000059, inset 0 0 0 1px #12081acc;}
.lum-cell.fill.lum-mk{animation:lumMk .45s ease-in-out infinite; outline:1.5px solid #fff; outline-offset:-1px; z-index:2;}
@keyframes lumMk{50%{filter:brightness(1.5) saturate(1.3)}}
#lumBlock{position:absolute; inset:2px; pointer-events:none; z-index:3;}
#lumBlock .lb{position:absolute; border-radius:0; image-rendering:pixelated; box-shadow:inset 3px 3px 0 #ffffff59, inset -3px -3px 0 #00000059, inset 0 0 0 1px #12081acc;}
/* ⚠⚠ v0.818 — #lumViz (the sixteen spectrum bars) IS DELETED, with #lumSky and #lumBeatMarks.
   Odaroo: "completely remove what's on the background — the lines could be confusing." All three
   drew vertical lines or bands the full height of the field, which is the shape of the SWEEP BAR,
   the one line in this game that means anything. Nothing goes behind this board again. */
#lumLine{position:absolute; top:2px; bottom:2px; width:3px; margin-left:-1px; background:linear-gradient(#fff,#f4c542,#fff); box-shadow:0 0 10px #fff, 0 0 22px #f4c542; z-index:5;}
/* v3.87 — the line PULSES on every beat it crosses: the sweep IS the tempo */
#lumLine.lum-beat{animation:lumBeatPulse .22s ease-out;}
/* ⭐⭐⭐ v0.818 — THE SHOWSTOPPER'S LUMEN INK, WEARING THE SAME CLOTHES. (Odaroo, 2026-08-14: the
   minis must be "a beautiful copy and paste from the actual game, just in a smaller screen.")
   These two are aliases of `#lumBlock .lb` and `#lumLine` directly above, declared HERE, beside
   what they copy, so a change to the real game's look is one edit and the mini cannot silently
   drift into being a different game with the same name.
   ⚠⚠ THE MINI CANNOT REUSE THOSE IDS. `scr-lumen` is still in the document while a showstopper
   runs — it is merely not the current screen — so a second `#lumLine` would make the page's ids
   ambiguous and `getElementById` would answer with whichever the parser met first. Classes are
   the only safe way to share a look between a screen and a mini. `.lum-cell` needed no alias: it
   was already a class, which is exactly why the settled gems were the one part of this mini that
   could be borrowed outright.
   ⚠ `.lm-line` sets top/left only — the mini gives it an explicit pixel height because its well is
   sized in cells, not stretched to a screen, so `bottom:2px` has nothing to resolve against. */
.lm-lb{position:absolute; border-radius:0; image-rendering:pixelated; box-shadow:inset 3px 3px 0 #ffffff59, inset -3px -3px 0 #00000059, inset 0 0 0 1px #12081acc; transition:left .07s linear, top .07s linear; z-index:3;}
.lm-line{position:absolute; top:0; width:3px; margin-left:-1px; background:linear-gradient(#fff,#f4c542,#fff); box-shadow:0 0 10px #fff, 0 0 22px #f4c542; z-index:5; pointer-events:none;}
/* ⚠⚠ v0.834 — the beat pulse fattens the bar with `scale`, not with `width`+`margin-left`. It
   fires on EVERY BEAT of every Lumen track, so on `width` it was a per-frame page layout for
   .22s several times a second, all run long. 7px/3px = 2.33x about the bar's own centre, which is
   what the old `margin-left:-3px` was compensating for by hand. The glow scales with it, so the
   box-shadow keyframe is gone rather than compounding. */
@keyframes lumBeatPulse{0%{scale:2.33 1;}100%{scale:1 1;}}
/* ⭐⭐⭐ v0.807 — THE FOUR BEATS, MARKED ON THE FIELD. (Odaroo: "put little marks and make sure that
   there are four beats at every bar… we're able to play in synchronisation with the song.")
   The field is exactly one bar wide, so a beat is a quarter of it: five dashed ticks (the two BAR
   lines at the edges brighter than the three beat lines) and four numbered pips along the top, of
   which `lumLoop` lights whichever beat the sweep is inside. */
/* ⚠⚠ v0.818 — DELETED HERE TOO: #lumBeatMarks and .lbm-bar (the dashed vertical beat ticks). See
   lumBeatMarks() in js/minigames.js — the function is kept only to CLEAR them. */
/* ⚠⚠ v0.815 — DELETED HERE: #lumBeatDots (the numbered 1 2 3 4 pips over the board),
   #lumSongLbl (the song title under it) and #lumProg (the six track pips). The playfield is
   seamless — nothing is printed on it and nothing hangs off it. See lumBeatMarks(). */
/* ⭐ v0.815 — THE COMBO MULTIPLIER, said over the middle of the field and gone in half a second.
   ⚠ pointer-events:none — it sits over the board the player is aiming at. */
#lumMult{position:absolute; left:50%; top:42%; transform:translate(-50%,-50%); z-index:8; pointer-events:none;
  font-family:var(--fontD); font-size:clamp(30px,9vw,58px); letter-spacing:2px; color:#fff; opacity:0;
  text-shadow:0 0 12px #f4c542, 0 0 26px #f4c542aa, 3px 3px 0 #1b1018;}
#lumMult.on{animation:lumMultPop .62s ease-out;}
@keyframes lumMultPop{0%{opacity:0; transform:translate(-50%,-50%) scale(.5)}18%{opacity:1; transform:translate(-50%,-50%) scale(1.18)}40%{transform:translate(-50%,-50%) scale(1)}100%{opacity:0; transform:translate(-50%,-62%) scale(1)}}
/* ⭐⭐⭐ v0.829 — THE SIDE COLUMNS COST NOTHING BUT THE FACE. (Odaroo: "minimise the boxes on the
   left and on the right and just maximise the whole playing field.")
   ⚠⚠⚠ THE PORTRAIT'S WIDTH IS NOT NEGOTIABLE AND IT SETS THE RAIL. `vsNamePlace` writes
   `inkPortW()` (--inkav, 69-78px) onto BOTH portraits inline with !important on every fold, for
   every VS game at once — so a narrower rail is not available and a `width` on `.gink-lum` is a
   rule that loses online and wins solo, i.e. an inklet that changes size when a match starts (the
   exact fault the v0.463 note in js/online.js is about).
   So the rail is the FACE's width, and everything else is fitted INSIDE it: the NEXT chip tucks
   UNDER the face at 22px and the two lines of figures sit BESIDE the chip. Nothing sticks out past
   the portrait, which is what takes the right-hand column from 94px to the face's own ~75. */
/* v3.49 — no bar to clear. ⚠⚠ v0.829 — THE WIDTH IS `--inkav`, THE HOUSE'S OWN PORTRAIT SIZE, and
   it is NOT `!important`: online, vsNamePlace writes exactly that number inline with !important on
   both portraits, so this is the same answer given twice and the two modes cannot disagree.
   ⚠ Removing the width altogether was tried and it is a full-width `<div>` — the inklet then
   measured 205px across and overlapped the board on every fold. */
.gink-lum{top:6px !important; left:6px !important; width:calc(var(--inkav,70px) * 0.55);}   /* ⚠ 0.55 is LUM_PORT_SCALE in js/minigames.js — the two must move together */
#lumSideBox{position:absolute; top:6px; left:6px; width:22px; z-index:7; background:#0d0b14; border:2px solid #ffffff44; box-shadow:2px 2px 0 #0007;}   /* the tiny NEXT chip */
/* ⭐⭐⭐ the two lines: best chain, then cleared/sent. No labels — a label costs a line of its own
   and these two figures are the only ones on the screen, so there is nothing to tell them from.
   ⚠ THE SIZE IS FIXED, NOT `cqw`. The v0.818 container-query sizing existed because `.trun-r b`
   inherits a 19px rule from a `min-width:640px` breakpoint in css/tetrink.css — these boxes borrow
   nothing from Tetr-Ink any more, so the figure can simply be stated. ⚠ Sized against the longest
   value it can reach: `9999/9999` is 9 characters, ~5.4px each at 9px in this face. */
#lumStatBox{position:absolute; left:32px; z-index:7; text-align:left; font-family:var(--fontD); line-height:1.2; text-shadow:1px 1px 0 #000, 0 0 6px #000;}
#lumFoeStat{position:absolute; right:32px; z-index:7; text-align:right; font-family:var(--fontD); line-height:1.2; text-shadow:1px 1px 0 #000, 0 0 6px #000;}
#lumStatBox b, #lumFoeStat b{display:block; font-weight:normal; font-size:13px; letter-spacing:0; color:#f4c542;}
#lumStatBox i, #lumFoeStat i{display:block; font-style:normal; font-size:8.5px; letter-spacing:0; color:#cbb8d8; white-space:nowrap;}
#lumFoeStat b{color:#e88ab0;}
#lumFoeStat i{color:#e0b8c8;}
#lumFoeSide{position:absolute; top:6px; right:6px; width:22px; z-index:7; background:#140d16; border:2px solid #ffffff33; box-shadow:2px 2px 0 #0007;}
/* ⭐⭐ v0.815 — ROW RAIL. When lumSize() finds that a row above the board leaves a bigger board than
   a column beside it (which is what the NARROW screen answers, because a 12×12 board is width-bound
   there and has height to spare), the NEXT box moves up beside the inkling instead of under it, and
   the board takes the full width. ⚠ The class is written by lumSize() alone — it is a measurement,
   not a media query, and a second rule keyed on width here would fight it. */
#scr-lumen.lum-rowrail #lumSideBox{top:6px;}
.lum-nx2{display:grid; grid-template-columns:1fr 1fr; gap:1px; padding:2px;}
.lum-nx2 i{display:flex; align-items:center; justify-content:center; aspect-ratio:1; border:1px solid #00000066; border-radius:2px;}
.lum-nx2 i svg{width:64%; height:64%; image-rendering:pixelated;}
.lum-plus svg{width:54%; height:54%; display:block; image-rendering:pixelated; filter:drop-shadow(1px 1px 0 #00000088);}   /* v3.98 — a slim cross, not a chunky slab */
/* ===== LUMEN INK — the plus block + the next preview =====
   ⚠⚠ v0.818 — #lumSky and the six .lsky* cosmos backdrops ARE DELETED (with @keyframes lskyDrift
   and lskyHue). The field sits on the screen's own flat ground now. A new record still announces
   itself — it repaints the board's two colours and plays a riser — and the roulette between tracks
   is where a record gets its moment. Do not paint anything behind this board again. */
#lumField{position:relative; z-index:1;}
/* ⭐⭐⭐ v0.818 — THE BOARD AND ITS PROGRESS BAR ARE ONE STACK. #lumWrap centres a single child, so
   the bar has to ride WITH the field rather than beside it — as a second flex child of the wrap it
   would have landed to the right of the board. The bar's own room is reserved by LUM_LBL_ROOM
   (js/minigames.js), which is the mechanism the v0.815 note said anything hung under the field must
   use; taking it out of the board by hand is how the labels used to end up under the D-pad. */
#lumStack{display:flex; flex-direction:column; align-items:center; gap:5px; max-width:100%;}
/* ⭐⭐⭐ v0.818 — HOW FAR THROUGH THIS RECORD YOU HAVE PUSHED THE NEEDLE. (Odaroo: "a progress bar
   at the bottom similar to the time-draining HP bar — RGB colours — going left to right at the very
   bottom under the actual gameplay area.")
   ⚠ THE RAINBOW IS SIZED TO THE TRACK, NOT TO THE FILL (`background-size:<track width>`), and the
   fill is a WINDOW onto it. A gradient painted across the fill itself re-scales every frame, so the
   colour under the player's eye slides backwards as the bar grows — the one thing a progress bar
   must never appear to do. `width:var(--lumProgW)` is not used: the width is written inline by
   lumProgUpd, which skips the write when the number has not changed. */
#lumProg{width:100%; height:9px; background:#0a0710; border:2px solid #ffffff33; box-shadow:inset 0 0 8px #000a; overflow:hidden; position:relative;}
#lumProg i{display:block; height:100%; width:0%; background:linear-gradient(90deg,#e23b4e,#e8912a,#f4c542,#6bbf59,#5fd0c8,#5b8fe0,#a24bcf); background-size:var(--lumProgTrack,100%) 100%; background-repeat:no-repeat; transition:width .1s linear; box-shadow:0 0 10px #ffffff55;}
/* ⚠⚠ ROW RAIL: THE FIGURES JOIN THE ROW, THEY DO NOT TAKE THE OPPOSITE CORNER. In row-rail mode
   the board owns the FULL width, so a box pinned to the top-RIGHT would be sitting on it — and
   `hudL` in lumSize() only ever widens the LEFT inset, so nothing would push the board clear of it.
   ⚠⚠ v0.829 — BOTH BOXES ARE PLACED BY lumSize() IN BOTH MODES NOW, because both positions are
   measured off the portrait and the portrait's width is a live variable (--inkav moves with the
   fold). A `left` written here would be a second answer to a question only the measurement can
   settle — the two-writers jitter the v0.820 note on `side:null` is about. */
/* ⚠ the blow landing is a FLASH on the field, not a toast: it happens several times a bar and the
   pop-up rules are for messages the game is telling you, not for a hit registering on a playfield */
#lumField.lumhurt{animation:lumHurt .34s ease-out;}
@keyframes lumHurt{0%{box-shadow:0 0 0 3px #e23b4e, 0 0 30px #e23b4ecc;}100%{box-shadow:0 0 22px #000a, inset 0 0 30px #0008;}}
.lum-plus{display:flex; align-items:center; justify-content:center; width:100%; height:100%; line-height:1; animation:lumPlusPulse 1.1s ease-in-out infinite;}   /* v3.98 — a simple cross that GLOWS in place instead of swelling */
@keyframes lumPlusPulse{0%,100%{opacity:.82}50%{opacity:1}}
.lum-cell.lum-zap{filter:brightness(2.2) saturate(.4); animation:lumZapFz .12s steps(2) infinite;}   /* banked by the sweep — frozen white, waiting to ERUPT */
@keyframes lumZapFz{0%{opacity:1}100%{opacity:.72}}
.lum-nx{display:inline-grid; grid-template-columns:1fr 1fr; gap:1px; vertical-align:middle; margin-left:3px;}
.lum-nx i{width:9px; height:9px; display:flex; align-items:center; justify-content:center; font-style:normal; font-size:8px; color:#fff; text-shadow:1px 1px 0 #000; border:1px solid #0008;}
/* ===== NIM — the 1·3·5·7 ink-drop duel (v3.08) ===== */
/* ===== CARTS GALLERY — a vertical scroller that works on BOTH screens: owned first, then the ? wall ===== */
.grid.gallery.vscroll{display:grid; grid-template-columns:repeat(auto-fill,minmax(160px,1fr)); gap:16px 12px; justify-items:center; align-content:start; padding:8px 6px 40px;}
@media (max-width:420px){ .grid.gallery.vscroll{grid-template-columns:repeat(2,1fr);} }   /* folded Fold: two big columns, scroll away */
.qcard{width:100%; min-height:168px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px; background:repeating-linear-gradient(45deg,#241d2b 0 8px,#1c1622 8px 16px); border:3px dashed #6b6478; border-radius:8px; cursor:pointer;}
.qcard .q-mark{font-family:var(--fontD); font-size:44px; color:#8a8296; text-shadow:2px 2px 0 #000;}
.qcard .q-sub{font-family:var(--fontD); font-size:7px; letter-spacing:1px; color:#6b6478;}
/* ===== NIM — a warm CASINO parlour: felt table, wood rail, lamplight ===== */
.nim-p.active{box-shadow:0 0 14px #f4c542, 2px 2px 0 #0006; transform:translateY(-3px); animation:nimBob 1s ease-in-out infinite;}
.nim-p.active .nim-av svg{animation:nimJig .5s steps(2) infinite;}
.nim-start .nim-rule{font-family:var(--fontD); font-size:clamp(10px,3.4vw,15px); color:#fdf6e3; letter-spacing:1px; margin-top:10px; text-shadow:2px 2px 0 #1b1510; animation:warnBlink 1s steps(2) infinite;}
/* ===== v3.48 — NIM in the ELEVATOR SHAFT: the cab climbs forever, windows sweep past, the cast peeks in ===== */
/* the coin flip — a fat pixel coin spinning on its axis, then landing with the call */
.nim-coin.land i{animation:none; transform:rotateY(0); box-shadow:inset -6px -6px 0 #d9a232, inset 6px 6px 0 #ffe58a, 0 0 18px #f4c542;}
.nim-flag::after{content:''; position:absolute; left:3px; top:2px; width:26px; height:20px; background-image:repeating-conic-gradient(#1b1510 0 25%, #fdf6e3 0 50%); background-size:10px 10px; border:1px solid #1b1510; animation:nimFlagWave .7s ease-in-out infinite alternate;}
/* ===== INKAMP — the jukebox reborn as a full-screen Winamp (v3.00) ===== */
#scr-jukebox{background:#1a1a22;}
#jbStage{display:flex; flex-direction:column; gap:3px; padding:8px 10px calc(236px + var(--padny,0px)); overflow:hidden;}
/* ⭐⭐ v0.561 — THE DESK: one column folded, two columns unfolded (deck+EQ | playlist), the way
   real Winamp docks its windows. ⚠ min-height/min-width:0 on every rung of this ladder — a flex
   item defaults to min-*:auto and refuses to shrink under its content, which is what turns "the
   playlist scrolls" into "the playlist pushes the bottom of the screen past the D-pad". */
.wjdesk{flex:1; min-height:0; display:flex; flex-direction:column; gap:3px;}
.wjcol{display:flex; flex-direction:column; gap:3px; min-height:0; min-width:0;}
/* ⚠⚠ 820px, NOT 640px, AND THE NUMBER IS MEASURED. Two columns at 640 give the deck 333px and the
   playlist 284px — and at those widths the now-playing line, the test booth and the record titles
   ALL cut. The desk only splits when both halves are still comfortable; below that a single wide
   column beats two cramped ones. The unfolded Fold (906) lands at 477 / 406, which measures clean. */
@media (min-width:820px){
  .wjdesk{flex-direction:row; align-items:stretch;}
  .wjcol{flex:1 1 54%; min-width:0;}
  .wjdesk>.wj.grow{flex:1 1 46%; min-width:0;}
}
.wj{background:linear-gradient(#3b4251,#23272f); border:2px solid; border-color:#6a7385 #14161b #14161b #6a7385; box-shadow:3px 4px 0 #00000055; display:flex; flex-direction:column; min-height:0; flex:0 1 auto; overflow:hidden;}
.wj.grow{flex:1 1 110px; min-height:96px;}
.wjtb{height:15px; flex:0 0 auto; display:flex; align-items:center; padding:0 5px; gap:5px;
  background:repeating-linear-gradient(0deg,#2b3140 0 2px,#4a5468 2px 4px);
  font-family:var(--fontD); font-size:7px; letter-spacing:2px; color:#c8d0e0; text-shadow:1px 1px 0 #000; user-select:none;}
.wjtb .t{flex:1; text-align:center; overflow:hidden; white-space:nowrap; text-overflow:clip;}
/* the tail of a window title — the flavour half. Folded there is no room for it, and a title bar
   CUTS rather than overflows, so it is removed rather than squeezed. */
.wjt-x{font-style:normal;}
@media (max-width:639px){ .wjt-x{display:none;} }
.wjx{width:11px; height:11px; flex:0 0 auto; background:linear-gradient(#8a94a8,#3b4251); border:1px solid #14161b; color:#14161b;
  font-size:8px; line-height:9px; text-align:center; cursor:pointer; font-family:monospace;}
.wjbody{padding:6px; display:flex; flex-direction:column; gap:5px; min-height:0;}
.wjlcd{background:#000; border:2px solid; border-color:#14161b #6a7385 #6a7385 #14161b; padding:5px 7px; display:flex; gap:8px; align-items:stretch; min-width:0;}
#wjTime{font-family:var(--fontD); font-size:24px; color:#00e800; text-shadow:0 0 6px #00e80088; letter-spacing:2px; display:flex; align-items:center; flex:0 0 auto;}
.wjlcdR{flex:1; min-width:0; display:flex; flex-direction:column; gap:3px; justify-content:center;}
#jbNow{color:#00e800; font-family:var(--fontD); font-size:9px; letter-spacing:1px; white-space:nowrap; overflow:hidden; text-shadow:0 0 4px #00e80066;}
/* ⭐⭐ v0.621 — THE MARQUEE. The reel holds TWO copies of the title and travels exactly -50%, which
   is what makes the wrap seamless; `jbMarquee()` in js/rink.js is what decides whether it rolls at
   all and how long a lap takes (derived from the WIDTH, so every title scrolls at one speed).
   ⚠ The reel must be `inline-flex`, not `inline-block` — the two <i> copies have to sit on one
   line no matter how long they are, and a plain inline-block reel wraps the second copy under the
   first, which turns the scroll into a stutter.
   ⚠ NO GAP between the copies. The separator is baked into the text (the trailing spaces on the
   ♪ prefix), because a flex gap is not part of the 50% the transform travels and would show as a
   jump every lap. */
.wjmq{display:inline-flex; white-space:nowrap; will-change:transform;}
.wjmq>i{font-style:normal; flex:0 0 auto; padding-right:30px;}
.wjmq.roll{animation:wjMarq linear infinite;}
@keyframes wjMarq{from{transform:translateX(0)} to{transform:translateX(-50%)}}
/* ⭐ v0.621 — THE BIG TRACK NUMBER, and it flips over when the record changes. */
.wjno{flex:0 0 auto; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:1px; padding-right:6px; border-right:1px solid #00380f;}
.wjnoD{font-family:var(--fontD); font-size:22px; line-height:1; color:#00801a; letter-spacing:1px; min-width:30px; text-align:center;}
.wjnoD.lit{color:#00e800; text-shadow:0 0 7px #00e80099;}
.wjnoD.flip{animation:wjNoFlip .42s ease-out;}
@keyframes wjNoFlip{
  0%{transform:rotateX(-90deg) scale(1.25); opacity:.2;}
  55%{transform:rotateX(12deg) scale(1.05); opacity:1;}
  100%{transform:rotateX(0) scale(1); opacity:1;}
}
.wjnoL{font-family:var(--fontD); font-size:6px; letter-spacing:2px; color:#00e80088;}
@media (max-width:479px){ .wjnoD{font-size:17px; min-width:24px;} .wjno{padding-right:4px;} }
.wjinfo{color:#00e800aa; font-family:var(--fontD); font-size:7px; letter-spacing:1px; display:flex; gap:9px; white-space:nowrap; overflow:hidden;}
.wjinfo span{flex:0 0 auto;}
/* folded: the LCD keeps the position readout and the music lamp — STEREO / 44 KHZ are decoration
   and are the first things to go, rather than letting the row run off the black glass. */
@media (max-width:479px){ .wjinfo .wjinfo-x{display:none;} .wjinfo{gap:7px;} }
/* ⚠ v0.621 — 38px HERE AND 38 ON THE `height` ATTRIBUTE IN js/rink.js. The canvas is drawn in its
   own pixels and then stretched to whatever CSS says; the two disagreeing is how you get a
   correctly-drawn analyser that renders squashed. The 20 bars need the extra height to read. */
#wjSpec{background:#000; height:38px; width:100%; display:block;}
@media (max-width:479px){ #wjSpec{height:30px;} }
.wjradio{width:64px; flex:0 0 auto; align-self:center;}
.wjradio svg{width:100%; display:block;}
/* ⚠⚠ v0.561 — THE RADIO IS PINNED AT 56px HERE, SCOPED TO THIS SCREEN. `.wjradio` also carries the
   shared `.jb-radio` class, whose rule further down the cascade was winning and letting the set
   grow to 190px — it ate the LCD's whole right-hand column, which is why the now-playing line was
   being cut at "— tune in be…". A flex basis, not just a width, or it grows again. */
#scr-jukebox .wjradio{flex:0 0 56px; width:56px; height:43px; align-self:center;}
#scr-jukebox .wjradio svg{height:100%;}
#scr-jukebox .wjlcd{align-items:center;}
.wjseek{position:relative; height:11px; background:#14161b; border:1px solid; border-color:#000 #6a7385 #6a7385 #000; cursor:pointer; flex:0 0 auto;}
.wjseek .f{position:absolute; left:0; top:0; bottom:0; width:0%; background:linear-gradient(90deg,#0a5a1a,#00e800); pointer-events:none;}
.wjseek .k{position:absolute; top:-2px; width:9px; height:13px; margin-left:-4px; background:linear-gradient(#8a94a8,#4a5468); border:1px solid #14161b; pointer-events:none;}
.wjctl{display:flex; align-items:center; gap:3px; flex-wrap:wrap;}
.wjb{height:24px; min-width:34px; background:linear-gradient(#5a6478,#2b3140); color:#dde;
  border:2px solid; border-color:#8a94a8 #14161b #14161b #8a94a8; font-size:11px; cursor:pointer;
  font-family:var(--fontD); line-height:1; padding:0 7px;}
.wjb:active{border-color:#14161b #8a94a8 #8a94a8 #14161b; background:linear-gradient(#2b3140,#5a6478);}
.wjb.gold{background:linear-gradient(#8a6a20,#5a4210); color:#ffd76a; border-color:#c8a44a #241c08 #241c08 #c8a44a; font-size:8px; letter-spacing:1px;}
.wjb.lit{color:#00e800; text-shadow:0 0 5px #00e800;}
.wjb.godshine{animation:godshine 1.2s linear infinite;}
.wjctl .sp{flex:1;}
.wjctl label{font-family:var(--fontD); font-size:7px; color:#c8d0e0; letter-spacing:1px; flex:0 0 auto;}
/* ⭐ v0.561 — THE VOLUME SLIDER TAKES THE ROOM THAT IS LEFT, it is not a fixed 86px. At 375 that
   fixed width plus four transport buttons and the readout wrapped the row onto a second line and
   pushed the percentage half off the end. */
.wjctl .wjvol{flex:1 1 60px; min-width:52px; max-width:120px;}
.wjctl .fv{font-family:var(--fontD); font-size:8px; color:#00e800; flex:0 0 auto; min-width:26px; text-align:right;}
.wjbody.wjplbody{flex:1; min-height:0;}
#scr-jukebox input[type=range]{accent-color:#00e800;}
/* EQ window — vertical faders like the real thing */
.wjeq{display:flex; align-items:stretch; gap:8px; justify-content:space-around; padding:2px 2px 0;}
.wjfad{display:flex; flex-direction:column; align-items:center; gap:2px; flex:1; min-width:0;}
.wjfad input[type=range]{writing-mode:vertical-lr; direction:rtl; height:64px; width:18px;}
.wjfad .fl{font-family:var(--fontD); font-size:7px; color:#c8d0e0; letter-spacing:1px; white-space:nowrap;}
.wjfad .fv{font-family:var(--fontD); font-size:8px; color:#00e800; text-shadow:0 0 4px #00e80066;}
.wjeqR{display:flex; flex-direction:column; gap:5px; justify-content:center; flex:1.2; min-width:0;}
/* ⚠ v0.561 — the test booth takes the LION'S SHARE of the EQ window, because it holds the only
   TEXT in it: three faders squeezed the booth to 124px and left its readout 6px wide. ⚠ Scoped by
   ID on purpose — an unscoped copy sits EARLIER in this file than `.wjeqR` and lost to it on
   source order, which is the same trap the Tetr-Ink HUD fell into. */
@media (min-width:560px){ #scr-jukebox .wjeqR{flex:2 1 250px;} #scr-jukebox .wjfad{flex:0 1 74px;} }
/* ⭐ v0.561 — THE BOOTH READOUT GETS ITS OWN LINE, ALWAYS. "— PICK A SOUND —" needs ~110px and the
   pager beside it leaves 64px in the unfolded EQ window, so on one line it was cut at every width
   the game actually runs at. Wrapping it is the fix; shrinking the type below 7px is not. */
.wjtest{display:flex; align-items:center; gap:5px 5px; min-width:0; flex-wrap:wrap;}
.wjtest .tl{font-family:var(--fontD); font-size:7px; color:#c8d0e0; letter-spacing:1px; flex:0 0 auto;}
.wjtest .ti{font-family:var(--fontD); font-size:8px; color:#00e800; min-width:34px; text-align:center; flex:0 0 auto;}
/* ⚠ v0.561 — #00b400 measured 4.4:1 on the panel grey, a hair under WCAG's 4.5. The booth readout
   is the one green that does NOT sit on the black glass, so it needs the brighter ink. */
.wjtest .tn{font-family:var(--fontD); font-size:7px; color:#00e800; flex:1 0 100%; order:4; min-width:0; white-space:nowrap;}
@media (max-width:480px){   /* folded: faders row on top, booth below — nothing off the right edge */
  .wjeq{flex-wrap:wrap;}
  .wjeqR{flex:1 1 100%;}
  .wjfad input[type=range]{height:48px;}
  .wjradio{display:none;}   /* the radio inklet sits out the narrow stage */
  #wjTime{font-size:18px;}
}
.wjpg{width:20px; height:18px; background:linear-gradient(#5a6478,#2b3140); color:#dde; border:2px solid; border-color:#8a94a8 #14161b #14161b #8a94a8; font-size:9px; cursor:pointer; padding:0; line-height:1; flex:0 0 auto;}
.wjpg:active{border-color:#14161b #8a94a8 #8a94a8 #14161b;}
/* playlist window */
.wjpl{flex:1; min-height:0; overflow-y:auto; background:#000; border:2px solid; border-color:#14161b #6a7385 #6a7385 #14161b; padding:3px;}
#scr-jukebox .jb-side{font-family:var(--fontD); font-size:7px; letter-spacing:2px; color:#e8b04a; padding:6px 5px 3px; border:none; background:none; text-align:left; margin:0;}
#scr-jukebox .jb-track{display:flex; align-items:center; gap:8px; padding:4px 6px; margin:0; cursor:pointer;
  background:none; border:none; border-radius:0; box-shadow:none; font-family:var(--fontD);}
#scr-jukebox .jb-track:hover{background:#0a1a0a;}
#scr-jukebox .jb-track.on{background:#16244a;}
#scr-jukebox .jb-track .jb-tnm{flex:1; color:#00b400; font-size:9px; letter-spacing:1px; text-shadow:none;}
#scr-jukebox .jb-track.on .jb-tnm{color:#fff; text-shadow:0 0 5px #00e800;}
#scr-jukebox .jb-track .jb-bpm{color:#00e800aa; font-size:8px;}
/* ⭐ v0.558 — WHERE THE RECORD PLAYS, in place of the old BPM figure. ⚠ IT IS NOT `.jb-bpm`:
   that class carries `content:'BPM'` on its ::after in css/menu.css, which would have printed
   "INKVILLE THEME BPM" down the whole shelf. A different meaning gets a different class.
   ⚠ `min-width:0` on the name is what makes the row give ground gracefully on the folded screen —
   a flex item defaults to min-width:auto and refuses to shrink below its text, which is how a
   23-character title pushed the tag off the end of the row. */
#scr-jukebox .jb-track .jb-tnm{min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
#scr-jukebox .jb-track .jb-wh{color:#e8b04a; font-size:7px; letter-spacing:1px; flex:0 0 auto; font-family:var(--fontD);}
#scr-jukebox .jb-track.on .jb-wh{color:#ffd970;}
/* ⚠⚠ v0.561 — NARROW HERE MEANS THE PLAYLIST WINDOW, NOT THE SCREEN. This used to be a viewport
   media query, and it was wrong the moment the desk grew a second column: a 906px screen is "wide"
   while the playlist beside the deck can be 284px, and the record titles cut inside it. The window
   measures itself — same lesson as the Tetr-Ink HUD (a size keyed on the window and a width keyed
   on the state drift apart). When the row is tight the tag drops to its own line under the name.
   ⭐ So it is UNCONDITIONAL rather than a breakpoint: the playlist window is 355px folded and 406px
   unfolded, and the longest title plus its tag needs more than either — every width that exists
   wants the stack. A rule that behaves the same everywhere cannot be wrong on one screen. */
#scr-jukebox .jb-track{flex-wrap:wrap; gap:0 8px; padding:5px 7px;}
#scr-jukebox .jb-track .jb-tnm{flex:1 1 auto; font-size:9px;}
#scr-jukebox .jb-track .jb-wh{flex:1 0 100%; order:3; opacity:.9;}
#scr-jukebox .jb-track .jb-go{color:#00e80055; font-size:8px;}
#scr-jukebox .jb-track.on .jb-go{color:#00e800;}
/* ⭐⭐ v0.621 — THE GRIP AND THE TRACK NUMBER.
   ⚠⚠ `touch-action:none` ON THE GRIP IS NOT OPTIONAL. The playlist scrolls, so without it the
   browser claims the very first vertical move for a scroll and the drag is stolen before it starts
   — the row simply will not follow your finger on a phone. It is scoped to the grip so dragging the
   playlist itself still scrolls it, which is what a player expects everywhere else on the row.
   ⚠ The grip is a 22px-wide target on purpose: an 8px glyph is not something a thumb can find. */
#scr-jukebox .jb-track .jb-grip{flex:0 0 auto; width:22px; text-align:center; color:#00e80066; font-size:11px; line-height:1;
  cursor:grab; touch-action:none; user-select:none; order:-2;}
#scr-jukebox .jb-track:hover .jb-grip{color:#00e800aa;}
#scr-jukebox .jb-track .jb-no{flex:0 0 auto; font-family:var(--fontD); font-size:9px; color:#e8b04a99; letter-spacing:1px; min-width:16px; order:-1;}
#scr-jukebox .jb-track.on .jb-no{color:#ffd970; text-shadow:0 0 5px #e8b04a88;}
#scr-jukebox .jb-track.drag{background:#0d2a12; outline:1px dashed #00e80088; opacity:.92;}
#scr-jukebox .jb-track.drag .jb-grip{cursor:grabbing; color:#00e800;}
#scr-jukebox .jb-side i{font-style:normal; color:#e8b04a66; font-size:6px; letter-spacing:1px;}
@media (max-width:479px){ #scr-jukebox .jb-side i{display:none;} }
/* ⭐⭐ v0.628 — SIDE B, THE CHIP. Odaroo's recordings are gold on this shelf; the chiptune band gets
   the analyser's own green, so which machine a row belongs to is readable at a glance rather than
   something you learn by pressing it. The two sides are also separated by a rule, because the
   header is a WALL — a row cannot be dragged across it (see jbDragStart). */
#scr-jukebox .jb-side.b{color:#00e800; margin-top:8px; border-top:1px solid #00380f; padding-top:7px;}
#scr-jukebox .jb-side.b i{color:#00e80055;}
#scr-jukebox .jb-track.chip .jb-no{color:#00b40099;}
#scr-jukebox .jb-track.chip.on .jb-no{color:#8affa0; text-shadow:0 0 5px #00e80088;}
#scr-jukebox .jb-track.chip .jb-wh{color:#00e80088;}
#scr-jukebox .jb-track.chip.on .jb-wh{color:#8affa0;}
.wjnoD.chip.lit{color:#8affa0; text-shadow:0 0 7px #00e80099;}
/* the seek bar cannot be scrubbed on Side B — a sequencer has no position to jump to. It still
   MOVES (it shows the loop), so the only honest cue is the cursor and a dimmer knob. */
.wjseek.noscrub{cursor:default;}
.wjseek.noscrub .k{opacity:.45;}
.wj .jb-radio.playing .jb-ant{animation:antWig .5s ease-in-out infinite alternate; transform-origin:71px 20px;}
.wj .jb-radio.playing .jb-needle{animation:needleWig 1.1s ease-in-out infinite alternate;}
@keyframes wallDrift{to{background-position:120px 60px}}
.rlock{background:#7a4a8f; border:2px solid #1b1510; display:flex; align-items:center; justify-content:center; z-index:2; transition:opacity .25s;}
.rlock.open{opacity:.25;}
.rkey{display:flex; align-items:center; justify-content:center; z-index:2; animation:keyPulse 1s ease-in-out infinite;}
@keyframes keyPulse{50%{transform:scale(1.18)}}
.inkLaugh{animation:inkLaughA .18s steps(2) infinite; transform-origin:center bottom;}
@keyframes inkLaughA{0%,100%{transform:rotate(-7deg) scale(1)}50%{transform:rotate(7deg) scale(1.14)}}
.inkRun{animation:inkRunA 1s ease-in forwards;}
@keyframes inkRunA{to{transform:translateX(240px) rotate(70deg); opacity:0}}
.screenGlitch{position:fixed; inset:0; z-index:9998; pointer-events:none; mix-blend-mode:screen;
  background:repeating-linear-gradient(0deg,#ff004022 0 2px,transparent 2px 4px); animation:scrGl .18s steps(2) infinite;}
@keyframes scrGl{0%{transform:translate(0,0); opacity:.45}50%{transform:translate(-3px,2px); opacity:.8; filter:hue-rotate(80deg)}100%{transform:translate(3px,-1px); opacity:.4}}
.budShake{animation:budShk .12s steps(2) infinite;}
@keyframes budShk{0%,100%{transform:translate(0,0)}25%{transform:translate(-2px,1px)}50%{transform:translate(2px,-1px)}75%{transform:translate(-1px,-1px)}}
.glitchTxt{animation:txtGl .16s steps(2) infinite;}
@keyframes txtGl{50%{transform:translate(1px,-1px); text-shadow:2px 0 #00e0ff,-2px 0 #ff0040}}
.inkGlitch{animation:inkGl .1s steps(2) infinite;}
@keyframes inkGl{0%{transform:translate(0,0)}25%{transform:translate(-3px,2px) skewX(8deg); filter:hue-rotate(90deg)}50%{transform:translate(3px,-2px) skewX(-8deg); filter:hue-rotate(200deg)}75%{transform:translate(-2px,1px)}}
#rinkBoard.shatter{animation:rShake .5s steps(4) 2;}
@keyframes rShake{25%{transform:translate(-3px,2px) rotate(-.6deg)}75%{transform:translate(3px,-2px) rotate(.6deg)}}
.rcrack{position:absolute; inset:0; z-index:1; pointer-events:none;}   /* the fracture runs UNDER the ice blocks */
.rcrack svg{width:100%; height:100%; display:block;}
.rcrack .ck{fill:none; stroke-linejoin:round; stroke-linecap:round; stroke-dasharray:220; stroke-dashoffset:220;
  animation:crackDraw .38s ease-out forwards; animation-delay:var(--d,0s);}
.rcrack .ck-under{stroke:#ffffff; stroke-width:2.6;}   /* the white ice shows beneath the fissure */
.rcrack .ck-main{stroke:#1b2b36; stroke-width:.9;}
.rcrack .ck-ring{stroke:#3d6478; stroke-width:.7;}
@keyframes crackDraw{to{stroke-dashoffset:0}}
.cardfire{box-shadow:0 0 20px #ff7a2e; position:relative;}
.cardfire::before{content:''; position:absolute; left:-6px; right:-6px; bottom:-9px; height:26px; z-index:6; pointer-events:none;
  background:radial-gradient(circle at 18% 100%,#ffd24a 4px,transparent 10px),radial-gradient(circle at 50% 100%,#ff7a2e 7px,transparent 14px),radial-gradient(circle at 82% 100%,#ffd24a 4px,transparent 10px);
  animation:fireFlick .3s steps(2) infinite;}
@keyframes fireFlick{50%{transform:translateY(-4px) scaleX(1.08)}}
#scr-rink{font-size:11px;}
.glogo{pointer-events:auto;}   /* the sign takes taps — ten of them do something */
.glogo .lg-ink,.glogo .lg-ville{position:relative; z-index:5; pointer-events:auto;}   /* the letters ride above the glitch layers so taps land */
#scr-rink .tiny{font-size:10px!important; line-height:1.5;}
/* ⭐⭐ v0.390 — THE TETR-INK FRAME, ON INK PANIK. Same wrap, same gap, same bottom clearance for the
   D-pad, same left-hand column of boxes under the inkling — so the two puzzle towers now read as one
   pair, and the board is sized off the same arithmetic (see hzSize) instead of a folded/unfolded
   guess. The old min-width:640px rule that dove the board into the pad's empty centre is retired:
   it left the left column nowhere to live and made the two folds two different games. */
/* ⚠ the 44px of headroom is not decoration: the INK PANIK wordmark and the STAGE tag are absolutely
   positioned over the top of this screen, and without it the board is drawn straight through the
   wordmark. Tetr-Ink gets the same clearance from its own wrap sitting 14px lower. */
/* v0.424 — the 44px top padding existed to clear the wordmark band. The band is gone from every
   game now, so that height goes back to the board. */
#horrorWrap{flex:1; min-height:0; position:relative; display:flex; gap:10px; align-items:center; justify-content:center; z-index:5; padding:8px 8px calc(236px + var(--padny,0px));}
/* ⭐⭐⭐ v0.483 — `position:relative; z-index:6` IS THE BUG FIX, NOT DECORATION. DO NOT REMOVE IT.
   Odaroo: the STAGE / SPEED / rival boxes were nearly unreadable against the nightscape. They were
   not badly coloured — they are the same opaque light-panel-on-dark-ink chrome Tetr-Ink uses. THE
   CITY WAS BEING PAINTED ON TOP OF THEM.

   #hzCity is `position:absolute; z-index:0` and covers the whole wrap. In CSS paint order a
   POSITIONED element with z-index 0 paints at step 8 — after the backgrounds (step 4) AND the text
   (step 7) of every NON-positioned sibling. Every other box on that screen is positioned and
   z-indexed (#hzChar 6, #hzInfo 6, #hzWinLine 7, #horrorGrid relative), so the city goes under all
   of them. #hzSide was the ONE element on the screen that was neither — so it was the one thing the
   city covered, which is exactly the set of boxes Odaroo photographed.

   ⚠⚠ WHY NOTHING CAUGHT IT: the canvas is `pointer-events:none`, so elementFromPoint happily
   reports the box on top and every hit-test-based check passes; and `crReadable` resolves the
   colour a text node sits on by walking its ANCESTORS, so a SIBLING painted over the top is
   invisible to it. That blind spot is now checked for directly — see inkBackdropFaults() in
   js/inktest.js, which fails any build where a full-bleed backdrop out-paints a HUD box.
   ⚠ Adding a backdrop to any other game means giving that game's HUD column the same treatment. */
#hzSide{display:flex; flex-direction:column; gap:8px; width:86px; flex:0 0 auto; order:-1; align-self:flex-start; margin-top:calc(var(--inkav,69px) + 30px);
  position:relative; z-index:6;}   /* v0.640 — was a flat 132 (18 less than Tetr-Ink's, because three boxes plus a rival board is a taller column). It is derived off --inkav now for the same reason Tetr-Ink's is: the portrait size is a var, so a hand-typed clearance goes stale the moment it changes. The 18px gap between the two columns is preserved. */
#scr-horror{--tp0:#2a1636; --tp1:#c6a4e2; --tbg1:#141019;}   /* the Panik palette for the shared .tet-box / .ttn-panel chrome */
.hz-box .hz-val{font-size:13px; letter-spacing:1px; line-height:1.5;}
/* ⭐⭐⭐ v0.683 — THE MATCH CLOCK. Centred, nothing but the time on it (Odaroo's ask, verbatim).
   ⚠⚠ FIXED WIDTH. It is centred with translateX(-50%), and a box that re-measures under a percentage
   transform slides sideways every time its text changes — the coin-toss fault in CLAUDE.md. 9.4em of
   the display face holds "59:59.90", the longest thing it can ever say, with room to spare.
   ⚠ `top` is written by hzClock (js/minigames.js) off the live name reel — do not set one here, or
   the inline value and the stylesheet will disagree about which band it lives in.
   ⚠ z-index 11 puts it level with the reel: it must clear the city backdrop (the positioned-sibling
   paint-order trap) and the board, and it is pointer-events:none so it can never eat a tap meant for
   the top row of panels. */
#hzClock{position:absolute; left:50%; transform:translateX(-50%); top:46px;
  width:9.4em; text-align:center; z-index:11; pointer-events:none;
  font-family:var(--fontD); font-size:calc(15px * var(--gs,1)); letter-spacing:1px; line-height:1.4;
  color:#fdf6e3; text-shadow:2px 2px 0 #1b1510, 0 0 8px #1b1510;}
/* ============================================================
   ⭐⭐⭐ v0.484 — INK PANIK'S RUN BOX STACKS. THIS IS THE "TIME 0:..." ODAROO PHOTOGRAPHED.

   ⚠⚠ IT WAS NEVER TETR-INK. Both games share this side column and both carry a TIME row, and
   v0.483 went after Tetr-Ink's — which stacks its label above its value and was measured clean at
   every size. The screenshot shows the label and the value on ONE LINE, and that is this box:
   `.hz-sub` was `display:flex; justify-content:space-between`, so "TIME" and the clock had to share
   71px between them. Measured: the clock clips the moment the run passes 9:59 ("10:05" wants 77px
   in a 71px row) and the CHAIN row clips at almost any value at all.

   This is EXACTLY the fault v0.464 wrote up and fixed for Tetr-Ink — "LABEL ABOVE VALUE, NEVER
   BESIDE IT" — and this box simply never got the same treatment. Stacked, each figure gets the full
   width of the column to itself, which is enough for every value any of the three can reach.
   ⚠ Sized against the WORST case, not a fresh run: "59:59" on the clock, "×99" on the chain and a
   six-figure ink total. A layout that fits 0:02 is not evidence of anything.
   ============================================================ */
.hz-box .hz-sub{display:flex; flex-direction:column; align-items:center; gap:1px; min-width:0;
  line-height:1.15; font-size:6px; letter-spacing:.5px;}
.hz-box .hz-sub span{white-space:nowrap; opacity:.62;}
.hz-box .hz-sub b{font-size:9px; min-width:0; max-width:100%;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.hz-slot3{text-align:center; padding:4px 5px 5px;}
.hz-slot3 .tlab{display:block; text-align:center;}
@media (min-width:640px){
  #hzSide{width:104px;}
  .hz-box .hz-val{font-size:16px;}
  .hz-box .hz-sub{font-size:8px;} .hz-box .hz-sub b{font-size:11px;}
}
#horrorGrid{position:relative; border-radius:3px; background:#1b101833; box-shadow:0 0 0 4px #1b1018, 0 0 0 7px #f4c54244, 0 6px 20px #0008;}   /* ⭐ v3.68 — the play field now reads as a framed, contained board (outline only, so the panels don't shift) */
#hzPanels{position:absolute; inset:0;}
#hzChar{display:none; position:absolute; left:1%; top:24%; width:76px; z-index:6; text-align:center; background:#1b1510dd; border:2px solid var(--gold); border-radius:6px; padding:4px 3px 5px; box-shadow:2px 2px 0 #0007;}   /* duplicate inklet + hearts box retired — one inklet (top-left bar) only */
.hzLogoJp{font-family:var(--fontJ); font-size:11px; color:#e6b6d4; margin-top:2px; letter-spacing:1px;}
#hzChar .hzCharArt{width:58px; height:58px; margin:0 auto; filter:drop-shadow(2px 2px 0 #1b1510);}
#hzChar .hzCharArt svg{width:100%; height:100%;}
#hzChar .hzCharArt.hzInk{image-rendering:pixelated; filter:drop-shadow(0 0 6px #7ab6ff88) drop-shadow(2px 2px 0 #1b1510);}
#hzChar .hzCharNm{font-size:8px; font-family:var(--fontD); color:#fdf6e3; background:#1b1510cc; padding:1px 3px; margin-top:2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:74px;}
.hzStat{font-size:7px; font-family:var(--fontD); color:#ffe08a; background:#1b1510dd; padding:2px 3px; margin-top:2px; line-height:1.35; white-space:nowrap; border-radius:2px; display:inline-block;}
#hzMonStat{position:absolute; right:1%; top:60%; z-index:6; font-size:7px; font-family:var(--fontD); color:#ff9a8a; background:#1b1510dd; padding:2px 4px; line-height:1.35; text-align:center; white-space:nowrap; border-radius:2px; box-shadow:1px 1px 0 #0007;}
#hzCombo{position:absolute; left:50%; top:24%; transform:translateX(-50%) scale(0); z-index:9; font-family:var(--fontD); font-size:20px; color:#f2c418; text-shadow:2px 2px 0 #1b1510,0 0 10px #f2c418; pointer-events:none; opacity:0; white-space:nowrap;}
#hzCombo.go{animation:hzComboA 1.06s ease-out 1;}
@keyframes hzComboA{0%{opacity:0; transform:translateX(-50%) scale(.4) rotate(-6deg)}25%{opacity:1; transform:translateX(-50%) scale(1.25) rotate(3deg)}45%{transform:translateX(-50%) scale(1) rotate(0)}80%{opacity:1}100%{opacity:0; transform:translateX(-50%) scale(1.1) translateY(-22px)}}
@keyframes hzCharBob{50%{transform:translateY(-5px) rotate(3deg)}}
#horrorWrap.frozen{box-shadow:inset 0 0 46px #2fe0ffcc; animation:hzFrz .67s ease-in-out infinite alternate;}   /* the whole stage ICES over during a chain */
@keyframes hzFrz{from{filter:saturate(.8) brightness(1)}to{filter:saturate(1.3) brightness(1.14) hue-rotate(-14deg)}}
#horrorGrid{position:relative; background:#141019; border:3px solid var(--ink); box-shadow:5px 5px 0 #1b151066; overflow:hidden;}
/* ⭐ v0.567 — the GAME OVER card over a board the grey curtain has filled (see hzShowOver).
   Deliberately the same shape as Tetr-Ink's #tetOver so the two games read as one house style. */
#hzOver{position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  background:#1b1018cc; color:#f4c542; font-family:var(--fontD); font-size:13px; letter-spacing:2px;
  text-align:center; line-height:2; z-index:5; text-shadow:2px 2px 0 #000; pointer-events:none;}
.hpan svg{width:70%; height:70%; image-rendering:pixelated; filter:drop-shadow(1px 1px 0 #00000055);}
/* Panel de Pon-style GLOSSY GEMS — rounded, top sheen, deep bottom bevel, sparkle dot */
.hpan{position:absolute; display:flex; align-items:center; justify-content:center; font-size:14px;
  color:#fdf6e3; border:2px solid #141019; border-radius:22%; cursor:pointer; box-sizing:border-box; font-weight:bold;
  background-image:linear-gradient(160deg, #ffffffaa 0%, #ffffff33 26%, #ffffff00 45%, #00000022 66%, #00000055 100%)!important;
  background-blend-mode:normal;
  box-shadow:inset 0 2px 0 #ffffff66, inset 2px 0 0 #ffffff2c, inset 0 -4px 0 #00000045, inset -2px 0 0 #00000026, 0 2px 0 #00000044;
  text-shadow:1.5px 2px 0 #00000077;}
.hpan::before{content:''; position:absolute; left:14%; top:10%; width:26%; height:16%; border-radius:50%; background:#ffffffcc; filter:blur(.4px); pointer-events:none;}
.hpan svg{position:relative; z-index:1; width:92%; height:92%; filter:drop-shadow(1px 1.5px 0 #00000066);}   /* the symbol FILLS the gem */
.hpan.emp{background-image:none!important; box-shadow:none; border-radius:0;}
.hpan.emp::before{display:none;}
.hpan.selq{outline:3px dashed #fdf6e3;}
/* ============================================================
   ⭐⭐⭐ v0.594 — GARBAGE SLABS (Ink Panik online). See js/panik-online.js for the mechanic.
   A slab has to read INSTANTLY as "not a panel" from the corner of the eye while you are playing, so
   it is the visual opposite of one at every level: flat where a panel is glossy, SQUARE where a panel
   is round, cream-and-black signage where a panel is a coloured gem. The heavy black keyline and the
   rivets are the 1930s cartoon crate — the thing that falls on the cat.
   ⚠ ONE ELEMENT PER BLOCK, spanning the whole rectangle. Drawing it cell by cell would give it
   internal borders and it would read as a suspicious run of identical tiles instead of one object.
   ============================================================ */
.hjunk{position:absolute; z-index:2; box-sizing:border-box; pointer-events:none;
  display:flex; align-items:center; justify-content:center; gap:6%;
  background:var(--jpaper,#cfcfcf); border:3px solid #1b1510; border-radius:3px;
  box-shadow:inset 0 3px 0 #ffffff66, inset 0 -5px 0 #00000033, 0 3px 0 #00000055;
  background-image:repeating-linear-gradient(135deg, #00000000 0 7px, #00000012 7px 14px);
  overflow:hidden;}
/* the rivets — one in each corner, drawn from the block's own ink colour */
.hjunk::before,.hjunk::after{content:''; position:absolute; left:5px; right:5px; height:5px;
  background:radial-gradient(circle at 3px 50%, var(--jink,#4a4a4a) 0 2.4px, #00000000 2.6px),
             radial-gradient(circle at calc(100% - 3px) 50%, var(--jink,#4a4a4a) 0 2.4px, #00000000 2.6px);}
.hjunk::before{top:4px;} .hjunk::after{bottom:4px;}
.hj-art{position:relative; z-index:1; display:flex; align-items:center; justify-content:center; max-height:74%;}
.hj-art svg{width:100%; height:100%; image-rendering:pixelated;}
.hj-nm{position:relative; z-index:1; font-family:var(--fontD); letter-spacing:.08em; color:#1b1510;
  text-shadow:1px 1px 0 #ffffff88;}
/* it FALLS in — heavy, with a squash at the bottom, so a slab landing is felt and not just noticed */
.hjunk.drop{animation:hjDrop .35s cubic-bezier(.36,0,.7,1) both;}
@keyframes hjDrop{0%{transform:translateY(var(--jd,-60px)) scaleY(1.06)}
  78%{transform:translateY(0) scaleY(1)} 88%{transform:translateY(0) scaleY(.9) scaleX(1.05)}
  100%{transform:translateY(0) scaleY(1)}}
/* HIT. The whole wall shudders and glows the moment a clear touches it, before the unwind starts. */
.hjunk.brk{animation:hjBrk .37s steps(2) infinite; border-color:#f4c542;}
@keyframes hjBrk{50%{filter:brightness(1.9) saturate(.2); transform:translateX(1px)}}
/* ⭐⭐ v0.604 — THE UNWIND, ONE CELL AT A TIME AND NOT RUSHED.
   TWO layers, and they do different jobs (see hzJunkBreak in js/panik-online.js):
   `.hjlit` is the SPARK on the cell being cracked this instant — it flips over, flashes and goes.
   `.hjcrk` is what it LEAVES BEHIND: a turned-over, cracked-open cell that stays put until the
   whole wall is undone. Without the second one the reveal was a twinkle you could miss. */
.hjlit{position:absolute; z-index:5; pointer-events:none; box-sizing:border-box;
  border:2px solid #fdf6e3; background:#fdf6e3;
  animation:hjLit .21s ease-out both;}   /* ⚠ v0.643 — SHORTER THAN .hjcrk's .24s, or the spark is still
                                            fading over the panel it was supposed to hand over to */
/* ⭐ v0.643 — THE "TUK" IS NOW ONLY THE FRONT HALF OF ONE TURN. The cell rotates to edge-on and is
   gone — `.hjcrk` picks the movement up from there and rotates the REAL PANEL into its place, so the
   two layers read as a single tile turning over. It used to rotate a full 180° on its own and land on
   a yellow blank, which is the "flip of a tile that's without anything" Odaroo was seeing. Shorter
   than the reveal on purpose: the spark must be finished before the panel arrives. */
@keyframes hjLit{0%{opacity:.9; transform:rotateY(0deg) scale(1); filter:brightness(2.6)}
  70%{opacity:.85; transform:rotateY(72deg) scale(1.03); background:#fdf6e3}
  100%{opacity:0; transform:rotateY(90deg) scale(1); background:#fdf6e3}}
/* ⭐⭐⭐ v0.643 — THE CELL AFTER IT HAS BEEN TURNED **IS THE PANEL IT WAS HIDING**.
   It used to be a blank yellow shard, which is exactly what Odaroo saw: a wall coming apart to
   reveal nothing, and then a second, different animation handing panels back. This is styled as a
   real `.hpan` now (its colour comes inline from hzRender) and it TURNS IN on its own axis, so the
   flip and the reveal are one movement. ⚠ It keeps its own z-index above the slab: the panel is
   sitting on top of the block it just came out of until the whole wall has been shown. */
/* ⭐⭐⭐ v0.683 — THE CONNECTION SHINE. Every cell of the matched run AND of every wall it reaches
   wears one of these, on the SAME animation-delay, so the whole group flares as one object before
   anything comes apart. See HZ_SHINE_MS in js/minigames.js.
   ⚠ z-index 6: over the panels (0), over the slabs (2) — you must see it ON the junk, that is the
   entire point — and UNDER the cursor (12), which canon rule 6 says nothing may ever cover.
   ⚠ It is a WHITE WASH plus a rim, not a background swap: the junk's own artwork has to stay
   readable underneath it or the player cannot tell which wall just lit up. */
.hshine{position:absolute; z-index:6; pointer-events:none; box-sizing:border-box;
  border:2px solid #fffbe8; border-radius:2px; background:#fffbe855;
  box-shadow:0 0 10px #fffbe8cc, inset 0 0 8px #ffffffaa;
  animation:hzShine .46s ease-in-out both;}
@keyframes hzShine{0%{opacity:0; transform:scale(.94)}
  22%{opacity:1; transform:scale(1.02)}
  55%{opacity:.62; transform:scale(1)}
  78%{opacity:1; transform:scale(1.02)}
  100%{opacity:.85; transform:scale(1)}}
.hjcrk{position:absolute; z-index:4; pointer-events:none; box-sizing:border-box;
  display:flex; align-items:center; justify-content:center;
  border:1px solid #1b1510; border-radius:2px; background:#f4c542;
  box-shadow:inset 0 2px 0 #ffffff55, inset 0 -3px 0 #00000033;
  animation:hjCrk .17s ease-out both;}
.hjcrk svg{width:100%; height:100%; image-rendering:pixelated;}
/* ⭐⭐⭐ v0.677 — NO TURN AT ALL. THE PANEL IS SIMPLY THERE.
   Odaroo, 2026-08-06: skip the paper animation, go straight to the block underneath.
   This used to rotate the cell in from edge-on (`rotateY(-96deg)` → 0) behind a cream `.hjlit` card
   doing the front half of the same turn — two layers spending half a second between the player and
   the thing they were waiting to see. The panel now exists at full size on frame one; all that is
   left is a single-frame bloom so the eye can follow WHICH cell just came free, and it is over
   before the next cell in the wall arrives.
   ⚠ It must stay SHORTER than HZ_CRACK_MS (104ms is the cadence, this is 170ms of very light
   brightness falloff on an already-solid tile) — a reveal that is still visibly animating when the
   whole wall drops is the "not smooth when it falls" half of the same complaint. */
@keyframes hjCrk{0%{opacity:1; transform:scale(1.06); filter:brightness(2.2)}
  100%{opacity:1; transform:scale(1); filter:brightness(1)}}
/* the empty cell a slab sits over — invisible, but it keeps the cursor and the taps working */
.hpan.hjcell{border-color:transparent!important; background:transparent!important;}
/* ⭐⭐ v0.646 — THE RIVAL'S SLABS, AND THEY HAVE TO BE UNMISSABLE. Odaroo could not see his attacks
   landing, and part of that was this: junk on the miniature was a near-black square on a dark board,
   at a quarter of a tile. It is the CRATE PAPER colour now with the same hatch as a real slab and a
   gold rim, so a bar arriving on their side reads at a glance from across the screen. */
.hpn-junk{background:#c9bda6!important; border:1px solid #1b1510!important; border-radius:1px!important;
  box-shadow:inset 0 0 0 1px #f4c54288!important;
  background-image:repeating-linear-gradient(135deg,#00000000 0 2px,#1b151033 2px 4px)!important;}
.hpn-junk::before{display:none!important;}
/* ⭐⭐⭐ v0.646 — YOUR OWN BAR, FALLING ONTO THEIR BOARD. See hnSentDrop in js/panik-online.js: it is a
   prediction drawn for a third of a second and then replaced by their real snapshot, so it must read
   as "this just landed" and get out of the way. Gold-rimmed while it falls, then it fades. */
/* ⚠⚠⚠ v0.648 — AND IT IS LOUD, BECAUSE ODAROO STILL COULD NOT SEE IT. The rival board is a QUARTER
   of a tile per cell — a one-row bar is six to ten pixels tall on a dark board, which is honest to
   scale and far too quiet to notice while you are playing your own board. So the bar is no longer
   drawn to scale: it has a MINIMUM height, a gold rim, an outward glow, a white flash on the frame
   it lands, and it hangs around afterwards long enough to be seen. It is the loudest thing on that
   little board on purpose — it is the only thing on it that is about something you just did. */
.hznDrop{position:absolute; z-index:8; pointer-events:none; box-sizing:border-box;
  min-height:7px; background:#f0e2bd; border:1.5px solid #f4c542; border-radius:1px;
  background-image:repeating-linear-gradient(135deg,#00000000 0 2px,#1b151055 2px 4px);
  box-shadow:0 0 8px #f4c542, 0 0 16px #f4c54288, inset 0 0 0 1px #fff8;
  animation:hznDrop 1.53s cubic-bezier(.36,0,.7,1) both;}
@keyframes hznDrop{
  0%  {transform:translateY(var(--jd,-40px)) scaleY(1.14); opacity:0}
  8%  {transform:translateY(calc(var(--jd,-40px) * .8)) scaleY(1.14); opacity:1}
  34% {transform:translateY(0) scaleY(1); opacity:1}
  40% {transform:translateY(0) scaleY(.7) scaleX(1.16); filter:brightness(2.2)}
  48% {transform:translateY(0) scaleY(1.06) scaleX(.98); filter:brightness(1)}
  54% {transform:translateY(0) scaleY(1)}
  86% {opacity:1}
  100%{transform:translateY(0) scaleY(1); opacity:0}}
/* ⭐⭐⭐ v0.684 — THE RIVAL'S CURSOR ON THE RIVAL'S BOARD. (Odaroo: "double-check that you include
   the cursor on the enemy inklet screen as well.")
   It is the SAME dashed two-cell selection square as `.hcur`, deliberately — a rival's miniature has
   to be a true small copy of what they are looking at, which is the rule .hpan and .tcell are drawn
   by too. Only three things differ, and all three are because it is drawn at a quarter size:
     · a 1.5px border, because a 3px dash on a 6px cell is a solid blob and reads as a filled square
       rather than as brackets;
     · a colour, not the white of your own — this is THEIR cursor and it must never be mistaken for
       yours at a glance across two boards;
     · no blink. The tick on the real cursor is there because it sits still while you think; this one
       is the only thing on that little board that MOVES, so it needs no help being noticed, and a
       flashing 6px rectangle at the edge of the eye is just noise.
   ⚠⚠ z-index 14 — ABOVE `.hznDrop` (8), which is the miniature's own falling-bar layer. This is
   canon rule 6 applied to the small board: the cursor lost to the unwind layers on the real board
   and vanished exactly when a wall came apart, and a cursor that hides under your own attack landing
   is hiding at the one moment you care what they do next. Nothing may be drawn over it. */
.hncur{position:absolute; z-index:14; pointer-events:none; box-sizing:border-box; border-radius:0;
  border:1.5px dashed #7ff3c8;
  filter:drop-shadow(1px 1px 0 #10201a) drop-shadow(-1px -1px 0 #10201a);}
/* the whole little board flinches white when one lands, so it is caught in the corner of the eye */
#hznMini.hnLand{animation:hnLandFlash .45s steps(2) 2;}
@keyframes hnLandFlash{50%{filter:brightness(1.9) drop-shadow(0 0 6px #f4c542)}}
/* ⭐⭐⭐ v0.646 — THE CURSOR IS ON TOP OF EVERYTHING, ALWAYS. Odaroo: "the cursor disappears behind the
   junk blocks." At z-index 3 it beat the slabs (2) but LOST to every layer of the unwind — `.hjcrk`
   (4), `.hpopfx` (4) and `.hjlit` (5) — so the moment a wall started coming apart, the one thing
   telling you where your hands are vanished underneath it. It is now above every layer inside
   #hzPanels; nothing in this game may ever be drawn over the cursor, because you cannot play what you
   cannot find. */
.hcur{position:absolute; border:3px dashed #ffffff; pointer-events:none; z-index:12; box-sizing:border-box; border-radius:0;
  filter:drop-shadow(1px 1px 0 #1b1510) drop-shadow(-1px -1px 0 #1b1510); animation:hcurTick 1.06s steps(2) infinite;}   /* DASHED white brackets — little lines, not a solid block */
@keyframes hcurTick{50%{border-color:#dfe8ee}}
/* — INK PANIC motion pack — */
.hpan.hsw{animation:hswap .19s ease-out both;}                      /* the two grabbed panels GLIDE across each other */
#pmBoard.pmUp{animation:pmUpA .27s ease-out;}                        /* the mini stack CLIMBS a step, panic-style */
/* THE COMBINATION — 4 rows of 4, evenly spread on every fold; finished rows light up and bank their ×combo */
.seq-grid{display:flex; flex-direction:column; gap:7px; align-items:center; margin:6px 0;}
.seq-r{display:flex; gap:14px; align-items:center; justify-content:center; padding:3px 10px; border:2px solid transparent; border-radius:4px;}
.seq-r .arrowfill{width:26px; height:26px; display:inline-block; transition:transform .1s;}
.seq-ab{display:inline-flex; align-items:center; justify-content:center; width:26px; height:26px; border:2px solid #fdf6e3; border-radius:50%; font-family:var(--fontD); font-size:13px;}
.seq-cur{transform:scale(1.3); filter:drop-shadow(0 0 6px #fff);}
.seq-r.lit{border-color:var(--gold); background:#f4c54222; box-shadow:0 0 10px #f4c54277; animation:seqLit .45s ease-out;}
@keyframes seqLit{0%{transform:scale(1.08); filter:brightness(1.8)}100%{transform:scale(1)}}
.seq-x{font-family:var(--fontD); font-size:11px; color:#f4c54244; width:28px; text-align:left;}
.seq-x.on{color:var(--gold); text-shadow:1px 1px 0 #1b1510; animation:seqLit .45s ease-out;}
@keyframes pmUpA{from{transform:translateY(10px)}to{transform:translateY(0)}}
@keyframes hswap{from{transform:translateX(var(--sw))}to{transform:translateX(0)}}
.hpan.hfall{animation:hfall .27s cubic-bezier(.4,0,1,1) both;}        /* gravity: tiles DROP into the gap, then squash */
@keyframes hfall{0%{transform:translateY(var(--fy))}75%{transform:translateY(0) scaleY(1)}88%{transform:translateY(0) scaleY(.82)}100%{transform:translateY(0) scaleY(1)}}
.hpopfx{position:absolute; z-index:4; pointer-events:none; display:flex; align-items:center; justify-content:center;
  animation:hpopfx .51s ease-out both;}
@keyframes hpopfx{0%{transform:scale(1) rotate(0); opacity:1; filter:brightness(2.4)}
  55%{transform:scale(1.32) rotate(7deg); opacity:.8; filter:brightness(1.4)}
  100%{transform:scale(.15) rotate(16deg); opacity:0}}
.hpopfx::before,.hpopfx::after{content:''; position:absolute; width:6px; height:6px; background:#fdf6e3; border:1px solid #1b1510;}
.hpopfx::before{animation:hpopdotA .51s ease-out both;}
.hpopfx::after{animation:hpopdotB .51s ease-out both;}
@keyframes hpopdotA{from{transform:translate(0,0); opacity:1}to{transform:translate(-16px,-20px); opacity:0}}
@keyframes hpopdotB{from{transform:translate(0,0); opacity:1}to{transform:translate(17px,-14px); opacity:0}}
#horrorGrid.lifting{box-shadow:0 -8px 18px -8px #2fe0ff, 0 0 0 2px #2fe0ff55;}   /* hold-B climb glow */
/* ⚠ v0.479 - THE OLD GREEN STRIPES ARE GONE. #horrorBg carried the striped wash that Odaroo asked
   to replace; the nightscape canvas is the backdrop now and this layer would only sit on top of it. */
#horrorBg{display:none;}
/* ⭐⭐⭐ v0.479 - THE NIGHTSCAPE. One canvas, drawn at 160x100 and blown up, which is what makes it
   read as pixel art AND what keeps it cheap on a phone. Behind everything, deaf to taps.
   ⚠ image-rendering:pixelated is doing real work here: without it the browser smooths the upscale
   and the whole scene turns into a watercolour. */
#hzCity{position:absolute; inset:0; width:100%; height:100%; z-index:0;
  pointer-events:none; image-rendering:pixelated; image-rendering:crisp-edges;
  opacity:.92;}
#horrorBoss{position:absolute; right:3%; bottom:5%; width:92px; height:102px; opacity:.45; animation:bossBob 3.2s ease-in-out infinite;}
@keyframes bossBob{50%{transform:translateY(-9px)}}
#warnOv{position:absolute; inset:0; z-index:70; display:none; align-items:center; justify-content:center;
  flex-direction:column; background:#1b1510e8; overflow:hidden; padding-bottom:22%;}   /* same vertical spot as BATTLE START */
#warnOv.on{display:flex;}
.warnSub2{font-family:var(--fontD); font-size:clamp(10px,3.6vw,17px); color:#fdf6e3; letter-spacing:1px; line-height:1.4; text-align:center; margin-top:12px; max-width:90%; z-index:2;
  text-shadow:2px 2px 0 #1b1510; animation:warnBlink 1s steps(2) infinite;}
.warnStripe{position:absolute; left:-20%; right:-20%; height:46px;
  background:repeating-linear-gradient(45deg,#c93a22 0 24px,#1b1510 24px 48px); opacity:.9;}
.warnStripe.t{top:10%;} .warnStripe.b{bottom:10%;}
.warnTxt{font-family:var(--fontD); color:#c93a22; font-size:26px; z-index:2;
  animation:warnBlink .6s steps(2) infinite; text-shadow:2px 2px 0 #000;}
@keyframes warnBlink{50%{opacity:.3}}
/* WARNING — same two-line shape as BATTLE START: huge 警告 on top, WARNING below, nothing else. It SHAKES like a buzzer. */
.warnBig{z-index:2; text-align:center; display:flex; flex-direction:column; align-items:center; gap:6px; animation:warnShake .09s steps(2) infinite;}
.warnJp{font-family:var(--fontJ); font-size:clamp(38px,15vw,72px); color:#c93a22; line-height:1;
  text-shadow:3px 3px 0 #1b1510,-3px -3px 0 #1b1510,3px -3px 0 #1b1510,-3px 3px 0 #1b1510,0 0 22px #c93a22cc;
  animation:warnFlic .18s steps(2) infinite;}
.warnEn{font-family:var(--fontD); font-size:clamp(22px,9vw,44px); font-weight:bold; color:#fff; letter-spacing:2px; line-height:1;
  text-shadow:3px 3px 0 #c93a22,-2px -2px 0 #1b1510,2px -2px 0 #1b1510,-2px 2px 0 #1b1510,4px 4px 0 #1b1510;}
@keyframes warnShake{0%{transform:translate(-4px,2px) rotate(-1deg)}50%{transform:translate(4px,-2px) rotate(1deg)}}
@keyframes warnFlic{50%{opacity:.32; filter:hue-rotate(-18deg)}}
/* ============================================================
   ⭐⭐⭐ v0.583 — THE TWO WELLS RESERVE THE NAME STRIP'S ROOM, SO THE NAME CAN GROW.
   (Odaroo, 2026-08-04: "the username font should be LARGE — since player levels were removed there
   is room to go big.")

   THIS BLOCK IS THE PRICE OF THAT, AND IT HAD TO BE PAID FIRST. The v0.581 note on --nmsz
   (css/base.css) left a standing instruction rather than a taste: 18px was a MEASURED ceiling, and
   the thing it was measured against is these two screens. Run against the real pre-flight, 19px put
   the Tetr-Ink well under the top HUD by 29x4 pixels and 20px by 35x6 — so raising the number
   without doing this first is not "bigger text", it is text drawn over the board. The note named the
   fix and the shape to copy (#scr-hell, games.css above): make the playfield's inset TRACK the name
   size instead of being a constant that goes quietly wrong the moment the number it was measured
   against moves. That is the third screen this exact failure has now visited.

   ⚠ THE 1.6 IS THE LINE BOX, NOT THE GLYPH. --nmsz is a font-size; the strip that carries it is a
   line box plus its own 2px leader and padding, and 1.6 is the ratio #scr-hell already measured for
   the identical strip. The +10px is the strip's own top offset (4px * --gs) with a little air.
   ⚠ ONLINE IS UNTOUCHED ON PURPOSE. `#tetWrap.vs-inset` / `#horrorWrap.vs-inset` are id+class and
   still outrank this, so the 94px match inset and its ≥640px reclaim both stand exactly as measured
   — a match already reserves far more than a name needs, and re-deriving it here would be a second
   copy of a number that has its own reasons.
   ⚠ A DEEPER INSET COSTS THE BOARD HEIGHT, and both wells are height-bound. That is the real trade
   and it is why the pre-flight has to see it: if a fold ever cannot afford this, the answer is a
   smaller --nmsz, not a smaller reservation.
   ============================================================ */
#tetWrap, #horrorWrap{ padding-top:calc((var(--nmsz,18px) * 1.6 + 10px) * var(--gs,1) + var(--hudtop,16px)); }
