/* ============================================================
   BENDING MACHINE — vector chrome (wave 15).
   The chrome is DRAWN HERE, in CSS + inline SVG, cel style:
   flat fills, dark line work, neon. Palette sampled from Sol's
   production paint. Sol keeps the illustration (cans, labels,
   capsules, stickers, coin, fx); everything structural is code,
   so chrome and hit-boxes share one coordinate system and can
   never drift apart (the 2026-08-04 radio lesson, generalized).
   ============================================================ */
/* The locked palette (Claude Design pass, 2026-08-06). Grouped by surface;
   a colour used outside its group is a bug. The station keeps the old blue
   wall palette on purpose — it is the room, not the machine. */
:root{
  /* body */
  --body-lite:#4c827b; --body:#3f6d68; --body-deep:#2d514d;
  --panel-lite:#4f8880; --panel:#457b74; --panel-deep:#33605a;
  /* devices */
  --dev-lite:#3d444c; --dev:#2f353b; --dev-deep:#242a2f;
  --dev-slot:#414954; --line:#171a1d; --scanner:#8f97ad;
  /* glass + rack */
  --interior:#1d2523; --interior-mid:#151b1a; --interior-deep:#0e1312;
  --rail-lite:#4d555c; --rail:#363c42; --rail-deep:#282d32;
  /* neon + keys */
  --glow:#ffc44d; --amber:#ffc44d; --plate:#e0a83a;
  --kp:#ead9b6; --kp-edge:#b9a780;
  --kp-alt:#f2c14e; --kp-alt-edge:#c99a2e;
  /* signage + paper */
  --coral:#e8635a; --red:#d94f4f;
  --cream:#f4e9d0; --gold:#e0b95c;
  /* CRT + station */
  --crt-bg:#0d1f16; --crt-fg:#4dff9d;
  --wall:#101637; --wall-lite:#151c42; --wall-edge:#1b2350;
  --ol:2.5px;               /* the cel outline weight */
  /* THE KEY LIGHT (WS8, BT-20): one light, up and to the left. A raised part catches it
     on its top-left inner edge and shades bottom-right; its drop falls down-right. */
  --key-rim:inset 1.5px 1.5px 0 rgba(255,255,255,.13), inset -1.5px -2px 0 rgba(0,0,0,.3);
  --key-drop:2px 3px 0 rgba(0,0,0,.25);
  /* One can size, four breakpoints: the single place the can size lives. */
  --can-w:64px; --can-h:97px;
  font-family:"Trebuchet MS","Segoe UI",system-ui,sans-serif;
}
*{ box-sizing:border-box; margin:0; padding:0; -webkit-tap-highlight-color:transparent; }
/* the machine gives its own feedback (keys travel, cans lift, flaps swing) —
   the OS's blue tap silhouette on top of that read as a glitch (Ben) */
html,body{ height:100%; }
body{ background:var(--wall); overflow:auto; }   /* browser zoom must be able to pan */
/* scale-fitted stage: the visual fits, so the oversize layout box must not scroll */
@media (min-width:561px){ body.fitted{ overflow:hidden; } }
button{ font:inherit; border:none; background:none; cursor:pointer; color:inherit; }

/* ---------- first-paint power gate ----------
   The real machine mounts behind this service cover. The progress bar is tied
   to decoded first-view art; POWER ON is the user gesture that starts the CRT
   and WebAudio. It is HTML rather than baked copy, so status remains live. */
.no-js #power-gate,.no-js #stage{ display:none; }
#power-gate{ --power-progress:0; position:fixed; inset:0; z-index:100;
  display:flex; align-items:center; justify-content:center; padding:20px;
  overflow:hidden; background-color:#0d1533;
  background-image:
    linear-gradient(rgba(40,52,96,.42) 2px, transparent 2px),
    linear-gradient(90deg, rgba(40,52,96,.34) 2px, transparent 2px);
  background-size:48px 60px;
  transition:transform .72s cubic-bezier(.72,0,.22,1), visibility .72s;
  will-change:transform; }
#power-gate[hidden]{ display:none; }
#power-gate.is-opening{ transform:translateY(-104%); visibility:hidden; }
.power-panel{ position:relative; width:min(540px, calc(100vw - 32px));
  border:4px solid var(--line); border-radius:7px; background:var(--body);
  box-shadow:10px 12px 0 rgba(0,0,0,.5), inset 0 0 0 3px rgba(255,255,255,.12);
  overflow:hidden; color:var(--cream); }
.power-panel:focus{ outline:none; }
.power-panel-head{ display:flex; align-items:center; justify-content:space-between; gap:16px;
  min-height:52px; padding:12px 28px; background:var(--coral); color:var(--cream);
  border-bottom:4px solid var(--line); font-weight:900; letter-spacing:.13em; }
.power-panel-head span:first-child{ font-size:14px; }
.power-panel-head span:last-child{ font-family:"Consolas","DejaVu Sans Mono",monospace;
  font-size:9px; letter-spacing:.08em; text-align:right; }
.power-panel-body{ padding:18px; background:var(--body-deep); text-align:center; }
.power-console{ position:relative; padding:26px 30px 18px; overflow:hidden;
  border:3px solid var(--line); border-radius:4px; background:var(--dev);
  box-shadow:inset 0 0 0 3px var(--dev-lite); }
.power-kicker{ color:#9db0b3; font-size:10px; font-weight:900; letter-spacing:.16em; }
#power-title{ margin-top:8px; color:var(--cream); font-size:clamp(30px,8vw,44px);
  line-height:1; letter-spacing:.045em; }
.power-readouts{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px;
  margin-top:22px; }
.power-readout{ display:flex; align-items:center; justify-content:center; gap:7px;
  min-width:0; min-height:38px; padding:7px 9px; border:3px solid var(--line);
  border-radius:3px; background:var(--crt-bg); box-shadow:inset 0 0 0 2px var(--dev-lite);
  font-family:"Consolas","DejaVu Sans Mono",monospace;
  color:var(--amber); font-size:10px; font-weight:700; letter-spacing:.06em; line-height:1.15; }
.power-readout p{ min-width:0; text-wrap:balance; }
.power-lamp{ width:9px; height:9px; flex:none; border:2px solid var(--line);
  border-radius:50%; background:var(--amber); box-shadow:0 0 8px rgba(255,196,77,.55); }
#power-gate[data-state^="ready"] .power-lamp{ background:var(--crt-fg);
  box-shadow:0 0 9px rgba(77,255,157,.65); }
#power-gate[data-state^="ready"] .power-lamp-chilled{ background:#55c8ff;
  box-shadow:0 0 9px rgba(85,200,255,.72); }
.power-meter{ height:14px; margin:7px 0 22px; padding:3px; overflow:hidden;
  border:2px solid var(--line); background:#101514; }
#power-meter-fill{ display:block; width:100%; height:100%; background:var(--amber);
  transform:scaleX(var(--power-progress)); transform-origin:left center;
  transition:transform .14s steps(4); }
#power-gate[data-state^="ready"] #power-meter-fill{ background:var(--crt-fg); }
#power-start{ display:inline-flex; align-items:center; justify-content:center; gap:11px;
  width:min(270px, 100%); min-height:54px; padding:10px 18px; border:3px solid var(--line);
  border-radius:5px; background:var(--amber); color:var(--line);
  box-shadow:0 5px 0 var(--kp-alt-edge), inset 0 3px 0 rgba(255,255,255,.3);
  font-size:16px; font-weight:900; letter-spacing:.16em;
  transition:transform .08s, background-color .08s, color .08s, box-shadow .08s, filter .16s; }
#power-start::before{ content:""; width:22px; height:22px; flex:none;
  background:currentColor;
  -webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2v9M5.64 5.64a9 9 0 1 0 12.72 0' fill='none' stroke='%23000' stroke-width='2.8' stroke-linecap='round'/%3E%3C/svg%3E") center/contain no-repeat;
  mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2v9M5.64 5.64a9 9 0 1 0 12.72 0' fill='none' stroke='%23000' stroke-width='2.8' stroke-linecap='round'/%3E%3C/svg%3E") center/contain no-repeat; }
#power-start:disabled{ cursor:wait; background:#9f957d; color:#34383a;
  box-shadow:0 5px 0 #746c58, inset 0 3px 0 rgba(255,255,255,.14); }
#power-start:not(:disabled):hover{ filter:brightness(1.06); }
#power-start:not(:disabled):active{ transform:translateY(4px);
  box-shadow:0 1px 0 var(--kp-alt-edge), inset 0 3px 0 rgba(255,255,255,.2); }
/* The useful panel reaction: once POWER ON is committed, the switch itself
   lights before the service cover clears. This state deliberately overrides
   the generic disabled treatment used while stock is still loading. */
#power-gate[data-state="powering"] #power-start{
  transform:translateY(4px); background:#fff2a8; color:var(--line);
  filter:brightness(1.12); transition:none;
  box-shadow:0 1px 0 var(--kp-alt-edge), inset 0 3px 0 rgba(255,255,255,.82),
    0 0 12px rgba(255,196,77,.95), 0 0 30px rgba(255,196,77,.62); }
#power-start:focus-visible{ outline:3px solid var(--cream); outline-offset:4px; }
/* the gate auto-focuses POWER ON for the promised Enter path, but the
   programmatic focus must not paint the keyboard ring on a fresh load
   (Ben, 2026-08-09) — power.js drops the flag on the first real key */
#power-start[data-autofocus]:focus-visible{ outline:none; }
.power-hint{ margin-top:15px; color:#aeb9ba; font-size:9px; font-weight:900; letter-spacing:.14em; }
.power-screw{ position:absolute; z-index:2; width:8px; height:8px; border-radius:50%;
  background:#7b858a; border:1px solid var(--line); }
.power-screw::after{ content:""; position:absolute; left:1px; right:1px; top:3px;
  height:1px; background:var(--line); transform:rotate(-24deg); }
.power-screw.ps-tl{ left:9px; top:9px; }.power-screw.ps-tr{ right:9px; top:9px; }
.power-screw.ps-bl{ left:9px; bottom:9px; }.power-screw.ps-br{ right:9px; bottom:9px; }
body.awaiting-power{ overflow:hidden; }
/* The dim lives on #alcove and #station INDIVIDUALLY, never on #stage: a
   filter on #stage makes it the containing block for the fixed-position
   station, which resizes the backdrop to the stage's (taller) box and then
   snaps back to the viewport the moment the filter lifts — the background
   zoom flash Ben caught after POWER ON (2026-08-09, proven at 3 viewports).
   A filter on #station itself has no such effect: only an ANCESTOR's filter
   re-homes a fixed element. */
body.awaiting-power #alcove,body.awaiting-power #station{ filter:brightness(.14) saturate(.35); }
body.awaiting-power .shelf-lip::after,body.machine-powering .shelf-lip::after,
/* the marquee sign's bulbs (WS4): dark sockets until the ceremony's finale */
body.awaiting-power #marquee .bulb .lamp,body.machine-powering #marquee .bulb .lamp{ animation:none; fill:#8a6a1e; }
body.awaiting-power #marquee .bulb .halo,body.machine-powering #marquee .bulb .halo{ animation:none; opacity:0; }
/* THE COLD START (Ben, 2026-08-09: "it boots up too quickly. i would like the
   lights to flicker a bit before fully turning on"). A tired fluorescent
   ballast: two failed strikes, a half-hold, a surge, one last dip, then on.
   steps(1) so every change is a hard cut — a tube has no fade. The animation
   is scoped to .machine-powering alone (not chained to awaiting-power) so it
   plays out fully; power.js keeps the class alive until the sequence ends.
   SECOND BOOT PASS (Ben, 2026-08-09: "the lights on the vending machine to
   turn on even more slowly. dramatic effect... fancier effects instead of
   just row by row"): the rails now wake AFTER the room has fully caught,
   in a shuffled order — the gacha shelf first, the top row in the middle,
   row 2 last — and each rail plays its own little strike-fail-swell
   instead of popping on. The marquee is the finale: a neon buzz. The CRT
   (1.05s) stays earlier by design — electronics wake before tubes warm. */
body.machine-powering #alcove,body.machine-powering #station{
  animation:scene-power-on 2.05s steps(1) both; }
body.machine-powering .shelf-lip::after{ animation:rail-warmup 1.15s both; }
body.machine-powering .shelf[data-shelf="3"] .shelf-lip::after{ animation-delay:2.1s; }
body.machine-powering .shelf[data-shelf="5"] .shelf-lip::after{ animation-delay:2.45s; }
body.machine-powering .shelf[data-shelf="1"] .shelf-lip::after{ animation-delay:2.85s; }
body.machine-powering .shelf[data-shelf="4"] .shelf-lip::after{ animation-delay:3.25s; }
body.machine-powering .shelf[data-shelf="2"] .shelf-lip::after{ animation-delay:3.65s; }
body.machine-powering #marquee .bulb .lamp{ animation:mq-boot .9s steps(1) 4.45s both; }   /* the finale: every bulb strikes on together (mq-boot is the sign's own) */
@keyframes scene-power-on{
  0%{ filter:brightness(.14) saturate(.35); }
  6%{ filter:brightness(.5) saturate(.6); }     /* first strike */
  9%{ filter:brightness(.16) saturate(.38); }   /* dies */
  17%{ filter:brightness(.44) saturate(.55); }  /* second strike */
  21%{ filter:brightness(.19) saturate(.4); }   /* dies again */
  32%{ filter:brightness(.55) saturate(.65); }  /* catches, half-bright */
  38%{ filter:brightness(.28) saturate(.5); }   /* stutters */
  48%{ filter:brightness(.82) saturate(.88); }  /* the surge */
  56%{ filter:brightness(.6) saturate(.75); }   /* one last dip */
  66%{ filter:brightness(.95) saturate(.97); }
  78%{ filter:brightness(1) saturate(1); }
  100%{ filter:brightness(1) saturate(1); } }
/* a single rail's cold start: strike, die, second strike that holds dim,
   a sag, then the slow swell to full with one last breath on the way up.
   No steps() — the near-adjacent early frames read as hard flicker while
   the back half eases, which is what a warming tube actually does. */
@keyframes rail-warmup{
  0%{ opacity:0; }
  7%{ opacity:.5; }
  12%{ opacity:.04; }
  26%{ opacity:.38; }
  44%{ opacity:.26; }
  72%{ opacity:.85; }
  86%{ opacity:.72; }
  100%{ opacity:1; } }
/* the marquee's neon buzz: on-off-on-sag-on — steps(1), a sign has no fade */
@keyframes bulb-buzz{
  0%{ opacity:0; } 14%{ opacity:1; } 22%{ opacity:0; }
  38%{ opacity:1; } 46%{ opacity:.35; } 62%{ opacity:1; }
  76%{ opacity:.55; } 100%{ opacity:1; } }

/* THE WARM BOOT (award-view menu, 2026-08-09): a RETURNING visitor's tubes
   are still warm — one quick restrike and the room is on, rails in a fast
   overlapping cascade, marquee right behind, ~1.6s total. The full 5.2s
   ceremony only ever plays whole on a genuine first visit. Same physics,
   different cathode temperature. */
body.warm-boot.machine-powering #alcove,body.warm-boot.machine-powering #station{
  animation:scene-warm-on .7s steps(1) both; }
body.warm-boot.machine-powering .shelf-lip::after{ animation:rail-warmup .45s both; }
body.warm-boot.machine-powering .shelf[data-shelf="3"] .shelf-lip::after{ animation-delay:.7s; }
body.warm-boot.machine-powering .shelf[data-shelf="5"] .shelf-lip::after{ animation-delay:.78s; }
body.warm-boot.machine-powering .shelf[data-shelf="1"] .shelf-lip::after{ animation-delay:.86s; }
body.warm-boot.machine-powering .shelf[data-shelf="4"] .shelf-lip::after{ animation-delay:.94s; }
body.warm-boot.machine-powering .shelf[data-shelf="2"] .shelf-lip::after{ animation-delay:1.02s; }
body.warm-boot.machine-powering #marquee .bulb .lamp{ animation:mq-boot .5s steps(1) 1.2s both; }
@keyframes scene-warm-on{
  0%{ filter:brightness(.14) saturate(.35); }
  18%{ filter:brightness(.72) saturate(.8); }
  30%{ filter:brightness(.5) saturate(.7); }
  55%{ filter:brightness(1) saturate(1); }
  100%{ filter:brightness(1) saturate(1); } }

/* ERASURE POWER-DOWN. The VOID stamp lands before this class is applied.
   From there the cold boot runs backward: the last rail lit is the first one
   out, each shelf dies separately, the marquee and CRT follow, and only then
   does the room settle into persistent darkness. */
body.machine-shutting-down #stage,body.machine-off #stage{ pointer-events:none; }
body.machine-shutting-down #alcove,body.machine-shutting-down #station{
  animation:scene-power-off 5.25s steps(1) both; }
body.machine-shutting-down .shelf-lip::after{
  animation:rail-power-off .62s ease-in both; }
body.machine-shutting-down .shelf[data-shelf="2"] .shelf-lip::after{ animation-delay:.85s; }
body.machine-shutting-down .shelf[data-shelf="4"] .shelf-lip::after{ animation-delay:1.45s; }
body.machine-shutting-down .shelf[data-shelf="1"] .shelf-lip::after{ animation-delay:2.05s; }
body.machine-shutting-down .shelf[data-shelf="5"] .shelf-lip::after{ animation-delay:2.65s; }
body.machine-shutting-down .shelf[data-shelf="3"] .shelf-lip::after{ animation-delay:3.25s; }
body.machine-shutting-down #marquee .bulb .halo{ animation:none; opacity:0; }
body.machine-shutting-down #marquee .bulb .lamp{
  animation:bulb-power-off .52s steps(1) 4.02s both; }
body.machine-shutting-down .crt-screen{
  animation:crt-power-off .72s steps(1) 4.3s both; }
body.machine-shutting-down #screen-lines{
  animation:screen-power-off .56s linear 4.24s both; }

body.machine-off{ cursor:pointer; }
body.machine-off #alcove,body.machine-off #station{
  animation:none; filter:brightness(.14) saturate(.35); }
body.machine-off .shelf-lip::after{ animation:none; opacity:0; }
body.machine-off #marquee .bulb .lamp{ animation:none; fill:#8a6a1e; }
body.machine-off #marquee .bulb .halo{ animation:none; opacity:0; }
body.machine-off .crt-screen{
  animation:none; background:#040705; box-shadow:inset 0 0 28px rgba(0,0,0,.92); }
body.machine-off #screen-lines,body.machine-off .crt-scan{ opacity:0; }

@keyframes rail-power-off{
  0%{ opacity:1; }
  28%{ opacity:.48; }
  42%{ opacity:.92; }
  61%{ opacity:.16; }
  100%{ opacity:0; }
}
@keyframes bulb-power-off{
  0%{ fill:#ffc44d; } 24%{ fill:#8a6a1e; } 42%{ fill:#ffc44d; }
  64%,100%{ fill:#8a6a1e; }
}
@keyframes crt-power-off{
  0%{ background-color:var(--crt-bg); filter:brightness(1); }
  22%{ filter:brightness(.55); }
  38%{ filter:brightness(.9); }
  58%{ filter:brightness(.18); }
  100%{ background-color:#040705; filter:brightness(.05); box-shadow:inset 0 0 28px rgba(0,0,0,.92); }
}
@keyframes screen-power-off{
  0%{ opacity:1; transform:scaleY(1); }
  70%{ opacity:.28; transform:scaleY(.08); }
  100%{ opacity:0; transform:scaleY(0); }
}
@keyframes scene-power-off{
  0%,72%{ filter:brightness(1) saturate(1); }
  78%{ filter:brightness(.78) saturate(.86); }
  84%{ filter:brightness(.48) saturate(.66); }
  90%{ filter:brightness(.24) saturate(.46); }
  100%{ filter:brightness(.14) saturate(.35); }
}

/* ATTRACT MODE (award-view menu, 2026-08-09): the idle machine sells to the
   room. One quiet pass — rails swell bottom-to-top, one can catches a
   spotlight while the CRT names it (attract.js) — then stillness again.
   Deliberately gentler than any boot: the boot is a machine waking, this
   is a machine breathing. No bulb work; the marquee already twinkles. */
body.attract .shelf-lip::after{ animation:rail-drift 1.5s ease-in-out both; }
body.attract .shelf[data-shelf="5"] .shelf-lip::after{ animation-delay:.12s; }
body.attract .shelf[data-shelf="4"] .shelf-lip::after{ animation-delay:.24s; }
body.attract .shelf[data-shelf="3"] .shelf-lip::after{ animation-delay:.36s; }
body.attract .shelf[data-shelf="2"] .shelf-lip::after{ animation-delay:.48s; }
body.attract .shelf[data-shelf="1"] .shelf-lip::after{ animation-delay:.6s; }
@keyframes rail-drift{
  0%{ filter:brightness(1); }
  45%{ filter:brightness(1.8); }
  100%{ filter:brightness(1); } }
/* the spotlight can: a breathing pulse for as long as the pass holds it —
   one swell was too easy to miss against the CRT's dwell (Ben, 2026-08-10).
   Infinite while .attract-spot is on; attract.js holds the class for
   roughly the teaser's screen time, and any input strips it mid-breath. */
.can-btn.attract-spot img{ animation:can-spot 1.8s ease-in-out .4s infinite; }
@keyframes can-spot{
  0%,100%{ filter:brightness(1); }
  50%{ filter:brightness(1.5) drop-shadow(0 0 7px rgba(255,214,120,.9))
    drop-shadow(0 0 22px rgba(255,196,77,.6)); } }

/* A short landscape window cannot centre a 380px service cover without
   hiding both its header and switch. Compress the plate first, then let the
   gate itself scroll at truly tiny heights so POWER ON always remains reachable. */
@media (max-height:520px){
  #power-gate{ align-items:flex-start; overflow-y:auto; padding-block:10px; }
  .power-panel{ margin:auto 0; }
  .power-panel-head{ min-height:42px; padding-block:8px; }
  .power-panel-body{ padding:10px; }
  .power-console{ padding-top:14px; padding-bottom:10px; }
  #power-title{ font-size:32px; }
  .power-readouts{ margin-top:14px; }
  .power-meter{ margin-top:6px; margin-bottom:14px; }
  .power-hint{ margin-top:9px; }
}

/* ============================================================
   THE STATION (wave 16) — drawn in SVG (index.html).
   WS6 (2026-09-22): on desktop and tablets it is REGISTERED to the
   machine. Slice-scaling it to the viewport let its scale follow the
   screen's width while the machine's followed the height, so on wide
   screens the machine floated above the floor. machine.js now sizes the
   station from the machine's own scale and seats the feet on SVG y880;
   the drawing is oversize, so it still covers any screen. Only portrait
   phones keep the full-bleed slice (the station is slivers there).
   ============================================================ */
#station{ position:fixed; inset:0; width:100%; height:100%; z-index:0; transition:none; }
#station .st-decor{ pointer-events:none; }
#st-certificate-wrap{ cursor:pointer; outline:none; }
#st-certificate-wrap svg{ pointer-events:none; }
.privacy-plaque-hit{ pointer-events:all; stroke:var(--amber); stroke-width:0; }
#st-certificate-wrap:hover #st-certificate{ filter:brightness(1.12); }
#st-certificate-wrap:focus-visible .privacy-plaque-hit{ stroke-width:3; }
#st-exit,#st-recycle,#st-trash{ cursor:pointer; outline:none; }
#st-exit:hover image,#st-exit:focus-visible image{ filter:brightness(1.2); }
#st-exit:focus-visible image{ outline:2px solid var(--amber); outline-offset:4px; }
#st-exit-plate{ animation:none; }
#st-exit:hover #st-exit-plate{ animation:station-exit-flicker 1.8s linear infinite; }

#stage[inert] #st-exit-plate,body.machine-off #st-exit #st-exit-plate,
body.machine-shutting-down #st-exit #st-exit-plate{ animation:none; }
@keyframes station-exit-flicker{
  0%,66%,78%,100%{ filter:brightness(1.12); opacity:.96; }
  68%,70%{ filter:brightness(.72); opacity:.76; }
  72%,74%{ filter:brightness(1.03); opacity:.9; }
  76%{ filter:brightness(.86); opacity:.84; }
}
@media(prefers-reduced-motion:reduce){ #st-exit-plate,#st-exit:hover #st-exit-plate{ animation:none; } }
#st-recycle,#st-trash{ transform-box:fill-box; transform-origin:center bottom; }
#st-recycle image,#st-trash image{ pointer-events:none; }
.recycle-mouth{ stroke:var(--glow); stroke-width:3; stroke-dasharray:9 9; opacity:0; pointer-events:none; }
.recycle-ready .recycle-mouth,#st-recycle:focus-visible .recycle-mouth,#st-trash:focus-visible .recycle-mouth{ opacity:1; }
.recycle-flight{ position:fixed; left:0; top:0; z-index:65; pointer-events:none;
  transform-origin:0 0; }
.recycle-flight .gcapsule.opened img{ display:block; width:100%; height:100%; object-fit:contain; }
.recycle-point{ position:fixed; z-index:66; color:var(--cream); font-size:14px;
  font-weight:900; pointer-events:none; white-space:nowrap;
  text-shadow:0 1px 2px var(--line); animation:recycle-score .8s ease-out both; }
@keyframes recycle-score{
  0%{ opacity:0; transform:translate(-50%,0); }
  18%,55%{ opacity:1; }
  100%{ opacity:0; transform:translate(-50%,-24px); }
}
.station-sr-only{ position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip-path:inset(50%); white-space:nowrap; }
@media (prefers-reduced-motion:reduce){
  .recycle-point{ animation:none; transform:translate(-50%,-8px); }
}
/* WS4 (BT-14): the name board is the resume button */
#st-resume{ cursor:pointer; outline:none; }
#st-resume image{ transition:filter .18s; }
#st-resume:hover image,#st-resume:focus-visible image{ filter:brightness(1.12); }
#st-resume:focus-visible image{ filter:brightness(1.12) drop-shadow(0 0 3px #ffc44d) drop-shadow(0 0 7px rgba(255,196,77,.8)); }
@media (prefers-reduced-motion: reduce){ #st-resume image{ transition:none; } #st-resume:hover image,#st-resume:focus-visible image{ transform:none; } }
#station text{ font-family:"Trebuchet MS","Segoe UI",system-ui,sans-serif; }
/* Item 52 earns one restrained approval glint from the physical certificate.
   The art stays static at every other moment; this is product feedback.
   SVG transform boxes keep the sparkle scale local. */
.cert-sheen-track,.cert-spark{ opacity:0; pointer-events:none; }
#st-certificate-wrap.cert-approved .cert-sheen-track{
  animation:cert-sheen 1.05s cubic-bezier(.22,.6,.26,1) both; }
#st-certificate-wrap.cert-approved .cert-spark{
  transform-box:fill-box; transform-origin:center; animation:cert-spark .62s ease-out both; }
#st-certificate-wrap.cert-approved .cert-spark-b{ animation-delay:.18s; }
#st-certificate-wrap.cert-approved .cert-spark-c{ animation-delay:.36s; }
@keyframes cert-sheen{
  0%{ opacity:0; transform:translateX(0); }
  18%{ opacity:.78; }
  72%{ opacity:.62; }
  100%{ opacity:0; transform:translateX(220px); }
}
@keyframes cert-spark{
  0%{ opacity:0; transform:scale(.2) rotate(-20deg); }
  35%{ opacity:1; transform:scale(1.15) rotate(0); }
  100%{ opacity:0; transform:scale(.55) rotate(18deg); }
}
.st-name{ fill:#1b2340; font-size:38px; font-weight:700; text-anchor:middle; letter-spacing:.18em; }
.st-nameEn{ fill:#4a5578; font-size:15px; font-weight:700; text-anchor:middle; letter-spacing:.28em; }
.st-exit{ fill:#bfe8cf; font-size:19px; font-weight:900; text-anchor:middle; letter-spacing:.14em; }
.st-exitJp{ fill:#8fc3a5; font-size:12px; font-weight:700; text-anchor:middle; letter-spacing:.3em; }
/* (.st-door went with the service door, 2026-08-09) */

/* ---------- stage: author at 860x1064 (WS6), scale to fit ---------- */
/* margin:auto centering (not justify/align center): a flex-centered child
   that overflows gets its top clipped unreachably; auto margins stay scrollable */
#stage{ position:relative; width:100%; min-height:100%; display:flex; }
#alcove{
  transition:none; /* fitted coordinates must settle before station registration */
  position:relative; z-index:1; width:860px; height:1064px; flex:none; margin:auto;
  /* top-left origin: initScale centers via computed margins, so the visual
     never extends above/left of the scroll area (center-origin clipped the
     machine's bottom on short viewports once the stage became scrollable) */
  transform-origin:0 0;
  /* no wall of its own since wave 16 — the station behind it IS the wall */
}
/* the machine's contact shadow on the platform, in the alcove so it scales with
   the machine (WS6): a dark pool right under the cabinet and a short fall-off
   along its bottom edge */
#alcove::after{
  content:""; position:absolute; left:calc(50% - 350px); width:700px; top:998px; height:34px;
  pointer-events:none;
  background:radial-gradient(ellipse 52% 60% at 50% 40%, rgba(0,0,0,.62), rgba(0,0,0,.28) 55%, transparent 75%);
}
#alcove::before{
  content:""; position:absolute; left:calc(50% - 356px); width:712px; top:1006px; height:8px;
  pointer-events:none; background:linear-gradient(180deg, rgba(0,0,0,.6), rgba(0,0,0,0));
}

/* ---------- radio ---------- */
/* radio: drawn in SVG (index.html, from tools/art-gen.mjs radio(true)). ON THE
   FACE since 2026-09-22 (WS4 BT-16): the fifth card of the control column,
   under the receipt, a 52px card its chassis fills so the column's four gaps
   stay equal (align-content:space-between does the rest). It lived in the paper bay from 2026-08-06 until the
   library took the bay (WS5). */
#radio-face{ flex-basis:100%; height:52px; }
#radio{ display:block; width:100%; height:100%; overflow:visible; }
#radio .rd-label{ font:700 7.2px "Trebuchet MS",system-ui,sans-serif; letter-spacing:1.15px;
  fill:#dfe4ef; }
#radio #radio-display{ font-family:"Consolas","DejaVu Sans Mono",monospace; font-size:11.5px;
  fill:#5ee0c8; white-space:pre; }
#radio.on #radio-display{ fill:#7dfadf; }
#radio.on .rd-lamp{ fill:#ff5a5a; filter:drop-shadow(0 0 4px rgba(255,90,90,.95)); }
#radio-knob,#radio-power{ cursor:pointer; }
/* the browser's outline draws a RECTANGLE around an SVG group's bbox, which
   is the white box Ben saw on the dial. Suppress it and draw a round one. */
#radio-knob:focus,#radio-power:focus{ outline:none; }
.rd-focus{ fill:none; stroke:var(--cream); stroke-width:2; opacity:0; }
#radio-knob:focus-visible .rd-focus,#radio-power:focus-visible .rd-focus{ opacity:.9; }
#radio-knob{ transform-box:fill-box; transform-origin:center; transition:transform .2s; }
#rd-scroll{ transform:translateX(0); }
#radio.scroll #rd-scroll{ animation:rloop 9s linear infinite; }
/* The crawl itself. radio.js doubles the station text and sets --rd-shift to
   exactly half the doubled length, so translating by that much lands copy two
   where copy one began: a seamless loop with no visible jump.
   This keyframe did not exist, which made the animation above a silent no-op —
   CSS drops an animation whose name it cannot resolve without warning, so long
   station names simply clipped and nothing ever moved (Ben, 2026-08-06).
   bm-verify now asserts every animation-name referenced in the sheet resolves. */
@keyframes rloop{ to{ transform:translateX(var(--rd-shift, 0px)); } }

/* ---------- zoom control ----------
   Fixed to the VIEWPORT, never to the stage: it drives the stage's transform,
   so living inside it would mean growing with the thing it grows.
   It borrows the keypad's caps — cream, dark outline, a real 4px travel — so
   it reads as part of the machine rather than as browser furniture bolted on
   top of a diorama. It is still UI, and it still sits in the corner where UI
   belongs; the styling only decides whether it looks like it was invited. */
/* The complete seal, including its background disc, bounces together.
   Its shared site-brand hit area never moves or affects layout. */
#brand-mark.reacting svg{ animation:brand-bounce .48s ease-out; }
@keyframes brand-bounce{
  0%,100%{ transform:translateY(0) rotate(0); }
  30%{ transform:translateY(-7px) rotate(-9deg); }
  65%{ transform:translateY(2px) rotate(5deg); }
}
@media(max-width:560px){
  /* Reserve the shared mark plus a 16px gap, including the phone's safe area.
     Extra viewport height belongs below the cabinet, not below the mark. */
  body:not(.phone-full-view){ --brand-clearance:calc(var(--site-brand-top) + var(--site-brand-size) + 16px);
    padding-top:var(--brand-clearance); }
  body:not(.phone-full-view) #stage{
    min-height:calc(100svh - var(--brand-clearance)); }
}
@media(prefers-reduced-motion:reduce){
  #brand-mark.reacting svg{ animation:brand-press .22s linear; animation-duration:.22s !important; }
  @keyframes brand-press{ 0%,100%{ opacity:1; } 50%{ opacity:.55; } }
}

#zoomctl{ position:fixed; right:14px; bottom:14px; z-index:30;
  display:flex; align-items:stretch; gap:4px; padding:5px;
  background:linear-gradient(180deg, var(--dev-lite), var(--dev) 55%, var(--dev-deep));
  border:2px solid var(--line); border-radius:8px;
  box-shadow:0 3px 0 rgba(0,0,0,.34); }
#zoomctl[hidden]{ display:none; }
#zoomctl button{ min-width:30px; height:28px; padding:0 6px;
  font-family:"Trebuchet MS","Segoe UI",system-ui,sans-serif;
  font-size:15px; font-weight:900; line-height:1; color:var(--line);
  background:linear-gradient(180deg, #fff8e6, var(--kp) 62%, #dfcea8);
  border:2px solid var(--line); border-radius:5px;
  box-shadow:0 3px 0 var(--kp-edge), 0 5px 5px rgba(0,0,0,.3); }
#zoomctl button:active:not(:disabled){ transform:translateY(2px);
  box-shadow:0 1px 0 var(--kp-edge), 0 2px 3px rgba(0,0,0,.3); }
#zoomctl button:focus-visible{ outline:3px solid var(--cream); outline-offset:3px; }
/* the readout doubles as reset, so it is a key too — just a wider, quieter one */
#zoom-level{ font-size:11px; letter-spacing:.04em; min-width:46px; }
#zoomctl button:disabled{ opacity:.42; cursor:default;
  box-shadow:0 3px 0 var(--kp-edge); }
/* the machine is already scaled to fit and pinch-zoom is native there, so on a
   phone this would be a redundant control sitting on top of the content */
@media (max-width:560px){ #zoomctl{ display:none; } }

/* ---------- machine shell ---------- */
#machine{
  /* centered via margin, not transform — shake() owns the transform channel */
  /* This local stacking context keeps the structural layers predictable;
     overlays live outside it. */
  position:absolute; left:50%; top:58px; margin-left:-350px; z-index:2;   /* WS6: 14 lower than the bench's 44 (Ben's red box), 56 of platform under the cabinet */
  width:700px; height:950px;
  background:linear-gradient(180deg, var(--body-lite) 0%, var(--body) 30%, var(--body-deep) 100%);
  border:calc(var(--ol) + 1px) solid var(--line); border-radius:16px;
  box-shadow:
    inset 0 0 0 6px var(--dev),
    inset 0 0 0 9px var(--gold),
    inset -20px 0 26px rgba(0,0,0,.1),
    inset 20px 0 26px rgba(255,255,255,.09),
    inset 0 22px 20px -12px rgba(255,255,255,.14),
    26px 34px 60px rgba(0,0,0,.4);   /* the cast shadow, not a drawn side */
  display:flex; flex-direction:column;
}
/* THE CEL DEPTH CHEAT IS RETIRED (Ben, 2026-08-06). It drew an opaque
   extruded side plus two corner strokes to hint at the box behind the front
   face. His call to cut it, and the reasoning is the right one: you are
   standing at EYE LEVEL with this machine, so you would never see its
   underside. The bottom face was the half that broke the perspective, and
   once that goes the side alone is a rim too thin to read — especially on a
   phone, where it also cost 15px of chassis width.
   What remains is the soft cast shadow above and #alcove::after's pool on
   the platform: light falling on a flat-facing object, which is what a cel
   scene actually wants. Do not reintroduce a drawn side without solving the
   bottom, or it comes back as a floating dark bar under the machine. */
.m-feet{ position:absolute; left:0; right:0; bottom:-14px; display:flex; justify-content:space-between; padding:0 60px; }
.m-feet i{ width:70px; height:14px; background:var(--dev); border:var(--ol) solid var(--line);
  border-radius:0 0 6px 6px; }

/* marquee (WS4, BT-15): the lit sign. The 524x96 drawing carries a 12px rim for the
   bulbs riding the box's outline. Centred on the cabinet (the bench placed it from the
   marquee row's edge, which sat it 21px right of centre); its height is the one Ben
   approved on the bench, the lightbox's top 9px above the cabinet's top edge. The phone
   twin fills the phone's marquee row, its rim on the row's edges. */
#marquee{ position:relative; flex:none; height:64px; margin:16px 18px 6px; }
.mq-sign{ position:absolute; left:calc(50% - 262px); top:-40px; width:524px; height:96px; z-index:5; }
.mq-sign svg,.mq-sign-phone svg{ display:block; width:100%; height:100%; overflow:visible; }
.mq-sign-phone{ display:none; }

/* deck: glass left, notch right. Grid, not flex — #deck and #bottomrow
   share the same column template so takeout aligns exactly under the
   glass (flex floors each item's share at its padding+border and skews
   the split). The notch hugs its content — no dead panel below. */
#deck{ flex:1; display:grid; grid-template-columns:minmax(0,1.55fr) minmax(0,1fr);
  grid-template-rows:minmax(0,1fr); gap:14px; padding:8px 18px; min-height:0; }
#glasswrap{ display:flex; flex-direction:column; min-width:0; min-height:0; }
/* THE TWO COLUMNS ARE THE SAME HEIGHT (Ben, 2026-08-06). The notch used to
   hug its content, so the display case ran ~90px taller and the machine
   read lopsided. It stretches now and the leftover is distributed BETWEEN
   the device cards by align-content:space-between — which is the fix he
   asked for and also the honest one: the slack becomes even breathing room
   inside the control column instead of a hole at the bottom of it. The
   cards keep their own heights; only the gaps grow. */
#notch{ position:relative;
  display:flex; flex-direction:row; flex-wrap:wrap; gap:5px; min-width:0; min-height:0;
  align-self:stretch; align-content:space-between;
  background:
    linear-gradient(90deg, transparent 0 96%, rgba(255,255,255,.45) 97.5%, rgba(255,255,255,.1) 100%),
    linear-gradient(180deg, var(--panel-lite) 0%, var(--panel) 45%, var(--panel-deep) 100%);
  border:var(--ol) solid var(--line); border-radius:12px; padding:8px; }
#crt,#keypad,#payment,#printer{ flex-basis:100%; min-width:0; }

/* ---------- glass + shelves: Sol's deep-navy neon interior, drawn ---------- */
#glass{ position:relative; flex:1;
  background:
    radial-gradient(92% 46% at 50% 28%, rgba(255,196,77,.13), transparent 72%),
    linear-gradient(180deg, var(--interior) 0%, var(--interior-mid) 55%, var(--interior-deep) 100%);
  border:var(--ol) solid var(--line); border-radius:12px; overflow:hidden;
  box-shadow:inset 0 0 0 3px rgba(255,196,77,.45), inset 0 0 26px rgba(255,196,77,.2),
    inset 5px 18px 26px rgba(0,0,0,.32), inset 0 -12px 22px rgba(0,0,0,.28);
  display:flex; flex-direction:column; padding:10px 10px 6px; gap:6px; }
.glass-sheen{ position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(115deg, rgba(255,255,255,.15) 0%, rgba(255,255,255,.05) 11%,
    transparent 22%, transparent 64%, rgba(255,255,255,.05) 76%, transparent 84%,
    rgba(255,255,255,.08) 100%); }
.shelf{ position:relative; flex:1; display:flex; flex-direction:column; justify-content:flex-end; }
.shelf.gacha{ flex:.62; }
.shelf-glow{ display:none; }   /* wave 15: the light lives under the rail now */
.slots{ display:flex; align-items:flex-end; justify-content:space-around; padding:0 4px; }
/* The takeaway row's divider rides RIGHT (Ben, 2026-08-11, corrected
   mid-thread, then nudged back left the same evening): the cup's pocket sits
   at the row's end with honest air (can + 48px), the bento pocket takes the
   rest — the bento breathes without the cup reading pinned to the wall.
   The takeaway row is ROW 3 since the 2026-09-22 swap (WS2). */
#shelf-3 .slot:last-child{ flex:0 0 calc(var(--can-w) + 48px); }
/* ROW 4 (WS7, BT-19): three bays split the row into equal thirds like every
   other row, the pocket tabs on the row's edges and one between bays; the
   continuous mechanism (.mech-bed, inline SVG) runs behind them, under the
   slots and above the glass, stopping at the rail. */
#shelf-4{ gap:0; justify-content:stretch; position:relative; z-index:1; }
#shelf-4 .slot{ flex:1 1 0; }
.mech-bed{ position:absolute; left:0; right:0; top:0; bottom:13px; z-index:0; overflow:hidden; pointer-events:none; }
.mech-bed svg{ width:100%; height:100%; display:block; }
/* Row 5 is four equal pockets: the three requests and the notice (54, since
   2026-09-22). The row is split evenly, so each can has the same air to
   either divider, including the two outside edges (13.75px at 366). */
#shelf-5{ justify-content:center; gap:0; }
#shelf-5 .slot{ flex:1 1 0; }
/* the rack rail: metal front + the neon tube along its lower edge */
.shelf-lip{ position:relative; height:13px;
  background:linear-gradient(180deg, var(--rail-lite) 0%, var(--rail) 45%, var(--rail-deep) 100%);
  border:2px solid var(--line); border-radius:3px;
  box-shadow:inset 0 2px 0 rgba(255,232,180,.26), inset 0 -3px 0 rgba(0,0,0,.32); }
.shelf-lip::after{ content:''; position:absolute; left:4px; right:4px; bottom:-3px; height:3px;
  border-radius:2px; background:var(--glow);
  box-shadow:0 0 9px 2px rgba(255,196,77,.85), 0 2px 18px 6px rgba(255,196,77,.32); }
/* CES 09 — selection current. A single cold-white charge rides the existing
   rail to the selected pocket; it is created on demand and never loops. */
.selection-current-pulse{ position:absolute; z-index:4; left:4px; bottom:-4px;
  width:34px; height:5px; border-radius:999px; opacity:0; pointer-events:none;
  background:#d6f3f8;
  box-shadow:0 0 7px 2px rgba(214,243,248,.96), 0 0 18px 6px rgba(98,216,224,.55); }
.selection-current-pulse.running{
  animation:selection-current .82s ease-in-out both; }
@keyframes selection-current{
  0%{ opacity:0; transform:translateX(0) scaleX(.55); }
  12%{ opacity:1; }
  78%{ opacity:1; transform:translateX(var(--selection-travel)) scaleX(1); }
  100%{ opacity:0; transform:translateX(var(--selection-travel)) scaleX(.72); }
}
.can-btn.selection-answer,.gcap-btn.selection-answer{
  animation:selection-answer .34s ease .4s both; }
@keyframes selection-answer{
  0%,100%{ transform:translateY(0); filter:none; }
  48%{ transform:translateY(-7px); filter:brightness(1.18)
    drop-shadow(0 0 8px rgba(214,243,248,.9)); }
}

/* slot + can (flex, not fixed % — row 1 carries five since the Google split) */
.slot{ position:relative; display:flex; flex-direction:column; align-items:center; gap:4px; flex:1; min-width:0; }
/* pocket tabs, drawn PER SLOT — the dividers sit between the goods by
   construction, no registration to drift (the whole point of wave 15) */
.slot::before,.slot::after{ content:''; position:absolute; bottom:-2px; width:8px; height:18px;
  background:linear-gradient(180deg, #5b646f, #3b434d 80%);
  border:2px solid var(--line); border-bottom:none; border-radius:3px 3px 0 0;
  box-shadow:inset 0 2px 0 rgba(255,232,180,.24); }
.slot::before{ left:-5px; }
.slot::after{ right:-5px; display:none; }
.slot:last-child::after{ display:block; }
.slot .coil{ position:absolute; bottom:26px; left:50%; transform:translateX(-50%);
  width:8px; height:8px; opacity:0; }
.can-btn{ position:relative; display:block; transition:transform .15s; }
/* The lift is right, but it must come back down. On a touch screen `:hover`
   STICKS after a tap and only clears when something else is tapped, so the
   sold-out ramen rose and stayed up (Ben, 2026-08-06) — visible on that can
   precisely because nothing else happens when you tap it. Hover is now gated
   to devices that actually hover; touch gets the same lift on :active, which
   releases the moment the finger does. */
@media (hover:hover){ .can-btn:hover{ transform:translateY(-3px); } }
.can-btn:active{ transform:translateY(-3px); }
.can-btn:focus-visible{ outline:3px solid var(--cream); outline-offset:3px; border-radius:8px; }
/* the tag is stuck ON the glass; the can sits BEHIND it — so from the
   front the tag naturally overlaps the goods and the rail (Ben's read,
   2026-08-06). Out of flow, which is also what finally seats every can
   directly on its rail instead of hovering a tag's height above it. */
.slot-tag{ position:absolute; left:50%; bottom:-13px; transform:translateX(-50%); z-index:3;
  background:var(--cream); color:var(--line); border:2px solid var(--line); border-radius:4px;
  font-size:10px; font-weight:900; padding:0 6px; letter-spacing:.08em; white-space:nowrap; }
.slot.soldout .slot-tag{ background:var(--red); color:var(--cream); }
/* (.soldout-strip pruned 2026-08-09 — the drawn text strip predates the
   rendered .can-strip sprite and nothing has emitted it since wave 25) */

/* dormant expansion-bay treatment; no bays are exposed in the current rack */
.bay{ width:58px; height:76px; border:2px dashed rgba(255,196,77,.5); border-radius:10px;
  background:rgba(4,12,26,.35); box-shadow:inset 0 4px 10px rgba(0,0,0,.3); }

/* ---------- sticker gacha: one explicit, purpose-built capsule bay ---------- */
.gcap-btn{ display:block; transition:transform .15s; }
@media (hover:hover){ .gcap-btn:hover{ transform:translateY(-3px); } }
.gcap-btn:active{ transform:translateY(-3px); }   /* same sticky-hover fix */
.gcap-btn:focus-visible{ outline:3px solid var(--cream); outline-offset:3px; border-radius:50%; }
.gcapsule{ position:relative; width:69px; height:69px; }   /* 46 x 1.5 (Ben, round eight: Astra's shells at x1.5 on desktop) */
.gc-top,.gc-bot{ position:absolute; inset:0;
  transition:transform .4s cubic-bezier(.4,0,.6,1), opacity .4s; }
.gc-top{ isolation:isolate; }
.gc-whole{ position:absolute; inset:0; width:100%; height:100%; object-fit:contain; pointer-events:none; }
.gcapsule:not(.popped) > .gc-top,.gcapsule:not(.popped) > .gc-bot,
#gcap:not(.popped) > .gc-top,#gcap:not(.popped) > .gc-bot{ visibility:hidden; }
.popped > .gc-whole{ visibility:hidden; }
.gc-top .gc-art,.gc-bot img{ display:block; width:100%; height:100%; object-fit:contain; }
/* The whole masters carry 9 / 16 / 7 transparent bottom rows in 360px.
   Seat the painted foot on the rail, proportionally at every shelf size. */
.gcapsule[data-bay="41"]{ --shell-seat:2.5%; }
.gcapsule[data-bay="42"]{ --shell-seat:4.444444%; }
.gcapsule[data-bay="43"]{ --shell-seat:1.944444%; }
.gcap-btn .gcapsule{ filter:drop-shadow(0 4px 3px rgba(0,0,0,.35)); transform:translateY(var(--shell-seat)); }
#tray .gcapsule{ cursor:pointer; width:66px; height:66px; }   /* the item in the bin scales with the shelf (44 x 1.5) */
/* pointer-events:none matches .can-img: the CAPSULE is the drag target, the
   worn sticker just rides it — and no native image-drag can start from it */
#tray .gcapsule.opened img{ width:100%; height:100%; object-fit:contain; pointer-events:none; }
/* An opened POSTER stands a touch taller than a can (Ben, 2026-09-23: "as tall if not taller").
   Boxed into the capsule's square, the 2:3 sheet stood two thirds of a can high. It takes the
   can's height plus 6%, and the width its own shape gives it, on every screen (--can-h). */
#tray .gcapsule.opened[data-pool="posters"]{ height:calc(var(--can-h) * 1.06); width:calc(var(--can-h) * 1.06 * 2 / 3); }

/* the coin that pays the machine */
/* Returning a reparented coin changes coordinate systems; never tween its
   layout, including under the global reduced-motion duration override. */
.coin{ touch-action:none; transition:none; }
/* (.coin.flying pruned 2026-08-09 — no code has added the class since the
   gacha's coin path became coinIntoSlot) */
.coin.dragging{ position:fixed; z-index:70; cursor:grabbing; transition:none !important; }
#coincup .coin{ cursor:grab; }
#coinslot.drag-ok{ outline:3px dashed var(--glow); outline-offset:3px; }
/* BT-50 (Ben, 2026-09-24): a thumb hides a 22px coin, so a coin held by a
   finger is drawn bigger. ::before paints the same coin, scaled about the
   same centre; the .coin box itself (what the drag, the cup and the slot
   test measure) keeps its size, so hit areas do not change. It exists at
   rest, invisible, so the grow and the shrink both animate. Ben
   (2026-09-28): 2x while held, 3x over the coin slot. The 36px pixel coin
   keeps hard pixels at those sizes. */
.coin::before{ content:''; position:absolute; inset:0; border-radius:50%; background:inherit;
  opacity:0; transform:scale(1); pointer-events:none; image-rendering:pixelated;
  transition:transform .16s cubic-bezier(.2,.9,.3,1.25), opacity .16s ease-out; }
.coin.held::before{ opacity:1; transform:scale(2);
  filter:drop-shadow(0 5px 6px rgba(0,0,0,.45)); }
/* over the slot it grows again, and glows, because it now covers the slot's dashed outline */
.coin.held.over-slot::before{ transform:scale(3);
  filter:drop-shadow(0 0 5px var(--glow)) drop-shadow(0 5px 6px rgba(0,0,0,.45)); }
/* above the cup's front wall and its neighbours while it sits in the cup */
.coin.held:not(.dragging){ z-index:4; }
@media (prefers-reduced-motion: reduce){ .coin::before{ transition:none; } }

/* the capsule viewer */
#gacha{ position:fixed; inset:0; z-index:40; display:flex; align-items:center; justify-content:center;
  background:rgba(10,13,26,.82); backdrop-filter:blur(3px); }
#gacha[hidden]{ display:none; }
.gacha-card{ display:flex; flex-direction:column; align-items:center; gap:16px; }
/* The 200px floor exists so the stage does not collapse mid-pop while the two
   capsule halves are flying apart. It has no business surviving the pop: once
   the capsule is gone the sticker alone should set the height, and holding 200
   left dead air above a short sticker's name (Ben, 2026-08-06). Scope it to
   the state that needs it rather than deleting it and reintroducing the jump. */
#gacha-stage{ position:relative; width:min(320px,80vw); height:auto;
  display:flex; align-items:center; justify-content:center; }
#gacha-stage:has(#gcap:not([hidden])){ min-height:200px; }
#gcap{ position:relative; width:200px; height:200px; }
#gcap.popped .gc-top{ transform:translate(-52px,-96px) rotate(-28deg); opacity:.25; }
#gcap.popped .gc-bot{ transform:translate(44px,80px) rotate(18deg); opacity:.25; }
/* BT-52: the open cup. Its painted inside (rear rim, inner walls, floor) sits
   behind the original front plastic, clipped (gacha.js) to the area the closed
   lid covered, so no new pixel shows until the lid lifts. The art is built in a
   frame registered like the bay's shell, so it takes the shell's own box. The
   front is positioned too, so it paints above the inside by tree order. */
#gcap .gc-bot > img, #gcap .gc-inside, #gcap .gc-inside img{ position:absolute; inset:0; }
/* a 'whole' cup (42, 43) carries its own front, with Astra's rounded rim corners */
#gcap[data-inside="whole"] .gc-bot > img{ visibility:hidden; }
/* the cup stays visible while the lid lifts off; gacha.js reveals the prize
   as soon as the 420ms opening ends, with no extra hold or departure */
#gcap[data-inside].popped .gc-top{ transform:translate(-26px,-118px) rotate(-20deg); opacity:0;
  transition:transform .42s cubic-bezier(.2,.8,.3,1), opacity .3s .12s ease-in; }
#gcap[data-inside].popped .gc-bot{ transform:translateY(4px); opacity:1;
  transition:transform .32s cubic-bezier(.3,1.7,.5,1); }
#gacha-sticker-shell{ --tilt-x:0deg; --tilt-y:0deg; --tilt-scale:1;
  --tilt-shadow-x:0px; --tilt-shadow-y:10px;
  position:relative; display:grid; place-items:center; perspective:760px;
  /* touch-action on the sticker ONLY: a thumb-drag on it belongs to the
     hold, never to page scroll — everywhere else scroll wins as normal */
  cursor:grab; touch-action:none; }
#gacha-sticker-shell[hidden]{ display:none !important; }
#gacha-sticker-shell.is-tilting{ --tilt-scale:1.035; }
/* held: the grip deepens the lift a touch beyond hover's */
#gacha-sticker-shell.is-held{ --tilt-scale:1.06; cursor:grabbing; }
/* POSTERS AND COMICS ARE BIG PRIZES (Ben, 2026-09-22, a red box over the viewer:
   "the poster can be way larger than the allocated space"; comics too). The
   stage and the prize grow per bay: a poster stands about three quarters of the
   viewport tall, a comic panel reads wide, and both leave room below for the
   name and the buttons. A HEIGHT, not a max: a max only ever shrinks an image,
   and the prize has to grow to the box (width follows its own proportions;
   object-fit keeps an unexpected shape undistorted). Stickers keep their 320. */
#gacha[data-bay="42"] #gacha-stage{ width:min(600px, 88vw); }
#gacha[data-bay="42"] #gsticker{ width:auto; height:min(76vh, calc(100vh - 190px), calc(88vw / .7)); max-width:min(600px, 88vw); max-height:none; object-fit:contain; }
#gacha[data-bay="43"] #gacha-stage{ width:min(820px, 92vw); }
#gacha[data-bay="43"] #gsticker{ width:auto; height:min(64vh, calc(100vh - 200px), calc(92vw / 1.34)); max-width:min(820px, 92vw); max-height:none; object-fit:contain; }
#gsticker{ display:block; max-width:min(320px,80vw); max-height:min(320px,44vh);
  transform:rotateX(var(--tilt-x)) rotateY(var(--tilt-y)) scale(var(--tilt-scale));
  transform-style:preserve-3d; transform-origin:center;
  filter:drop-shadow(var(--tilt-shadow-x) var(--tilt-shadow-y) 24px rgba(0,0,0,.5));
  transition:transform .16s cubic-bezier(.2,.8,.2,1), filter .16s ease;
  will-change:transform,filter; pointer-events:none; }
#gsticker[hidden]{ display:none !important; }   /* hidden must actually hide */
#gacha-name{ color:var(--cream); font-weight:900; letter-spacing:.14em; font-size:13px; min-height:16px; }

/* the can: Sol's sprite in a fixed 58x88 frame (sprites share one canvas,
   so cup/short live inside the same frame) */
.can{ position:relative; width:var(--can-w); height:var(--can-h); }
.can-img{ display:block; width:100%; height:100%; object-fit:contain; pointer-events:none;
  filter:drop-shadow(0 4px 3px rgba(0,0,0,.35)); }
.can-strip{ position:absolute; inset:0; width:100%; height:100%; object-fit:contain; pointer-events:none; }
/* SEATING. A can rests on the rail when its INK touches it, not when its box
   does — and the box is what CSS lays out, so every sprite needs the distance
   between the two given back.
   MEASURED, not guessed (2026-08-06): every stock sprite is a 116x176 canvas
   and bottoms out at row 173. Wave 21 moved the cup noodle's ink and its
   registered SOLD OUT overlay down 17 source rows, so it now obeys the same
   contract instead of carrying a per-item correction.
     --seat-base  the 2 transparent rows every sprite carries (~1.2% of the
                  frame), which left every can on the machine hovering ~1px
                  over its rail;
     --seat       an explicitly reviewed optical correction only (Apple and
                  Assistant below), never compensation for a malformed source.
   PERCENTAGES, not px: the frame shrinks to 38, 34 and 27px wide down the
   mobile ladder, and a fixed 9.4px would bury the can at every one of them. A
   translate percentage resolves against the element's own height, so one
   number seats the sprite at every size — which is the point of deriving a
   value instead of typing one.
   bm-audit's RAIL check measures the rendered ink against the rail, so a
   wrong number here fails the gate instead of shipping. */
/* The sold-out strip is a second sprite registered to the same 116x176 canvas,
   so it has to take the identical translate or it slides off the can it bands. */
.can-img,.can-strip{ --seat:0%; transform:translateY(calc(1.2% + var(--seat))); }
/* 54's body is 78/174px versus the stock 85/172px proportion. Widen its
   painted sprite on shelf, tray and scanner; preserve height and rail seating. */
.can[data-art="54-privacy-notice"] .can-img{
  transform:translateY(calc(1.2% + var(--seat))) scaleX(1.102415);
}
/* The DynaTAC is physically much narrower than a can, but the source sprite
   compounded that honest silhouette with a lot of side air: its ink is only
   44/116px wide while the cans beside it occupy 83-85/116px. Enlarge the
   SHELF instance from its foot so the brick body reads as equal stock. The
   fixed 116x176 asset and row-173 seating contract remain untouched; only
   the antenna is allowed to rise into the spare headroom above the goods. */
.can[data-art="25-contact-me"] .can-img{
  transform-origin:50% 100%;
  transform:translateY(calc(1.2% + var(--seat))) scale(1.28);
}
/* Apple and Assistant share the same row-173 baseline as every other can, so
   this 1% is Ben's eye, not geometry: both cans taper to a narrower foot and
   read as touching down later. Kept because he asked for it and looked at it;
   flagged here so nobody later "fixes" it back and reopens the argument. */
.can[data-art="15-apple"] img,
.can[data-art="13-google-assistant"] img{ --seat:1%; }
/* THE BENTO (2026-08-10 reorg): a double-wide seat — Ben ruled it may take
   more space. Its 460x330 canvas carries 28 transparent rows under the ink
   (8.5% of frame height), so the seat gives that back the same way the
   116x176 cans give back their 2 rows. Measured from the ingest ink bbox
   (y29 h273), not guessed. */
/* Scaled UP again 2026-08-11 (Ben: the cup still sat awkward beside it).
   The first bump only widened the FRAME — .can's shared 97px height kept
   the contained 460x330 sprite at ~135px, so extra width was letterbox.
   The bento gets its own height now. 1.28 x can-h is measured, not chosen:
   the shelf offers 121.9 machine px from rail to the rail above, the ink
   tops out at 82.7% of the frame (8.8% transparent + the 8.5% seat ride),
   so 124px of frame leaves ~19px of air above the ink — the same order of
   breathing room its neighbours keep. Width stays ≥ 1.394 x height at every
   breakpoint so the sprite is always height-limited and the seat's
   percentage math keeps meaning what it says.
   1.28 was the first cut and it FAILED the equal-row gate: slots + lip at
   1.28 x can-h out-minimums the shelf's flex share, so shelf-2 silently
   grew and squeezed the gacha bay (the battery caught both). 1.18 is the
   true ceiling: 114.5 + the 13px lip = 127.5, inside the 128.9 row. */
.can[data-art="21-bento-box"]{ width:calc(var(--can-w) * 2.75); height:calc(var(--can-h) * 1.18); }
/* Keep the equal-row layout box at its proven ceiling, then enlarge only the
   painted bento from its seated foot. This uses the available head/side air
   without making row 2 steal height from its neighbours. */
.can[data-art="21-bento-box"] .can-img{
  /* The 1.15 scale magnifies the old seat offset too; 8.6% restores the
     painted foot to the shared rail baseline (audit-measured, not guessed). */
  --seat:8.6%; transform-origin:50% 100%;
  transform:translateY(calc(1.2% + var(--seat))) scale(1.15);
}
/* (the procedural can interior — .can-body/.can-lid/.can-deco/.can-brand/
   .can-kind and their cup/clear/unlabeled variants — was pruned 2026-08-09:
   canEl() has emitted sprite <img>s only since the cans became art, so none
   of those children have existed for waves. git remembers the drawing.) */

/* falling can + dropzone */
#dropzone{ position:absolute; inset:0; pointer-events:none; z-index:3; }
.falling{ position:absolute; z-index:4; }
/* The drop, with weight (2026-08-06). It used to be one linear-ish slide that
   stopped in mid-air and cut to the tray, which is what read as unconvincing.
   Now: accelerate under gravity, meet the floor of the well, squash on impact,
   one small bounce, settle. transform-origin at the foot so the squash pushes
   DOWN into the floor instead of shrinking about the middle. */
.can.falling{ transform-origin:bottom center;
  animation:candrop .7s linear forwards; }
@keyframes candrop{
  0%{ transform:translateY(0) rotate(0deg);
      animation-timing-function:cubic-bezier(.45,.02,.85,1); }        /* gravity */
  58%{ transform:translateY(var(--drop)) rotate(var(--spin)) scale(1,1); }
  /* HIT-STOP: ~2.5 frames frozen dead at contact before the squash. The eye
     registers the hit as weight instead of one continuous CG slide — the
     game-feel trick, applied to the one moment that owns it (2026-08-09).
     58% is unchanged, so vend.js's DROP_IMPACT sound stays frame-true. */
  63.5%{ transform:translateY(var(--drop)) rotate(var(--spin)) scale(1,1); }
  68%{ transform:translateY(var(--drop)) rotate(var(--spin)) scale(1.07,.9);
       animation-timing-function:cubic-bezier(.3,.7,.4,1); }          /* impact squash */
  80%{ transform:translateY(calc(var(--drop) - 15px)) rotate(calc(var(--spin) * 1.12)) scale(.97,1.04);
       animation-timing-function:cubic-bezier(.5,0,.9,1); }           /* the bounce */
  91%{ transform:translateY(var(--drop)) rotate(calc(var(--spin) * 1.04)) scale(1.03,.96); }
  100%{ transform:translateY(var(--drop)) rotate(calc(var(--spin) * 1.04)) scale(1,1); }
}
/* out through the floor into the chute, on the same beat as the flap */
.can.falling.chuted{ animation:none;
  transform:translateY(calc(var(--drop) + 46px)) rotate(var(--spin));
  opacity:0; transition:transform .22s cubic-bezier(.5,0,1,1), opacity .22s ease-in; }
/* The drop smear was CUT (Ben, 2026-08-06: "why is there a blue streak
   following the can when it drops? what is that?"). It was three soft
   pale-blue glow sprites played as a motion trail — and the comment on the
   focused-can animation forty lines down already says it: a cel-shaded object
   has no motion blur. A soft gradient behind a hard-inked can reads as a
   stray blue smudge, not speed.
   This is not "pending a better smear" — Ben's read is that less is more here
   and the effect was doing harm, so the drop is simply cleaner with nothing.
   The fall already sells itself with the spin, the chute and the thunk. The
   three fx/can-drop-smear-*.png files stay on disk, unreferenced, rather than
   being deleted; nothing needs to replace them. */

/* ---------- bottom row: takeout left, open paper bay right ---------- */
/* height is FIXED (matches the drawn takeout, geometry freeze 2026-08-04):
   the deck above it is flex:1, so any growth here would resize the glass and
   lift every can off its drawn rail. Nothing in here may push. */
#bottomrow{ flex:none; height:182px; display:grid;
  grid-template-columns:minmax(0,1.55fr) minmax(0,1fr);
  gap:14px; padding:0 18px; margin-bottom:20px; }
#takeout{ min-width:0; position:relative;
  background:linear-gradient(180deg, var(--dev-lite), var(--dev) 45%, var(--dev-deep));
  border:var(--ol) solid var(--line); border-radius:12px; padding:10px 12px 12px;
  box-shadow:var(--key-rim); }
/* The library SVG owns its frame. Retain the bay's geometry for the receipt
   and door, without painting the obsolete recess behind a second cabinet. */
#paperbay{ position:relative; min-width:0;
  border:var(--ol) solid transparent; border-radius:12px;
  perspective:520px; }
/* WS5 (BT-17): the bay is the lending library. The cabinet is SVG; the door is
   one real button over the glass, hinged on the RIGHT (transform-origin at the
   right edge), swinging OUT under the bay's 520px perspective (Ben: a 2D swing
   read as inward). Open enough to reach in, closed enough that the pane still
   reads as glass. z-index 7 keeps it under the receipt (#paper, 8), which still
   hangs over the bay. */
#library{ position:absolute; inset:5px; z-index:7; perspective:520px; }
#library svg{ width:100%; height:100%; display:block; }
/* the door covers the back panel (9 units in from each side of the 237-unit art); a cabinet
   library.js has resized sets --door-l and --door-w to its new width */
#library-door{ position:absolute; left:var(--door-l, 3.8%); top:5.4%; width:var(--door-w, 92.4%); height:89.3%; border-radius:4px; cursor:pointer; padding:0;
  background:linear-gradient(135deg, rgba(255,255,255,.16) 0%, rgba(255,255,255,.03) 45%, rgba(255,255,255,.1) 55%, rgba(255,255,255,.02) 100%);
  border:1.5px solid rgba(244,233,208,.45); box-shadow:inset 0 0 0 1px rgba(0,0,0,.25);
  transform-origin:100% 50%; transform:rotateY(0deg); transition:transform .6s cubic-bezier(.2,.7,.2,1); }
#library-door::before{ content:""; position:absolute; left:15.5%; top:0; width:6.4%; height:100%;
  background:rgba(255,255,255,.08); transform:skewX(-20deg); }
#library-door::after{ content:""; position:absolute; left:3.6%; top:42%; width:6px; height:16%; border-radius:3px;
  background:var(--cream); border:1.5px solid var(--line); }
#library.open #library-door{ transform:rotateY(62deg); box-shadow:-12px 0 24px rgba(0,0,0,.45); }
#library-door:focus-visible{ outline:3px solid var(--cream); outline-offset:3px; }
#library-volume-one{position:absolute;padding:0;border:0;background:transparent;cursor:pointer;touch-action:manipulation;z-index:1}
#library-volume-one[hidden]{display:none}
#library .volume-one{pointer-events:none}
#library.open:has(> #library-volume-one:not([aria-expanded="true"]):is(:hover,:focus-visible)) .volume-one{transform:translateY(-8px)}
#library-volume-one:focus-visible{outline:none}
/* A top-layer dialog escapes the machine's Fit transform. The same-origin
   child begins transparent so its book can leave the real shelf visibly. */
body.atlas-reading{overflow:hidden}
#atlas-modal{position:fixed;inset:0;width:100%;height:100%;max-width:none;max-height:none;margin:0;padding:0;border:0;background:transparent;color:var(--cream);overflow:hidden}
#atlas-modal::backdrop{background:transparent}
#atlas-frame{position:absolute;inset:0;width:100%;height:100%;border:0;background:transparent;visibility:hidden}
#atlas-modal[data-phase="opening"] #atlas-frame,#atlas-modal[data-phase="open"] #atlas-frame{visibility:visible}
#atlas-retry:hover{background:#35524e}
#atlas-retry:focus-visible{outline:2px solid #f2cc70;outline-offset:3px}
.atlas-notice{position:absolute;left:50%;bottom:32px;transform:translateX(-50%);max-width:min(90vw,440px);padding:0 20px;background:#1d3636;box-shadow:0 6px 22px #0005;border-radius:4px;text-align:center;font:13px var(--mono,monospace)}
.atlas-notice:has(#atlas-status[hidden]){display:none}
#atlas-status{margin:16px 0;line-height:1.5}
#atlas-retry{margin:0 0 16px;padding:10px 18px;min-height:44px;background:transparent;color:var(--cream);border:1px solid #b89856;font:inherit;cursor:pointer}
#atlas-modal [hidden]{display:none}
@media(max-width:600px){.atlas-notice{bottom:18px;font-size:11px}}
/* Mechanica is an independent book; the Atlas selectors and artwork stay intact. */
#library-volume-two{position:absolute;padding:0;border:0;background:transparent;cursor:pointer;touch-action:manipulation;z-index:1}
#library-volume-two[hidden]{display:none}
#library .volume-two{pointer-events:none}
#library.open:has(> #library-volume-two:not([aria-expanded="true"]):is(:hover,:focus-visible)) .volume-two{transform:translate(5.28px,-8px)}
#library-volume-two:focus-visible{outline:none}
body.mechanica-reading{overflow:hidden}
#mechanica-modal{position:fixed;inset:0;width:100%;height:100%;max-width:none;max-height:none;margin:0;padding:0;border:0;background:transparent;color:var(--cream);overflow:hidden}
#mechanica-modal::backdrop{background:transparent}
#mechanica-frame{position:absolute;inset:0;width:100%;height:100%;border:0;background:transparent;visibility:hidden}
#mechanica-modal[data-phase="opening"] #mechanica-frame,#mechanica-modal[data-phase="open"] #mechanica-frame{visibility:visible}
#mechanica-retry:hover{background:#35524e}
#mechanica-retry:focus-visible{outline:2px solid #f2cc70;outline-offset:3px}
.mechanica-notice{position:absolute;left:50%;bottom:32px;transform:translateX(-50%);max-width:min(90vw,440px);padding:0 20px;background:#1d3636;box-shadow:0 6px 22px #0005;border-radius:4px;text-align:center;font:13px var(--mono,monospace)}
.mechanica-notice:has(#mechanica-status[hidden]){display:none}
#mechanica-status{margin:16px 0;line-height:1.5}
#mechanica-retry{margin:0 0 16px;padding:10px 18px;min-height:44px;background:transparent;color:var(--cream);border:1px solid #b89856;font:inherit;cursor:pointer}
#mechanica-modal [hidden]{display:none}
@media(max-width:600px){.mechanica-notice{bottom:18px;font-size:11px}}
@media (prefers-reduced-motion: reduce){ #library-door, #library .spine{ transition:none; } }
/* filled, not outlined (design pass 2026-08-06): on a mid-tone body an
   outlined plate disappears — it has to be solid */
/* WS4: COLLECT ITEM (Ben: not every item is an experience; COLLECT YOUR ITEM until BT-41, when the
   phone's bin narrowed to the glass and "COLLECT ITEM ... works too") is a BACKLIT plate: dark
   at rest, lit amber while stock sits in the bin, brighter with a glow for two seconds
   on each landing (face.js keeps #takeout's has-item and lit classes) */
.to-plate{ text-align:center; background:#2a2718; color:#7d6a36; border:2px solid var(--line);
  border-radius:6px; font-size:12px; font-weight:900; letter-spacing:.18em; padding:3px 0; margin-bottom:8px;
  transition:background .25s, color .25s, box-shadow .25s; }
#takeout.has-item .to-plate{ background:#f2b73f; color:#241a02; box-shadow:0 0 8px rgba(255,196,77,.5); }
#takeout.lit .to-plate{ background:#ffc44d; color:#241a02;
  box-shadow:0 0 16px rgba(255,196,77,.9), inset 0 0 0 1px rgba(255,255,255,.3); }
.to-arrow{ display:inline-block; animation:toblink 1.6s steps(2) infinite; }
@keyframes toblink{ 50%{ opacity:.25; } }
/* WS8 (BT-20): ONE vend port (Ben, round four: "the flap and the hole below it don't
   make sense together"). The flap is the door of the bin: it covers the whole port,
   hinged at the top, dark at rest; it is pushed in on the landing beat and the stock
   props it open (#takeout.has-item, face.js) until the bin empties. A pushed-in door
   paints BEHIND the stock, which is what puts the stock inside the machine. The
   footprint is the old flap (52) plus the tray (70), so the geometry freeze holds. */
#port{ position:relative; height:122px; }
.port-cavity{ position:absolute; inset:0; z-index:0; border-radius:8px; background:#0a0d0c;
  box-shadow:inset 0 0 0 3px var(--line), inset 0 26px 30px rgba(0,0,0,.8),
    inset 12px 0 16px -10px rgba(255,255,255,.06), inset -12px 0 16px -10px rgba(0,0,0,.7); }
#flap{ position:absolute; inset:0; z-index:6; border-radius:8px; overflow:visible; perspective:420px; }
/* stock inside: the door drops to z-index 0, so the bin (z-index auto, after it in the DOM,
   and lifted to 12 only while an item is in hand, BEND-14) paints the stock in front of it */
#takeout.has-item #flap{ z-index:0; }
.flap-face{ position:absolute; inset:3px; background:#2a3034; border:2px solid #14181a;
  border-radius:6px; transform-origin:top center; cursor:pointer;
  /* the house spring (the coin landing's curve) on the swing; browsers
     without linear() keep the first, already-bouncy cubic */
  transition:transform .3s cubic-bezier(.6,0,.7,1.4);
  transition:transform .38s linear(0, .18 19%, .58 38%, .94 55%, 1.07 68%, 1.02 80%, .99 90%, 1);
  background-image:linear-gradient(180deg, rgba(255,255,255,.14), rgba(255,255,255,.04) 34%,
    transparent 46%, rgba(0,0,0,.18) 100%); }
/* wear: a palm-buff in the middle (every visitor pushes here) and can-slide
   scratches along the bottom edge — riding the flap so they swing with it */
.flap-face::after{ content:''; position:absolute; inset:0; border-radius:4px; pointer-events:none;
  background:
    radial-gradient(46% 42% at 50% 56%, rgba(255,255,255,.06), transparent 74%),
    linear-gradient(90deg, transparent 0 9%, rgba(0,0,0,.30) 9% 10.5%, transparent 10.5% 24%,
      rgba(255,255,255,.09) 24% 25%, transparent 25% 43%, rgba(0,0,0,.24) 43% 45%,
      transparent 45% 66%, rgba(255,255,255,.07) 66% 67.5%, transparent 67.5% 88%,
      rgba(0,0,0,.26) 88% 89.5%, transparent 89.5%);
  background-repeat:no-repeat, no-repeat;
  background-size:100% 100%, 100% 6px;
  background-position:0 0, 0 calc(100% - 2px); }
#flap.open .flap-face,#takeout.has-item #flap .flap-face,#takeout.peek #flap .flap-face{ transform:rotateX(-64deg); }
#takeout:not(.has-item):not(.peek) #flap:hover .flap-face{ transform:rotateX(-9deg); }
/* FIXED height, never content-driven: the tray used to grow when a can landed
   (cans are taller than the well), which shrank #deck and lifted every shelf
   off its drawn rail (Ben, 2026-08-04). Tray items scale to fit instead. */
#tray{ position:absolute; left:0; right:0; bottom:0; isolation:isolate; display:flex; gap:6px; align-items:flex-end;
  height:70px; padding:6px 9px 8px; overflow:visible;
  background:transparent; border-radius:8px; }   /* WS8: the port's cavity is the bin now; the tray keeps its floor and lip */
/* The tray is intentionally isolated so its floor, stock and retaining lip
   keep three stable planes at rest. A dragged item must temporarily lift that
   WHOLE context above the rack, though: a child's z-index cannot escape an
   isolated parent, which let the shelf number tags paint over stock in hand. */
#tray:has(> .tray-item-lifted){ z-index:12; }
/* Three hard cel planes: the narrow top edge is the rear of the well, the
   widening trapezoid is its floor, and the exposed surround becomes the side
   walls. The old full-width glare wedge made this read as one flat rectangle. */
#tray::before{ content:''; position:absolute; z-index:0; left:7px; right:7px;
  top:14px; bottom:4px; background:#111615;
  clip-path:polygon(12% 0, 88% 0, 100% 100%, 0 100%);
  box-shadow:inset 0 3px 0 var(--line); pointer-events:none; }
/* The retaining lip owns the foreground. Letting it cover the last few pixels
   of every dispensed object is the occlusion cue that places stock inside the
   cavity instead of pasting it onto the cavity's face. */
#tray::after{ content:''; position:absolute; z-index:4; left:5px; right:5px;
  bottom:2px; height:11px; background:var(--dev); border:2px solid var(--line);
  clip-path:polygon(1% 0, 99% 0, 96% 100%, 4% 100%);
  box-shadow:inset 0 2px 0 rgba(255,255,255,.13); pointer-events:none; }
#tray .can,#tray .gcapsule{ flex:none; }   /* a spree never squishes the tray */
/* cans stay FULL SIZE and overflow upward out of the well — the tray's box
   just never changes, so nothing above it can move (Ben: not an ant farm) */
#tray .can,#tray .gcapsule{ position:relative; z-index:2; }
#tray > .tray-item-lifted{ z-index:50; }
#tray .can,#tray .gcapsule{ cursor:grab; touch-action:none; }
#tray .tray-arriving{ transform-origin:bottom center;
  animation:tray-arrive .34s cubic-bezier(.22,.78,.3,1.18) both; }
@keyframes tray-arrive{
  0%{ transform:translateY(-25px) scale(.92); opacity:0; }
  16%{ opacity:1; }
  72%{ transform:translateY(2px) scale(1.025); opacity:1; }
  100%{ transform:translateY(0) scale(1); opacity:1; }
}
/* BT-32 (Ben's picks): a capsule falls out from under the flap into the bin
   (gacha.js animates the fall and the flap-edge clip), then five gold stars
   flash once around it on the contact frame and their DOM is removed. */
#tray .gcapsule.tray-dropping{ pointer-events:none; }
#tray .cap-sparkle{ position:absolute; inset:0; z-index:3; pointer-events:none; }
#tray .cap-star{ position:absolute; width:max(9px, 16.7%); aspect-ratio:1;
  margin:calc(max(9px, 16.7%) / -2) 0 0 calc(max(9px, 16.7%) / -2);
  background:var(--amber);
  clip-path:polygon(50% 0, 61.4% 38.6%, 100% 50%, 61.4% 61.4%, 50% 100%, 38.6% 61.4%, 0 50%, 38.6% 38.6%);
  animation:cap-star .54s ease-out both; }
@keyframes cap-star{
  0%{ transform:scale(0) rotate(0deg); opacity:1; }
  45%{ transform:scale(1.15) rotate(45deg); opacity:1; }
  100%{ transform:scale(0) rotate(90deg); opacity:0; }
}
@media (prefers-reduced-motion: reduce){ #tray .cap-sparkle{ display:none; } }
/* CES 07 — cold-vend release. Five blurred plumes bloom once around a newly
   landed can, then their DOM is removed. No decorative particle field runs
   while the machine is idle. */
.cold-vend-fx{ position:absolute; inset:0; z-index:1; overflow:visible;
  pointer-events:none; }
.vend-vapor{ position:absolute; left:50%; top:54%; width:34px; height:25px;
  border-radius:50%; opacity:0; filter:blur(4px); mix-blend-mode:screen;
  background:
    radial-gradient(circle at 31% 60%, rgba(236,252,252,.9) 0 21%, transparent 55%),
    radial-gradient(circle at 60% 38%, rgba(214,243,248,.86) 0 27%, transparent 62%),
    radial-gradient(ellipse at 50% 70%, rgba(162,224,231,.7) 0 30%, transparent 70%); }
.cold-vend-fx.running .vend-vapor{
  animation:cold-vapor 1.18s ease-out var(--vapor-delay) both; }
.cold-vend-active > .can-img{ position:relative; z-index:2; animation:cold-can 1.08s ease both; }
@keyframes cold-vapor{
  0%{ opacity:.96; transform:translate(-50%,-50%) scale(.5); }
  38%{ opacity:.78; }
  100%{ opacity:0; transform:translate(
    calc(-50% + var(--vapor-x)), calc(-50% + var(--vapor-y))) scale(2.65); }
}
@keyframes cold-can{
  0%,100%{ filter:drop-shadow(0 4px 3px rgba(0,0,0,.35)); }
  38%{ filter:brightness(1.1) drop-shadow(0 0 7px rgba(214,243,248,.62)); }
}
#tray .can:active{ cursor:grabbing; }
/* The blue box Ben saw while dragging (2026-08-06). A tabbable div gets the
   UA's own focus ring on pointerdown and, because a drag is a pointer gesture
   across the page, the browser also paints a text selection over whatever it
   crosses. Kill both, then give keyboard users the house ring back — the same
   3px cream at 3px offset every other focusable object on the machine wears. */
#tray .can,#tray .gcapsule{ user-select:none; -webkit-user-select:none; }
#tray .can:focus,#tray .gcapsule:focus{ outline:none; }
#tray .can:focus-visible,#tray .gcapsule:focus-visible{
  outline:3px solid var(--cream); outline-offset:3px; border-radius:8px; }

/* ---------- notch devices ---------- */
/* device family look: navy plate, top light, mint rim (Sol's cluster language) */
/* CRT — the bezel carries a control chin under the screen, like a real set */
#crt{ position:relative; }
/* The refusal light (Ben, 2026-09-23): a small red lamp under the screen, beside the speaker. Dark
   until a selection is refused, then red for a beat (vend.js refuse). A held class, not a keyframe,
   so reduced motion still sees it. */
#crt-led{ position:absolute; right:65px; bottom:12px; z-index:2; width:7px; height:7px; border-radius:50%;
  background:#3a1616; box-shadow:inset 0 1px 1.5px rgba(0,0,0,.7), 0 0 0 1px rgba(0,0,0,.35);
  transition:background .35s ease, box-shadow .35s ease; pointer-events:none; }
#crt-led.refused{ background:#ff3b30; transition:none;
  box-shadow:0 0 6px 1px rgba(255,59,48,.9), 0 0 0 1px rgba(0,0,0,.35); }
.crt-bezel{ position:relative;
  background:linear-gradient(180deg, var(--dev-lite), var(--dev) 55%, var(--dev-deep));
  border:var(--ol) solid var(--line); border-radius:12px; padding:10px 10px 26px;
  box-shadow:0 0 0 2.5px rgba(255,196,77,.3), var(--key-drop), var(--key-rim); }
/* THE MOULDED-LABEL FAMILY (Webby item 9, 2026-08-06). BENDING SYSTEMS,
   RECEIPT and COIN RETURN are the same kind of object — text moulded into
   the machine's plastic — so they share one size (10px), one colour and the
   same engraved relief. The chin was 8px of near-background #57648f and
   unreadable; it joins the family instead of whispering under it. Tracking
   stays per-element (COIN RETURN has a button to fit inside). TEAR HERE,
   the fourth member, died with the paper bay's lip. The battery pins the
   family; --mold is the single source for its colour. */
:root{ --mold:#aab3cc; }
.crt-bezel::after{ /* molded maker's mark on the chin — BENDING, with the
  hyphen, matching the maker's plate (Ben, 2026-08-06) */
  content:'BENDING SYSTEMS'; position:absolute; left:15px; bottom:7px;
  font-size:10px; font-weight:900; letter-spacing:.22em; color:var(--mold);
  text-shadow:0 -1px 0 rgba(0,0,0,.5), 0 1px 0 rgba(255,255,255,.06); }
/* THE CRT'S FONT (WS1, Ben: Inconsolata). One face on every machine, shipped
   with the site: a static instance of the variable font at weight 500,
   width 110 (Consolas's advance, so the desktop grid is what the copy was
   measured on), subset to the CRT's characters, 10 KB. The phone gets the
   width-100 cut so the same 21 letters fit its narrower glass. The emoji and
   the (ㆆ_ㆆ) emoticon fall through to the device's own font on purpose. */
@font-face{ font-family:"Inconsolata CRT"; font-weight:500; font-display:block;
  src:url("../assets/fonts/inconsolata-crt.woff2") format("woff2"); }
@font-face{ font-family:"Inconsolata CRT Narrow"; font-weight:500; font-display:block;
  src:url("../assets/fonts/inconsolata-crt-narrow.woff2") format("woff2"); }
/* THE GRID (WS1): five rows maximum, the block vertically centred in the
   glass. Normal (1 to 3 rows) 15px on 21px rows, 21 letters a row; dense (4 or
   5 rows) 12.5px on 16.25px rows, 25 letters. Line heights are px, never
   unitless, so the row count cannot drift with the font. The glass is 108
   tall: 84 of content, 63 for three normal rows, 81.25 for five dense. */
.crt-screen{ position:relative; height:108px; border-radius:10px; display:flex; align-items:center;
  background:radial-gradient(130% 130% at 50% 42%, #12281c 0%, var(--crt-bg) 62%, #081209 100%);
  border:2px solid #000; overflow:hidden; padding:10px 12px;
  box-shadow:inset 0 0 28px rgba(0,0,0,.6), inset 0 0 6px rgba(77,255,157,.12),
    0 0 22px rgba(77,255,157,.22); }   /* the phosphor bloom, drawn (was screen-glow.png) */
#screen-lines{ width:100%; font-family:"Inconsolata CRT","Consolas","DejaVu Sans Mono",monospace; font-weight:500;
  font-size:15px; line-height:21px; color:var(--crt-fg);
  text-shadow:0 0 6px rgba(77,255,157,.55); white-space:pre-wrap;
  transition:opacity .13s linear; }
#screen-lines.dense{ font-size:12.5px; line-height:16.25px; }
#screen-lines.swap{ opacity:.18; }   /* dim-out instead of blink-to-black */
/* PRE-LAID-OUT TYPING (screen.js): the untyped remainder is in the flow but
   unseen, so a word can never jump rows mid-crawl. The cursor is the next
   letter's cell while typing (.next) and a real node at rest (.cursor); both
   are one cell wide and one letter tall at every font size the glass uses.
   .cursor gives its width straight back (margin-right:-1ch): it advances
   nothing, so on a full last row it overhangs into the glass's padding
   instead of claiming a row of its own and shoving the message up. */
#screen-lines .rest{ visibility:hidden; }
#screen-lines .next{ color:transparent; text-shadow:none;
  background:linear-gradient(var(--crt-fg),var(--crt-fg)) 0 100% / 100% .97em no-repeat;
  animation:blink 1.1s steps(1) infinite; }
#screen-lines .cursor{ display:inline-block; width:1ch; height:.97em; margin-right:-1ch; background:var(--crt-fg);
  animation:blink 1.1s steps(1) infinite; vertical-align:text-bottom; }
@keyframes blink{ 50%{ opacity:0; } }
.crt-scan{ position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(115deg, rgba(255,255,255,.055) 0%, transparent 26%),
    repeating-linear-gradient(180deg, rgba(0,0,0,.18) 0 1px, transparent 1px 3px); }
.crt-screen.glitch{ animation:glitch .5s steps(3) infinite; }
@keyframes glitch{
  0%{ transform:translate(0); filter:none; }
  33%{ transform:translate(-2px,1px) skewX(2deg); filter:hue-rotate(40deg); }
  66%{ transform:translate(2px,-1px) skewX(-1.5deg); filter:invert(.08); } }
/* the volume toggle: set INTO the bezel's chin — part of the machine, not a
   pill floating over the screen (Ben, 2026-08-04). The speaker is DRAWN,
   an icon not an acronym (design pass 2026-08-06): amber cone + two arcs
   when live, grey cone + a cross when muted. The pill is a FIXED 48x23 so
   muting can never resize the button. */
.snd{ position:absolute; right:11px; bottom:4px; z-index:2; width:48px; height:23px;
  background:#20262b; border:2px solid var(--line); border-radius:6px;
  box-shadow:inset 0 2px 4px rgba(0,0,0,.55), 0 1px 0 rgba(255,255,255,.06); }
.snd i{ position:absolute; top:50%; pointer-events:none; }
.snd-cone{ left:15px; margin-top:-5px; width:0; height:0; border-style:solid;
  border-color:transparent #6d757d transparent transparent; border-width:5px 6px 5px 0; }
.snd-w{ border:1.5px solid #6d757d; border-left:none; }
.snd-w1{ left:24px; margin-top:-3.5px; width:3px; height:7px; border-radius:0 5px 5px 0; }
.snd-w2{ left:27px; margin-top:-5.5px; width:5px; height:11px; border-radius:0 8px 8px 0; opacity:.6; }
.snd-x{ left:25px; margin-top:-4.5px; width:9px; height:9px; }
.snd-x::before,.snd-x::after{ content:''; position:absolute; left:-1px; right:-1px; top:50%;
  margin-top:-1px; height:1.5px; background:#6d757d; }
.snd-x::before{ transform:rotate(45deg); } .snd-x::after{ transform:rotate(-45deg); }
.snd.on .snd-cone{ border-right-color:var(--glow); }
.snd.on .snd-w{ border-color:var(--glow); border-left:none; }
.snd.on .snd-x{ display:none; }
.snd:not(.on) .snd-w{ display:none; }

/* keypad */
#keypad{ background:linear-gradient(180deg, var(--dev-lite), var(--dev) 55%, var(--dev-deep));
  border:var(--ol) solid var(--line); border-radius:12px; padding:10px 12px;
  box-shadow:0 0 0 2.5px rgba(255,196,77,.3), var(--key-drop), var(--key-rim); }
.kp-row{ display:flex; gap:8px; justify-content:center; margin-bottom:8px; }
.kp{ width:52px; height:40px; color:var(--line);
  background:var(--kp) linear-gradient(180deg, rgba(255,255,255,.42) 0%, rgba(255,255,255,.12) 40%,
    transparent 52%, rgba(0,0,0,.06) 100%);
  border:var(--ol) solid var(--line); border-radius:9px; font-size:19px; font-weight:900;
  box-shadow:0 4px 0 var(--kp-edge), 2px 6px 6px rgba(0,0,0,.3);
  transition:transform .05s, box-shadow .05s; }
.kp:active,.kp.pressed,.kp.touch-preview{ transform:translateY(3px);
  box-shadow:0 1px 0 var(--kp-edge), 0 2px 3px rgba(0,0,0,.3); }
.kp.alt{ background:var(--kp-alt) linear-gradient(180deg, rgba(255,255,255,.42) 0%, rgba(255,255,255,.12) 40%,
    transparent 52%, rgba(0,0,0,.06) 100%);
  box-shadow:0 4px 0 var(--kp-alt-edge), 2px 6px 6px rgba(0,0,0,.3); }
.kp.alt:active,.kp.alt.pressed,.kp.alt.touch-preview{ box-shadow:0 1px 0 var(--kp-alt-edge), 0 2px 3px rgba(0,0,0,.3); }
/* The keypad and payment buttons had no designed focus ring — the machine's
   primary input surface was invisible to a keyboard user. Give them the same
   cream ring the cans, tray and zoom controls already wear, offset outward so
   it reads on the dark keypad/payment graphite. Buttons only; the draggable
   cans and the drop-zone slots keep their own treatment (see F5). */
.kp:focus-visible{ outline:3px solid var(--cream); outline-offset:3px; border-radius:12px; }
#cardreader:focus-visible,#billslot:focus-visible,#coinreturn:focus-visible,
.snd:focus-visible{ outline:3px solid var(--cream); outline-offset:3px; }
.kp-row.caps{ margin-bottom:0; }
.kp-cell{ display:flex; flex-direction:column; align-items:center; gap:6px; }

/* (the frequency-true key grime shipped 2026-08-10 and Ben removed it the
   same day: realistic, but it read as a dirty UI, not a loved machine. The
   flap's can-slide scratches survive — mechanical wear, not grime.) */
/* #bcc7dd, up from #8fa0c8: the only labels naming CLEAR and ENTER measured
   APCA Lc −45 at 10px/900 — well under what tiny bold caps need. Same
   blue-grey family, now Lc −66 (2026-08-09 contrast pass). */
.kp-cap{ height:12px; font-size:10px; font-weight:900; letter-spacing:.16em; color:#bcc7dd; }

/* payment panel: a strict grid so the columns line up. Row logic is
   physical (Ben, 2026-08-04): scan card LEFT, COIN RETURN middle (two
   lines allowed), and the coin catch RIGHT — directly under the slot
   the coin goes into, because that is where a catch lives */
/* coin slot narrowed 16px -> 10px (design pass 2026-08-06): at 16 it took
   a candy bar */
#payment{ display:grid; grid-template-columns:1.15fr 1fr 46px 10px; grid-template-rows:auto auto;
  gap:8px 10px; align-items:stretch;
  background:linear-gradient(180deg, var(--dev-lite), var(--dev) 55%, var(--dev-deep));
  border:var(--ol) solid var(--line); border-radius:10px; padding:10px 12px;
  box-shadow:0 0 0 2.5px rgba(255,196,77,.3), var(--key-drop), var(--key-rim); }
/* Row 1: card | bill | coin (item 17), and the two slots are EQUAL by
   construction (Ben, 2026-08-06) — one flex wrapper, flex:1 each, no grid
   coincidence to drift. The wrapper spans the grid columns; row 2 below
   keeps its own alignment and owes this row nothing. */
#payslots{ grid-column:1 / 4; grid-row:1; display:flex; gap:10px; }
#cardreader{ position:relative; flex:1 1 0; height:28px;
  background:linear-gradient(180deg, #565f6a, var(--dev-slot) 70%, #39414b); border:2px solid var(--line);
  border-radius:4px; }
#billslot{ flex:1 1 0; height:28px;
  background:linear-gradient(180deg, #565f6a, var(--dev-slot) 70%, #39414b); border:2px solid var(--line);
  border-radius:4px; display:flex; align-items:center; justify-content:center; }
#billslot i{ display:block; width:78%; height:6px; background:#000; border-radius:2px;
  box-shadow:inset -1px -2px 0 rgba(255,255,255,.1); }
.cr-slot{ position:absolute; left:5px; right:5px; top:5px; height:5px; background:#000; border-radius:2px; }
.cr-led{ position:absolute; right:4px; bottom:4px; width:5px; height:5px; border-radius:50%;
  background:#2ea84f; box-shadow:0 0 5px rgba(46,168,79,.9); }
#cardreader.declined .cr-led{ background:#ff3b30; box-shadow:0 0 6px rgba(255,59,48,.95); }
#coinslot{ grid-column:4; grid-row:1; background:#000; border:2px solid #cdd4e2;
  border-radius:3px; box-shadow:inset 1.5px 3px 5px rgba(0,0,0,.9), inset 0 0 0 1px rgba(255,255,255,.07);
  position:relative; overflow:hidden; }
/* the paid coin: a brief glint INSIDE the slot, never outside the machine.
   It is drawn at the FULL 26px of a cup coin and the 10px slot clips it to a
   central band — same object, seen through a narrow mouth. Drawn at 12px to
   fit the slot it read as a different, tinier coin than the one you just
   dragged in, which is the thing Ben caught (2026-08-06). Sizing it to the
   opening was solving the slot's problem, not the coin's. */
.coin.in-slot{ position:absolute; width:26px; height:26px; left:50%; margin-left:-13px;
  top:-28px; transition:top .34s cubic-bezier(.5,0,1,1); }
#coinreturn{ grid-column:2; grid-row:2;
  background:linear-gradient(180deg, #565f6a, var(--dev-slot) 70%, #39414b); color:var(--mold);
  border:2px solid var(--line); border-radius:6px; font-size:10px; font-weight:900;
  letter-spacing:.08em; padding:6px; line-height:1.3;
  text-shadow:0 -1px 0 rgba(0,0,0,.5), 0 1px 0 rgba(255,255,255,.06);
  box-shadow:1px 2px 0 rgba(0,0,0,.35); }
#coinreturn:active{ transform:translateY(2px); box-shadow:0 0 0 rgba(0,0,0,.35); }
#coincup{ grid-column:3 / 5; grid-row:2; position:relative; min-height:30px; background:#12172b;
  border:2px solid var(--line); border-radius:0 0 8px 8px; overflow:visible;
  box-shadow:inset 3px 5px 9px rgba(0,0,0,.55), inset -1px -1px 0 rgba(255,255,255,.05); }
/* Cup/rim touches pick up a coin too. Declare this before contact so the
   browser keeps that gesture as a drag; an empty cup remains scrollable. */
#coincup:has(.coin){ touch-action:none; }
/* the cup's FRONT WALL (Webby item 10): a real catch has depth, so the last
   few pixels of a seated coin disappear behind it. Drawn as ::after, which
   paints above the coin children by tree order — no z-index needed. Kept
   short (8px) so the coins stay easy to grab by their upper half. */
/* the MOUTH of the return chute: a slot across the back wall near the rim.
   The coin slides out of it (machine.js clips the coin to the mouth line as
   it emerges), so a returned coin comes from inside the machine, never from
   above it (Ben, 2026-09-21). ::before paints under the coin children. */
#coincup::before{ content:''; position:absolute; left:5px; right:5px; top:2px; height:5px;
  background:#04060e; border-radius:0 0 3px 3px;
  box-shadow:0 1px 0 rgba(255,255,255,.07), inset 0 1px 0 rgba(0,0,0,.8); }
#coincup::after{ content:''; position:absolute; left:0; right:0; bottom:0; height:8px;
  background:linear-gradient(180deg, rgba(255,255,255,.07), #1a2036 45%, #0c101f);
  border-top:2px solid var(--line); border-radius:0 0 6px 6px; }
.coin{ position:absolute; width:26px; height:26px; border-radius:50%;
  background:url("../assets/art/production/fx/coin.png") center/cover no-repeat; }

/* printer */
#printer{ background:linear-gradient(180deg, var(--dev-lite), var(--dev) 55%, var(--dev-deep));
  border:var(--ol) solid var(--line); border-radius:12px; padding:8px 12px;
  box-shadow:0 0 0 2.5px rgba(255,196,77,.3), var(--key-drop), var(--key-rim); }
.pr-label{ display:flex;align-items:center;justify-content:space-between;gap:8px;color:var(--mold); font-size:10px; font-weight:900; letter-spacing:.22em; margin-bottom:6px;
  text-shadow:0 -1px 0 rgba(0,0,0,.5), 0 1px 0 rgba(255,255,255,.06); }
.pr-slot{ position:relative; height:14px; background:#000; border:2px solid var(--line); border-radius:4px;
  box-shadow:inset 2px 3px 6px rgba(0,0,0,.85), inset -1px -1px 0 rgba(255,255,255,.07); }
@font-face{font-family:Receipt;font-weight:400;src:url('../assets/fonts/receipt-regular.woff2') format('woff2');font-display:swap}
@font-face{font-family:Receipt;font-weight:700;src:url('../assets/fonts/receipt-bold.woff2') format('woff2');font-display:swap}
#receipt-read,#receipt-tear{display:block;background:none;border:0;padding:0;font:inherit;color:inherit;letter-spacing:inherit;text-shadow:inherit;cursor:pointer}
#receipt-read{cursor:zoom-in}
#receipt-read:disabled{cursor:default;opacity:1}
/* Same inset charcoal key as the CRT mute control, sized for a text label. */
#receipt-tear{display:inline-flex;align-items:center;justify-content:center;flex:none;gap:5px;
  height:23px;padding:0 7px;background:#20262b;border:2px solid var(--line);border-radius:6px;
  box-shadow:inset 0 2px 4px rgba(0,0,0,.55),0 1px 0 rgba(255,255,255,.06);
  color:var(--cream);font-size:9px;letter-spacing:1px;white-space:nowrap}
#receipt-tear .receipt-tear-icon{display:block;flex:none;width:12px;height:12px;background:currentColor;
  -webkit-mask:url('../assets/icons/receipt/scissors.svg') center/contain no-repeat;
  mask:url('../assets/icons/receipt/scissors.svg') center/contain no-repeat;pointer-events:none}
#receipt-tear:not(:disabled) .receipt-tear-icon{color:var(--glow)}
#receipt-tear:hover:not(:disabled){background:#293238}
#receipt-tear:active:not(:disabled){box-shadow:inset 0 3px 6px rgba(0,0,0,.7);transform:translateY(1px)}
#receipt-tear:disabled{cursor:default;color:#6d757d}
#receipt-read:focus-visible,#receipt-tear:focus-visible{outline:3px solid var(--cream);outline-offset:3px}
#paper{position:absolute;left:6px;right:6px;top:10px;max-height:0;z-index:8;
  background:linear-gradient(90deg,#ececde 0,#fbf9f0 2%,#fffdf5 48%,#f7f5e9 96%,#ecebdd);
  border:0;border-radius:0;box-shadow:3px 8px 8px #203d343b;
  color:#383b33;font:10.5px/1.32 Receipt,Consolas,monospace;font-synthesis:none;
  -webkit-text-size-adjust:100%;text-size-adjust:100%;transition:max-height .18s linear}
#paper::before{content:'';position:absolute;inset:0;pointer-events:none;opacity:.12;
  background-image:radial-gradient(#5b614b .45px,transparent .8px);background-size:4px 3px;mix-blend-mode:multiply}
#paper:not(:empty)::after{content:'';position:absolute;left:0;right:0;top:100%;height:7px;
  background:linear-gradient(135deg,#f8f6eb 5px,transparent 0) 0 0/10px 10px,
  linear-gradient(225deg,#f8f6eb 5px,transparent 0) 0 0/10px 10px;pointer-events:none}
#paper.readable{cursor:zoom-in}
#paper.printing{transition:max-height .15s steps(3,end)}
#paper .thermal-ink{opacity:1;position:relative}
#paper.printing .thermal-ink{animation:thermal-ink .13s steps(3,end) .085s both}
@keyframes thermal-ink{0%{opacity:0;filter:blur(.35px)}45%{opacity:.38}100%{opacity:1;filter:none}}
#paper .p-inner{display:flex;flex-direction:column;max-height:inherit;min-height:0;overflow:hidden}
/* The sheet leaves the dark slot with a shallow forward bend. This lip stays
   registered to the dispenser while the text feeds underneath it. */
#paper .p-inner::before{content:'';position:absolute;z-index:2;top:0;left:0;right:0;height:11px;
  border-radius:0 0 50% 50% / 0 0 2px 2px;pointer-events:none;
  background:linear-gradient(180deg,#14221db3 0,#52624a52 2px,#fffef5cc 5px,#bbc0a52b 8px,transparent 11px)}
#paper .p-body{flex:1 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;scrollbar-width:thin;
  overscroll-behavior:contain;padding:12px 14px}
#paper .p-head{text-align:center;line-height:1.35}
#paper .p-brand{display:block;font-size:9px;letter-spacing:1px}
#paper .p-title{display:grid;grid-template-columns:20px minmax(0,1fr) 20px;align-items:center;gap:6px;margin:8px 0 4px}
#paper .p-title-name{display:flex;align-items:center;justify-content:center;gap:5px;font-size:16px;line-height:1.1;font-weight:700}
#paper .p-can-name{min-width:0}
#paper .p-dot{flex:none}
#paper .p-title.no-motif{display:block}
#paper .p-label{font-size:10px;font-weight:700;letter-spacing:.4px;margin:6px 0 3px}
#paper .p-value{padding-left:0;overflow-wrap:normal;word-break:normal}
#paper .p-bullet{padding-left:1em;text-indent:-1em;margin:3px 0;overflow-wrap:normal;word-break:normal}
#paper .p-inventory .p-item,#paper .p-prose .p-keep{white-space:nowrap}
#paper .p-prose{margin:5px 0;text-wrap:pretty}
#paper .p-rule{border-top:1px dashed #797e6a;margin:10px 0}
#paper .p-small{font-size:9px;letter-spacing:.7px;line-height:1.45}
#paper .p-actions{display:flex;flex:none;align-items:center;justify-content:space-between;gap:10px;min-height:32px;margin:0 14px;padding:5px 0 7px;background:none;border:0;border-top:1px dashed #797e6a}
#paper .p-actions:empty{display:none}
#paper .p-action{display:inline-flex;align-items:center;justify-content:center;gap:5px;flex:0 1 auto;
  min-height:32px;padding:0;background:none;border:0;border-radius:0;box-shadow:none;color:inherit;
  font:10px Receipt,Consolas,monospace;letter-spacing:.2px;white-space:nowrap;cursor:pointer}
#paper .p-action:only-child{margin-inline:auto}
#paper .p-action:last-child:not(:first-child){margin-left:auto}
#paper .p-action:hover:not(:disabled) span{text-decoration:underline;text-underline-offset:3px}
#paper .p-action:disabled{opacity:.5;cursor:default}
#paper .p-glyph{display:inline-block;flex:none;width:12px;height:12px;background:currentColor;
  -webkit-mask:var(--receipt-icon) center/contain no-repeat;mask:var(--receipt-icon) center/contain no-repeat}
#paper .p-title>.p-glyph{width:20px;height:20px;justify-self:center}
#paper [data-icon="download"]{--receipt-icon:url("../assets/icons/receipt/download.svg")}
#paper [data-icon="trash-2"]{--receipt-icon:url("../assets/icons/receipt/trash-2.svg")}
#paper [data-icon="earth"]{--receipt-icon:url("../assets/icons/receipt/earth.svg")}
#paper [data-icon="cookie"]{--receipt-icon:url("../assets/icons/receipt/cookie.svg")}
#paper [data-icon="speaker"]{--receipt-icon:url("../assets/icons/receipt/speaker.svg")}
#paper [data-icon="map-pin"]{--receipt-icon:url("../assets/icons/receipt/map-pin.svg")}
#paper [data-icon="apple"]{--receipt-icon:url("../assets/icons/receipt/apple.svg")}
#paper [data-icon="key-round"]{--receipt-icon:url("../assets/icons/receipt/key-round.svg")}
#paper [data-icon="file-down"]{--receipt-icon:url("../assets/icons/receipt/file-down.svg")}
#paper [data-icon="money-with-wings-print"]{--receipt-icon:url("../assets/icons/receipt/money-with-wings-print.svg")}
#paper .p-void{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  font-size:34px;font-weight:900;color:rgba(217,79,79,.8);transform:rotate(-14deg);letter-spacing:.2em;pointer-events:none}
body.receipt-reading{overflow:hidden}
#receipt-reader{width:min(430px,calc(100% - 24px));max-height:calc(100dvh - 32px);margin:auto;padding:0;
  border:1px solid #2b3b33;border-radius:16px;background:#d6e2d6;color:#383b33;box-shadow:0 12px 45px #0005}
#receipt-reader::backdrop{background:#0b1918b8}
#receipt-reader[open]{display:flex;flex-direction:column}
.receipt-reader-header{display:flex;flex:none;align-items:center;justify-content:space-between;padding:6px 20px;
  font:12px Receipt,Consolas,monospace;letter-spacing:.5px;color:#526859}
.receipt-reader-header button{background:none;border:0;padding:10px 4px;min-height:44px;font:inherit;color:inherit;cursor:pointer}
.receipt-reader-content{min-height:0;overflow:auto;overscroll-behavior:contain;padding:0 20px 28px}
#receipt-reader #paper{position:relative;inset:auto;width:100%;max-height:none;font-size:15px;cursor:auto;transition:none}
#receipt-reader .p-inner,#receipt-reader .p-body{max-height:none;overflow:visible}
#receipt-reader .p-inner::before{display:none}
#receipt-reader .p-body{padding:24px 24px 16px}
#receipt-reader .p-brand{font-size:12px;letter-spacing:1.5px}
#receipt-reader .p-title{grid-template-columns:32px minmax(0,1fr) 32px;gap:10px;margin:11px 0 4px}
#receipt-reader .p-title-name{font-size:25px;letter-spacing:1px;gap:8px}
#receipt-reader .p-title>.p-glyph{width:30px;height:30px}
#receipt-reader .p-label{font-size:13px;letter-spacing:.6px;margin:8px 0 4px}
#receipt-reader .p-prose{margin:7px 0}
#receipt-reader .p-rule{margin:15px 0}
#receipt-reader .p-small{font-size:12px;letter-spacing:1px}
#receipt-reader .p-actions{min-height:44px;margin:0 24px;gap:18px;padding:2px 0 8px}
#receipt-reader .p-action{font-size:13px;min-height:44px;gap:7px}
#receipt-reader .p-action>.p-glyph{width:16px;height:16px}
@media(max-width:400px){
  .receipt-reader-content{padding-left:14px;padding-right:14px}
  #receipt-reader .p-body{padding-left:18px;padding-right:18px}
  #receipt-reader .p-actions{margin-left:18px;margin-right:18px}
  #receipt-reader .p-title{grid-template-columns:24px minmax(0,1fr) 24px;gap:6px}
  #receipt-reader .p-title>.p-glyph{width:24px;height:24px}
  #receipt-reader .p-title-name{font-size:21px;letter-spacing:0;gap:5px}
}
@media(max-width:350px){#receipt-reader .p-title-name{font-size:18px;gap:4px}}
#paper.tearing{ animation:tear .45s ease-in forwards; }
@keyframes tear{ to{ transform:translateY(30px) rotate(3deg); opacity:0; } }

/* scanner: the one grey device in the navy family (Sol's cluster) —
   the scan card leads row 2 on the left, label below the window */
#scanner{ grid-column:1; grid-row:2; display:flex; flex-direction:column;
  justify-content:center; gap:2px; padding:3px 6px;
  background:linear-gradient(180deg, #aab1c4, #8f97ad 70%, #7e879e);
  border:2px solid var(--line); border-radius:5px; }
.sc-window{ position:relative; width:100%; height:16px; background:#150a0d; border:2px solid var(--line); border-radius:4px; overflow:hidden; }
.sc-beam{ position:absolute; left:6%; right:6%; top:50%; height:2px; background:#ff3b30;
  box-shadow:0 0 10px 2px rgba(255,59,48,.8); border-radius:2px; }
#scanner.scanning .sc-beam{ animation:beam .5s ease-in-out 3; }
@keyframes beam{ 0%,100%{ top:16%; } 50%{ top:72%; } }
#scanner.drag-ok{ outline:3px dashed var(--glow); outline-offset:3px; }
/* the scan ceremony (Ben, 2026-08-06): the can rides over and parks on the
   scanner's RIGHT half — bigger than the device, so half coverage is what a
   real scan looks like — and the beam works the exposed left half */
.scan-ride{ position:absolute; z-index:9; pointer-events:none;
  transition:transform .38s cubic-bezier(.4,.1,.3,1), opacity .3s; }
.scan-ride .can-img{ filter:drop-shadow(0 6px 6px rgba(0,0,0,.45)); }
/* #6d1a16, down from #c93a33: bright red on the mid-grey scanner measured
   APCA Lc 16 — decorative, not readable. The deep red keeps the "red accent
   on the one grey device" rule and lands Lc 38, the best the grey allows
   without leaving the red family (2026-08-09 contrast pass). */
.sc-label{ text-align:center; color:#6d1a16; font-size:10px; font-weight:900; letter-spacing:.12em; }
.sc-long{ display:none; }
.sc-short{ display:inline; }

/* (the maker's plate left with the service cover, 2026-09-22, WS5; its help
   line went with it, Ben: not on the receipt card. The name lives on the CRT
   bezel and the kicker) */

/* ---------- can zoom overlay ---------- */
#zoom,#leave{ position:fixed; inset:0; z-index:40; display:flex; align-items:center; justify-content:center;
  background:rgba(10,13,26,.82); backdrop-filter:blur(3px); }
/* the overlay SCROLLS whenever its content outgrows the window (Ben's
   Indeed screenshot, 2026-08-11: buttons clipped on a 1000px-tall desktop
   too, not just landscape phones). flex-start + margin:auto is the classic
   centered-until-it-can't pattern — centered when it fits, scrollable when
   it doesn't, never clipped at the top. */
#zoom{ align-items:flex-start; overflow-y:auto; }
#zoom[hidden],#leave[hidden]{ display:none; }
.zoom-card{ display:flex; flex-direction:column; align-items:center; gap:18px;
  margin:auto; padding:16px 0; min-height:min-content; }
/* the can column: the drink quote centers under the CAN, not under the
   can-and-card group (Ben, 2026-08-11) */
#can-col{ display:flex; flex-direction:column; align-items:center; gap:12px; min-width:0; }
/* the can never smooshes: one min() picks the tightest constraint and
   aspect-ratio derives the height (landscape phones squashed it before) */
/* THE FOCUSED CAN IS A DESKTOP-SIZED OBJECT NOW (Ben, 2026-08-06). It was
   capped at 300px, which on a 27" monitor put the nutrition label at ~258px
   wide — a resume rendered at the size of a business card. The cap is the
   only thing that moves: the ratio, the label's 86% share and every type
   size inside it are unchanged, so the whole card simply gets bigger
   together. 62vh keeps it inside a short laptop window; mobile is untouched
   below, where 80vw was already doing the right thing. */
/* toned down from 430/62vh (Ben, 2026-08-11): the focused can dominated the
   card and drowned the action row on ~1000px-tall windows */
#zoom-can{ position:relative; width:min(360px, 80vw, 50vh); aspect-ratio:150 / 230;
  height:auto; perspective:1200px; }
/* the bento is the one landscape object in the machine — the portrait can
   box would shrink it to a snack. Its zoom keeps the render's own aspect. */
#zoom-can[data-art="21-bento-box"]{ aspect-ratio:1478 / 1064; width:min(500px, 86vw, 60vh); }
/* the can no longer flips at all (Ben, 2026-08-10) — it is art, full stop.
   The nutrition label moved to the SIDE PANEL below; the old .bigcan.flipped
   / .bigface.back machinery went with it. */
.bigcan{ position:absolute; inset:0; }
.bigface{ position:absolute; inset:0; border:4px solid var(--line);
  border-radius:26px; overflow:hidden; display:flex; flex-direction:column; align-items:center;
  box-shadow:inset -30px 0 40px rgba(0,0,0,.18), inset 20px 0 30px rgba(255,255,255,.14), 0 20px 50px rgba(0,0,0,.5); }
.bigface.artface{ background:transparent; border:none; box-shadow:none; }
.label-img{ position:absolute; inset:0; width:100%; height:100%; object-fit:contain;
  filter:drop-shadow(0 18px 40px rgba(0,0,0,.55)); }
/* Match the stock 440/900 body proportion to 54's 413/918 art. Width only:
   the slightly taller approved drawing and all source pixels stay unchanged. */
#zoom-can[data-art="54-privacy-notice"] .label-img{ transform:scaleX(1.086683); }
/* (.big-lid/.big-deco/.big-brand/.big-kind/.big-slogan pruned 2026-08-09 —
   bigCanHTML() has rendered the label art whole since the zoom went raster;
   the slogans those classes carried now live only in data.js, unrendered,
   which is flagged to Ben in OUTSTANDING) */
/* dates chip removed from the zoom front (Ben, 2026-08-04) — the dates
   live on in the nutrition serving line and every receipt */
/* nutrition back — the FDA label, played straight (that's the joke) */
/* full width and no longer shoved to the bottom of a can — the label is the
   object now, so it gets the whole card and a real drop shadow */
.nut{ position:relative; width:100%; background:#fff; border:2.5px solid #111; border-radius:2px;
  padding:14px 16px 15px; color:#111;
  box-shadow:0 14px 34px rgba(0,0,0,.55);
  font-family:"Helvetica Neue","Arial Narrow",Arial,sans-serif; }
.nut h3{ font-size:24px; font-weight:900; letter-spacing:-.02em; line-height:1.05;
  border-bottom:1px solid #111; padding-bottom:2px; }
.nut .serving{ font-size:10px; font-weight:700; padding:3px 0 4px; }
.nut .nut-bar{ height:8px; background:#111; margin:0 0 3px; }
.nut .nut-amount{ display:flex; justify-content:space-between; font-size:8.5px; font-weight:900;
  letter-spacing:.02em; border-bottom:4px solid #111; padding-bottom:2px; }
.nut table{ width:100%; border-collapse:collapse; font-size:10px; }
.nut td{ padding:3px 0; border-bottom:1px solid #111; }
.nut td:first-child{ font-weight:700; padding-right:6px; }
.nut td.one-line{ white-space:nowrap; }   /* one- and two-word labels stay on one line (vend.js oneLine) */
.nut td:nth-child(2){ text-align:right; color:#222; text-wrap:balance; }
.nut td:last-child{ text-align:right; font-weight:900; padding-left:8px; white-space:nowrap; }
.nut tr:last-child td{ border-bottom:4px solid #111; }
.nut .foot{ font-size:8px; line-height:1.35; padding-top:4px; color:#111; }
/* position:relative is load-bearing: the macro wall is a positioned layer,
   and a static barcode would paint underneath it (caught 2026-08-06) */
.big-barcode{ position:relative; margin-top:14px; margin-bottom:0; width:38%; height:44px; background:#fff;
  border:3px solid var(--line); border-radius:4px; display:flex; align-items:center; justify-content:center; }
.big-barcode i{ display:block; height:70%;
  background:repeating-linear-gradient(90deg,#111 0 3px, #fff 3px 5px, #111 5px 6px, #fff 6px 10px);
  width:86%; }
/* WS2 (BT-21): the privacy cans say what SCAN does, one plain line under the
   barcode and above BACK / SCAN BARCODE. Set like the display caption: cream
   on the card, no box, so it reads as the machine's own note, not label copy. */
.scan-explain{ position:relative; margin:9px 0 0; max-width:100%; text-align:center; color:var(--cream);
  font-size:10.5px; line-height:1.35; letter-spacing:.03em; opacity:.92;
  text-shadow:0 2px 8px rgba(0,0,0,.6); }
/* the display caption: the can's slogan, set like the shelf card a machine
   clips beside the goods — quiet italic cream, between object and actions */
#zoom-slogan{ max-width:min(360px, 86vw); text-align:center; color:var(--cream);
  font-size:13.5px; font-style:italic; letter-spacing:.04em; opacity:.92;
  text-shadow:0 2px 8px rgba(0,0,0,.6); }
#zoom-slogan[hidden]{ display:none; }
.zoom-actions{ display:flex; gap:12px; flex-wrap:wrap; justify-content:center; }
#side-panel > #zoom-actions,
#contact-col > #zoom-actions{ margin-top:10px; }
.zbtn{ background:var(--dev); color:var(--cream); border:2px solid var(--line); border-radius:8px;   /* 3 -> 2 (Ben, 2026-09-22: the outline was a little thick) */
  font-weight:900; letter-spacing:.1em; font-size:13px; padding:10px 18px;
  box-shadow:0 4px 0 rgba(0,0,0,.35); }
.zbtn:active{ transform:translateY(3px); box-shadow:0 1px 0 rgba(0,0,0,.35); }
/* the house focus ring — overlay and receipt buttons were the last controls
   still wearing the browser default (2026-08-09 focus pass) */
.zbtn:focus-visible,.p-action:focus-visible{ outline:3px solid var(--cream); outline-offset:3px; }
/* A dark underlay keeps the cream ring visible on the cream paper. */
#paper .p-action:focus-visible{box-shadow:0 0 0 7px #383b33}
/* Honest action hierarchy (Ben, 2026-08-11): coral advances or commits;
   graphite backs out beside a forward CTA. A lone BACK is itself the clear
   focus, so it takes coral. */
.zbtn.back{ background:var(--dev); }
.zbtn.cta{ background:var(--coral); }
.zoom-actions.solo .zbtn.back{ background:var(--coral); }
/* CONTACT ME Wave 23: handset and SIM are fully drawn. Code retains only four
   transparent accessible hit boxes registered over the physical cap art. */
.zoom-row{ position:relative; display:flex; align-items:center; justify-content:center; gap:26px; }
/* The side panel IS the nutrition label (Ben, 2026-08-11: the career cards
   came out — "keep it strictly to the nutrition label and have it appear by
   default"). It mounts beside the can for every labelled item, no flip, no
   toggle; only the handset and the unlabeled can go without. The panel-in
   turn is kept as the label's arrival, not a state change. */
#side-panel{ position:relative; width:360px; flex:none; }
#nutri-card{ display:flex; flex-direction:column; align-items:center; justify-content:center;
  animation:panel-in .45s cubic-bezier(.5,0,.3,1.2); }
@keyframes panel-in{ from{ transform:rotateY(70deg); opacity:.15; } }
/* DESKTOP NUTRITION LABEL (Ben, 2026-08-11): the first mounted label still
   read like fine print beside a full-size can. At ordinary desktop/tablet
   heights, use the available vertical room for a genuinely readable card.
   The adaptive width leaves the 360px can and an 18px gap inside a 768px
   viewport; short windows and portrait phones retain their compact rules. */
@media (min-width:760px) and (min-height:601px){
  #zoom.paneled .zoom-row{ gap:18px; }
  #zoom.paneled #side-panel{ width:min(420px, calc(100vw - 394px)); }
  /* Bento is wider than every portrait can. Give it the remaining horizontal
     budget after the full-size nutrition panel instead of letting the row
     create a browser scrollbar in an 856px review panel. Its source ratio
     remains untouched; only the focused-view width participates in the fit. */
  #zoom.paneled #zoom-can[data-art="21-bento-box"]{
    width:min(500px, calc(100vw - 456px), 60vh);
  }
  #zoom.paneled .nut{ border-width:3px; padding:20px 22px 21px; }
  #zoom.paneled .nut h3{ font-size:32px; padding-bottom:3px; }
  #zoom.paneled .nut .serving{ font-size:13px; padding:5px 0 6px; }
  #zoom.paneled .nut .nut-bar{ height:10px; margin-bottom:4px; }
  #zoom.paneled .nut .nut-amount{ font-size:11px; border-bottom-width:5px;
    padding-bottom:3px; }
  #zoom.paneled .nut table{ font-size:13px; line-height:1.12; }
  #zoom.paneled .nut td{ padding:5px 0; }
  #zoom.paneled .nut td:first-child{ padding-right:9px; }
  #zoom.paneled .nut td:last-child{ padding-left:10px; }
  #zoom.paneled .nut tr:last-child td{ border-bottom-width:5px; }
  #zoom.paneled .nut .foot{ font-size:10px; line-height:1.4; padding-top:6px; }
  #zoom.paneled .big-barcode{ width:42%; height:52px; margin-top:18px; }
  #zoom.paneled .scan-explain{ font-size:12.5px; margin-top:11px; }
  #zoom.paneled #side-panel > #zoom-actions{ margin-top:10px; }
}
/* (the two-face #side-flip machinery and the shelf placard — #career-card,
   band, price-burst, proofs, slot-tag chips — were removed with the cards,
   2026-08-11. git remembers the placard.) */
#zoom.contact #zoom-can{ width:300px; aspect-ratio:1 / 2; }
#zoom.contact .zoom-row{ align-items:flex-start; gap:16px; }
/* The SIM is supporting evidence, not the hero. Its 8:5 carrier proportions
   stay exact while a 400px width brings it closer to the handset's visual
   weight and frees the dead air Ben flagged. The column owns BACK as well,
   which keeps the exit directly beneath the evidence it accompanies. */
#contact-col{ display:none; flex:none; flex-direction:column; align-items:center; }
#zoom.contact #contact-col:not([hidden]){ display:flex; }
#contact-sim{ display:none; position:relative; width:400px; height:250px; flex:none; overflow:visible; }
#zoom.contact #contact-sim:not([hidden]){ display:block;
  filter:drop-shadow(0 14px 24px rgba(0,0,0,.48)); }
.sim-art{ position:absolute; inset:0; display:block; width:100%; height:100%; object-fit:fill; }
.phone-actions{ position:absolute; inset:0; z-index:3; pointer-events:none; }
.phone-btn{ position:absolute; left:var(--cap-left); top:var(--cap-top);
  width:var(--cap-width); height:var(--cap-height); pointer-events:auto;
  display:block; padding:0; border:0; border-radius:7px; background:transparent;
  color:transparent; font-size:0; cursor:pointer; touch-action:manipulation; }
.phone-btn:hover{ background:rgba(255,196,77,.18); }
.phone-btn:focus-visible{ outline:3px solid var(--amber); outline-offset:-5px;
  background:rgba(255,196,77,.16); }
.phone-btn:active{ transform:translateY(1px); background:rgba(232,99,90,.2); }
/* The handset has no nutrition label or scan shortcut. Its registered caps
   are the interface, with the shared BACK control below as a reliable exit. */

/* the one-word receipt for the copy button */
#toast{ position:fixed; left:50%; bottom:56px; transform:translateX(-50%) translateY(8px);
  z-index:80; padding:8px 18px; background:var(--cream); color:var(--line);
  border:2.5px solid var(--line); border-radius:8px; font-weight:900;
  font-size:13px; letter-spacing:.08em; box-shadow:0 4px 0 rgba(0,0,0,.35);
  white-space:nowrap; opacity:0; pointer-events:none;
  transition:opacity .18s, transform .18s; }
#toast.show{ opacity:1; transform:translateX(-50%) translateY(0); }
/* anchored under the phone that was clicked, rather than at the page edge */
#toast.anchored{ position:absolute; left:50%; top:100%; bottom:auto;
  margin-top:12px; padding:6px 14px; font-size:12px; }

.leave-card{ background:var(--dev); border:4px solid var(--line); border-radius:14px;
  color:var(--cream); max-width:min(340px,86vw); padding:20px 22px; display:flex; flex-direction:column; gap:14px; }
.leave-head{ font-weight:900; letter-spacing:.16em; color:var(--coral); font-size:13px; }
.leave-card p{ font-size:13.5px; line-height:1.5; }
.leave-url{ display:block; overflow-wrap:anywhere; }
.leave-privacy-link{ color:inherit; overflow-wrap:anywhere; }
.leave-privacy-link:focus-visible{ outline:2px solid var(--cream); outline-offset:2px; }
#leave.exit-prank .leave-card{ width:390px; max-width:calc(100vw - 32px); }
#leave.resume-download .leave-card{width:460px;max-width:calc(100vw - 32px)}
#leave.resume-download .zoom-actions{display:grid;grid-template-columns:1.5fr 1fr 1fr;gap:10px}
#leave.resume-download .zbtn{display:inline-flex;align-items:center;justify-content:center}
@media (max-width:480px){
  #leave.resume-download .zoom-actions{grid-template-columns:repeat(2,minmax(0,1fr))}
  #leave.resume-download #leave-stay{grid-column:1 / -1}
}
#leave.exit-prank .leave-head{ letter-spacing:.04em; font-size:16px; line-height:1.4; }
#leave.exit-prank .zoom-actions{ flex-wrap:nowrap; gap:10px; }
#leave.exit-prank .zbtn{ padding:10px 12px; letter-spacing:.02em; font-size:12px; white-space:nowrap; }

/* ---------- the overlay shell (design pass 2026-08-06, LOCKED) ----------
   One shell, four overlays. The card SNAPS in over three stepped frames
   rather than easing — a cel-shaded object has no motion blur. Opacity
   only: a stepped SCALE jumped the card's edges sideways frame by frame
   (Ben saw the can shift left-right on open). Reduced motion gets the end
   state via the global rule. Failure never gets an overlay: wrong codes
   and declined cards belong to the CRT. */
@keyframes ovin{ from{ opacity:0; } }
#zoom:not([hidden]) .zoom-card, #gacha:not([hidden]) .gacha-card,
#leave:not([hidden]) .leave-card{ animation:ovin .12s steps(3); }

/* (The raster DRESSED MODE block lived here until wave 15. The chrome is
   now drawn per-element above — there is nothing left to register.) */

/* ============================================================
   MOBILE — SIDE CONSOLE (≤560px, design pass 2026-08-06, LOCKED).
   Not a miniature and not a tall stack: the same machine stood
   upright. Glass left, control column right — CRT, keypad, payment
   top to bottom, the order you use them — then the printer FULL
   WIDTH below (the receipt's 39-char monospace lines are what set
   that width), takeout at the bottom. This is the authored geometry
   beneath the persistent full-machine phone fit. Browser pinch zoom
   enlarges it only when the visitor chooses. The !important is the
   belt to that suspender.
   ============================================================ */
@media (max-width:560px){
  #power-gate{ padding:14px; }
  .power-panel{ width:min(430px, calc(100vw - 20px)); border-width:3px; }
  .power-panel-head{ min-height:48px; padding:10px 22px; gap:10px; }
  .power-panel-head span:first-child{ font-size:12px; }
  .power-panel-head span:last-child{ max-width:126px; font-size:8px; }
  .power-panel-body{ padding:12px; }
  .power-console{ padding:23px 20px 15px; }
  #power-title{ font-size:clamp(28px,9vw,35px); }
  .power-readouts{ margin-top:22px; }
  .power-readout{ font-size:9.5px; }
  #power-start{ width:min(252px, 100%); min-height:52px; font-size:14px; }
  body{ overflow-x:hidden; overflow-y:auto; }
  /* BT-37: keep the cabinet close to the top-left mark on every phone.
     Override the desktop auto margins as well as the stage alignment. */
  #stage{ min-height:100svh; display:flex; flex-direction:column; justify-content:flex-start; }
  /* BT-41 (Ben, 2026-09-23): the machine meets the screen edge to edge, "we should not be
     displaying the wall for no good reason". No air above or below it either. */
  #alcove{ width:100%; height:auto; transform:none !important;
    flex:none; padding:0; margin:0 auto; }
  #alcove::before,#alcove::after{ display:none; }

  /* radio: phones FOLLOW it into the bay (item 18's open mobile question,
     decided at build). The machine carries its radio at every width — the
     bay becomes a short strip at the bottom of the chassis, same lore, no
     second layout for the one prop that just moved to end exactly that. */

  /* ONE GRID for the whole chassis (2026-08-06). #deck, #notch and
     #bottomrow all go display:contents so every device is a direct grid
     item — which is the only way the radio bay can sit in the LEFT column
     beside the payment panel, the empty space Ben circled. Nested grids
     could not reach across that boundary, and faking it with absolute
     positioning would be a hand-tuned offset in a second coordinate
     system, which is the exact mistake this project keeps paying for. */
  /* margins back to an even 4px now the drawn side is gone — the 15px the
     extrude needed on the right was real chassis width on a 390px screen */
  #machine{ position:relative; left:auto; top:auto;
    width:auto; height:auto; margin:0;
    display:grid; grid-template-columns:minmax(0,1.7fr) minmax(140px,1fr);
    gap:7px; padding:10px; }
  #machine .m-feet{ display:none; }
  #deck,#bottomrow{ display:contents; }
  /* THE RIGHT COLUMN IS EXACTLY AS IT WAS — CRT, keypad, payment down
     rows 2-4, each at its natural height. The radio bay does NOT get a row
     of its own: it shares the glass's grid area and sits at the BOTTOM of
     it, which is the empty space that was already there (Ben, 2026-08-06:
     "you didn't have to change anything on the right side"). Giving the
     bay its own row stretched the device cards, which pushed the speaker
     toggle out of its bezel and opened a hole under CLEAR/ENTER. Two items
     in one grid area, one aligned to each end, costs the layout nothing. */
  #marquee{ grid-column:1 / -1; grid-row:1; }
  /* BT-41: the glass spans the whole right column (screen, keypad, coin panel, radio). The bin
     is only as wide as the glass and the book cabinet takes the rest of its row, under the right
     column (Ben: "the collection bay doesnt need to be that wide"); library.js narrows the
     cabinet's art to that bay. */
  #glasswrap{ grid-column:1; grid-row:2 / span 4; align-self:stretch; }
  #paperbay{ grid-column:2; grid-row:7; align-self:stretch; }
  #crt{ grid-column:2; grid-row:2; }
  #keypad{ grid-column:2; grid-row:3; }
  #payment{ grid-column:2; grid-row:4; }
  #printer{ grid-column:1 / -1; grid-row:6; }
  #takeout{ grid-column:1; grid-row:7; }
  /* each device card keeps its own height — a stretched card moves the
     things absolutely positioned inside it (the speaker toggle lives at
     the bezel's bottom edge, not the grid row's) */
  #crt,#keypad,#payment{ align-self:start; }

  #marquee{ margin:4px 12px 2px; height:52px; }
  .mq-sign{ display:none; }
  .mq-sign-phone{ display:block; position:absolute; inset:-3px 0; z-index:5; }

  /* #notch dissolves too — every device is a grid item of #machine above.
     The glass spans exactly TWO rows (CRT + keypad) and the radio bay takes
     the third beside the payment panel, which is what closes the hole under
     the display case instead of leaving it as chassis. */
  /* the column's 136px floor is where a three-across key holds exactly 40px —
     on the narrowest phones the GLASS gives way, never the touch targets */
  /* 10px side padding keeps every card inside the shell's gold moulding —
     at 6px the devices sat on top of the ring and read as overflowing */
  #notch{ display:contents; }
  #glass{ padding:8px 6px 6px; }
  /* shelves need a definite share to seat their goods; with the wrap
     auto-sized they take their natural content height instead of flexing */
  /* BT-41: the glass is as tall as the right column now, so the shelves share that height
     (a little air between rows) instead of leaving an empty band under the last one */
  .shelf{ flex:1 1 auto; }
  .shelf.gacha{ flex-grow:.62; }
  /* the goods sit ON the rail here too — no stray bottom padding lifting them */
  .slots{ padding:0 2px; }
  /* the cup's 3.3% desktop seat correction does not apply here: at the 38px
     mobile sprite the ink already met the rail (audit-measured), and the
     inherited desktop value sank it 1.8px (2026-08-09) */
  .can[data-art="43-cup-noodle-shoyu"] .can-img,
  .can[data-art="43-cup-noodle-shoyu"] .can-strip{ --seat:0%; }

  /* goods sized to the 63% glass — the shelves stay 5·5·5·4·4 */
  :root{ --can-w:38px; --can-h:58px; }
  .gcapsule{ width:50px; height:50px; }   /* BT-41: the shared height gives row 4 room (Ben: "we have the space") */
  /* the tap target keeps its 40px floor, but the capsule sits at the BOTTOM
     of it so it rests on the rail like everything else */
  .gcap-btn{ min-width:50px; min-height:50px; display:flex; align-items:flex-end; justify-content:center; }
  .gcap-btn .gcapsule{ width:50px; height:50px; }
  /* Match the painted height of 42/43 without changing the 50px tap target.
     Stickers occupies 318 of its 360 source rows; the others occupy 292/294. */
  .gcap-btn .gcapsule[data-bay="41"]{ scale:.92; transform-origin:bottom center; }
  #shelf-4 .slot{ flex:1 1 0; }
  #tray .gcapsule{ width:50px; height:50px; }
  #shelf-5{ gap:0; }
  #shelf-5 .slot{ flex:1 1 0; }   /* four pockets share the phone row */
  .bay{ width:34px; height:48px; }
  .slot-tag{ font-size:8px; padding:0 4px; letter-spacing:.04em; border-width:1.5px; }   /* one step thinner on the smaller phone card (Ben, 2026-09-23) */
  .soldout-strip{ font-size:6.5px; }
  .slot::before,.slot::after{ width:6px; height:14px; }
  /* NO bottom padding. A second .slots rule further down this same media
     block used to re-add 2px and silently beat the one above that had already
     removed it, so every can on mobile hovered 2px over its rail — invisible
     to the old box-based RAIL check and to me, for two waves. Duplicated
     selectors in one block are how a fix gets undone without leaving a mark. */

  /* the control column: keys flex to the column and keep the touch floor.
     flex-basis:0 is scoped to ROW children only — inside a .kp-cell the
     cell's column flex would read it as a HEIGHT and squash the key
     (caught by the sweep: 21px-tall ENTER, 2026-08-06) */
  #keypad{ display:block; padding:3px; touch-action:none; }
  .kp{ min-width:0; width:auto; height:27px; font-size:13px; padding:0; }
  .kp-row > .kp{ flex:1 1 0; }
  .kp-row{ gap:5px; margin-bottom:3px; }
  /* CLEAR/ENTER caps get real air under their keys (Ben, 2026-08-06) */
  .kp-cell{ flex:1 1 0; min-width:0; gap:7px; }
  .kp-cell .kp{ flex:none; width:100%; }
  .kp-cap{ font-size:7px; height:7px; line-height:7px; letter-spacing:.08em; }

  .crt-bezel{ padding:6px 6px 24px; }   /* the phone glass needs the width: 21 letters at 10px (WS1) */
  /* the moulded family drops to 8px together on mobile (pr-label and
     coinreturn below do the same) — one family, one step. SIZE is the
     family contract; TRACKING is per-element, and here it has to give: at
     .14em the label measured 88.6px into a chin that offers 84 before the
     speaker toggle, so the last letters ran under it. Measured, not
     eyeballed — .06em lands it at ~78px with room to spare. */
  .crt-bezel::after{ font-size:8px; letter-spacing:.06em; left:8px; bottom:7px; }
  .crt-screen{ height:86px; padding:6px 3px; }   /* BT-41: five rows of 14px and no more, shorter than the 96 before (Ben: the taller glass was "way too tall") */
  /* the narrow cut at 10px holds Ben's rows (the widest, 52 · DO NOT SELL/SHARE,
     is 22 letters = 110px) in the phone's 114px glass; five rows on 14px use
     70 of 80. The phone never takes dense. WS4's phone frame widens the CRT
     and lifts this to 13px. */
  #screen-lines{ font-family:"Inconsolata CRT Narrow","Consolas","DejaVu Sans Mono",monospace; font-size:10px; line-height:14px; }
  /* A compact, icon-only plate leaves a separate seat for the refusal lamp. */
  .snd{ width:32px; right:6px; transform:scale(.8); transform-origin:right bottom; }
  .snd-cone{ left:7px; }
  .snd-w1{ left:16px; }
  .snd-w2{ left:19px; }
  .snd-x{ left:17px; }
  #crt-led{ right:40px; bottom:10px; width:6px; height:6px; z-index:3; }

  /* payment goes VERTICAL down the column — card then bill stacked (the
     equal-size wrapper goes column here), coin row, then the scanner
     SHARES the last row with the coin bowl (Ben, 2026-08-06). The bowl
     keeps the right side, under the slot the coin goes into. */
  /* BT-41 (Ben, 2026-09-23: equal spacing, "the coin return button irl doesnt look that big"):
     every part has its own row with the same 5px gap; the card and bill slots are a matched
     pair; COIN RETURN and SCAN are a matched pair of small buttons, each beside what it works
     with (the coin slot, then the cup the coins drop into). */
  #payment{ margin:0; padding:4px 8px; display:grid; grid-template-columns:58px 1fr;
    grid-template-rows:18px 18px 28px 28px; align-content:start; gap:5px 10px; }
  #payslots{ display:contents; }
  #cardreader{ grid-column:1 / -1; grid-row:1; }
  #billslot{ grid-column:1 / -1; grid-row:2; }
  #billslot,#cardreader{ height:18px; flex:none; }
  /* Both openings share their insets and height. Only the card reader has
     a status LED; the cash slot keeps the same blank space on its right. */
  #billslot{ position:relative; }
  .cr-slot,#billslot i{ position:absolute; left:6px; right:22px; top:50%; margin-top:-2.5px; width:auto; height:5px; }
  .cr-led{ right:7px; top:50%; margin-top:-2.5px; bottom:auto; }
  #coinslot{ grid-column:2; grid-row:3; justify-self:center; align-self:center; width:14px; height:28px; }
  #coinreturn{ grid-column:1; grid-row:3; font-size:7.5px; line-height:1.2; padding:0 2px; letter-spacing:.06em; }
  #scanner{ grid-column:1; grid-row:4; }
  #coincup{ grid-column:2; grid-row:4; min-height:0; height:28px; }
  .coin{ width:22px; height:22px; }
  #coincup::before{ top:2px; height:4px; }
  /* the in-slot coin follows the cup coin down, so the two stay one object */
  .coin.in-slot{ width:22px; height:22px; margin-left:-11px; top:-24px; }
  .sc-window{ height:12px; }
  .sc-label{ font-size:8px; }
  .pr-label{ font-size:8px; }

  /* the radio on a phone (BT-41): its own row at the foot of the right column, full column
     width, its bottom level with the glass's; it never touches the coin panel */
  #radio-face{ grid-column:2; grid-row:5; align-self:end; height:auto; width:100%; margin:0; }
  #radio{ height:auto; aspect-ratio:223 / 52; }
  #library{ inset:4px; }
  /* Seat the key inside the original compact dispenser height. Its 19px
     phone height matches the mute key's 0.8 scale without lowering the bin. */
  #printer{ padding:1px 10px 5px; }
  .pr-slot{ height:10px; }
  #paper{ top:6px; }
  .pr-label{ margin-bottom:2px; }
  #receipt-tear{ height:19px; }
  #receipt-tear:focus-visible{ outline-offset:-3px; }
  #paper{ left:9px; right:9px; }
  #port{ height:90px; }   /* BT-41: the bin cut to its tallest item, the bento (68px) in the 70px tray under a 20px door */
  #tray{ min-height:56px; padding:6px 7px 7px; }

  /* the nutrition label is a proper card on the side panel now — full
     panel width, phone-scaled padding (Ben, 2026-08-10: the can-shaped
     label read as a mistake on mobile) */
  .nut{ width:100%; padding:10px 11px; }

  /* The nutrition panel stays beside the can conceptually, but stacks
     below it on portrait screens so neither object is reduced to
     unreadable thumbnail size. The overlay scrolls rather than clipping. */
  #zoom.paneled{ align-items:flex-start; overflow-y:auto; padding:22px 0; }
  #zoom.paneled .zoom-card{ min-height:min-content; }
  #zoom.paneled .zoom-row{ flex-direction:column; align-items:center; gap:12px; }
  #zoom.paneled #zoom-can{ width:min(180px, 46vw, 29vh); }
  #zoom.paneled #zoom-can[data-art="21-bento-box"]{ width:min(300px, 80vw, 34vh); }
  #zoom.paneled #side-panel{ width:min(340px, 88vw); }
  #zoom.paneled #side-panel > #zoom-actions{ margin-top:8px; }

  /* At 300px the 88px source caps remain 44px touch targets. Keep the phone
     at that interaction-safe size on a 390px portrait screen, then trim only
     the supporting SIM, inter-object air, and shell padding so BACK stays in
     the initial viewport instead of peeking below the fold. */
  #zoom.contact .zoom-card{ padding:8px 0; }
  #zoom.contact #zoom-can{ width:min(300px, 80vw, 36vh); }
  #zoom.contact .zoom-row{ flex-direction:column; align-items:center; gap:8px; }
  #zoom.contact #contact-sim{ width:min(400px, 72vw, 31vh); height:auto;
    aspect-ratio:8 / 5; margin-top:0 !important; }
  #zoom.contact #contact-col > #zoom-actions{ margin-top:6px; }
}

/* LANDSCAPE PHONES + SHORT WINDOWS (Ben's live find, 2026-08-11): an
   844x390-class window passed the ≤560 width check as "desktop", so the
   focused can sized to 62vh and the action row drowned below the fold
   with no scroll, and the career card clipped both edges. Short viewports
   get the same courtesy as narrow ones: the overlay scrolls, the objects
   share the height budget, the card wears its compact type. Width rules
   above and height rules here only co-fire in tiny square windows, where
   the more specific narrow-phone selectors still win. */
@media (max-height:600px){
  #zoom{ align-items:flex-start; justify-content:flex-start; overflow-y:auto; padding:14px 0; }
  /* Auto block margins center an overflowing flex item by giving it negative
     top space in Chrome, which clipped the can lid above the scroll origin.
     Short windows start at the real top and scroll down instead. */
  #zoom > .zoom-card{ min-height:min-content; margin:0 auto; gap:12px; }
  #zoom-can{ width:min(240px, 30vw, 56vh); }
  #side-panel{ width:min(340px, 42vw); }
  #zoom.contact #zoom-can{ width:min(240px, 30vw, 56vh); }
  /* the gacha viewer shares the overlay contract */
  #gacha{ align-items:flex-start; justify-content:flex-start; overflow-y:auto; padding:14px 0; }
  #gacha > .gacha-card{ margin:0 auto; }
}

/* small phones: the column floor has taken glass width, so the goods shrink */
@media (max-width:380px){
  :root{ --can-w:34px; --can-h:52px; }
  .bay{ width:30px; height:44px; }
}
/* an SE-width screen forces the choice the kit named: narrower glass */
@media (max-width:330px){
  :root{ --can-w:27px; --can-h:41px; }
  .gcapsule,.gcap-btn .gcapsule{ width:26px; height:26px; }
  .bay{ width:24px; height:36px; }
  .slot-tag{ font-size:7px; padding:0 3px; }
}

/* PHONE FULL VIEW (Ben, corrected 2026-08-12): the whole machine remains in
   frame on a phone, portrait or landscape. This class removes the authored
   layout box from document flow, fits it to both viewport axes, and locks
   layout scroll. Machine interactions never remove it or enlarge the machine;
   a closer view belongs exclusively to the visitor's browser-native pinch
   zoom. Kept after the phone media rules so its transform deliberately wins
   over the native `transform:none !important`. */
body.phone-full-view{ overflow:hidden; }
body.phone-full-view #stage{
  display:block; height:100svh; min-height:100svh; max-height:100svh;
  overflow:hidden;
}
body.phone-full-view #alcove{
  position:absolute; left:50%; top:50%; margin:0 !important;
  transform-origin:50% 50%;
  /* Fit immediately, even under the reduced-motion rule's 1ms durations,
     so the station measures the final scale when the viewport changes. */
  transition:none;
  transform:translate(-50%,-50%) scale(var(--phone-full-scale,1)) !important;
}
/* The app-level zoom control does not participate in the persistent phone
   fit. Phones use their native pinch gesture, so do not expose a dead control
   in landscape merely because its CSS width exceeds the portrait breakpoint. */
body.phone-full-view #zoomctl{ display:none; }

/* reduced motion */
@media (prefers-reduced-motion:reduce){
  *{ animation-duration:.001s !important; transition-duration:.001s !important; }
  .selection-current-pulse,.cold-vend-fx{ display:none !important; }
  .can-btn.selection-answer,.gcap-btn.selection-answer{ animation:none !important;
    filter:brightness(1.16) drop-shadow(0 0 7px rgba(214,243,248,.78)); }
  .cold-vend-active > .can-img{ animation:none !important;
    filter:brightness(1.12) drop-shadow(0 0 8px rgba(214,243,248,.7)); }
  #paper.printing{ transition:none !important; }
  #paper.printing .thermal-ink{ animation:none !important; opacity:1; filter:none; }
  .cover-screw.removed{ animation:none !important; opacity:0; }
  .cover-screw.removed + .cover-screw-hole{ transition:none !important; opacity:1; }
  #service-cover.released{ animation:none !important; opacity:0; }
  /* The approval remains perceivable without sending a highlight across the
     plate: one brief, motionless lift, then the certificate returns to rest. */
  .cert-sheen,.cert-spark{ display:none; }
  #station #st-certificate-wrap.cert-approved{
    filter:brightness(1.28) drop-shadow(0 0 5px rgba(255,196,77,.82)) !important;
  }
  #gsticker{ transform:none !important; filter:drop-shadow(0 10px 24px rgba(0,0,0,.5)); }
  /* no tilt, no hold — so no grab cursor promising one */
  #gacha-sticker-shell{ cursor:default; }
  /* the gear trains hold still — an infinite spin at collapsed duration
     would strobe, which is worse than motion, not less */
  /* (the gear flankers reduced-motion rule left with them; the mechanism SVG stills itself) */
}
