/* ==========================================================================
   $VEND — the machine
   --------------------------------------------------------------------------
   The machine is not a hero image. It IS the interface, and every part of it is
   a real control.

   The object is built the way the reference is: a spray-painted marquee, a
   neon-edged glass case holding four shelves of five, a narrow control column,
   a graffiti'd side wall with the mascot leaning on it, and a bone skirt at the
   bottom with the dispenser tray cut into it.

   Every edge is a drawn line and every shadow is a hard offset. Blur is reserved
   for GLOW, because glow is a light source and light is the only thing on this
   page that is allowed to be soft.
   ========================================================================== */

/* ==========================================================================
   Stage — the machine fills the viewport
   --------------------------------------------------------------------------
   "Somewhat fullscreen": on desktop the machine is sized to the viewport HEIGHT
   first and takes whatever width that implies, so it never needs scrolling to be
   seen whole. `min(100%, height × ratio)` is what makes it give up width on a
   short window instead of overflowing.
   ========================================================================== */

.stage {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  padding: 16px 18px 0;
  /* The sidebar is the chrome on desktop, so there is no top bar to subtract. */
  min-height: 100vh;
}

.machine {
  --ratio: 0.845;          /* width ÷ height of the object, from the reference */
  --panel-w: 21%;
  --wall-w: 15%;
  position: relative;
  /*
   * The reserve is what the machine gives up to the floor glow and to breathing
   * room — 26px of glow below it, 16px of stage padding above, and the rest as
   * margin. It was 104px, which cost 36px of machine height on every screen and
   * was the direct cause of the control panel being clipped on a 1366x768
   * laptop. MEASURED: the panel needs ~440px and was getting 364.
   */
  width: min(100%, calc((100vh - 64px) * var(--ratio)));
  aspect-ratio: var(--ratio);
  display: flex;
  flex-direction: column;
  isolation: isolate;
}

/* The floor: a soft pool of light under the machine, plus the faint reflection
   of its own neon. The only blurred thing that is not a glow on the machine. */
.machine::after {
  content: "";
  position: absolute;
  left: 2%;
  right: 2%;
  bottom: -26px;
  height: 40px;
  background:
    radial-gradient(46% 100% at 50% 0%, rgba(61, 139, 255, .3) 0%, transparent 70%),
    radial-gradient(30% 90% at 18% 0%, rgba(255, 138, 40, .16) 0%, transparent 72%);
  filter: blur(7px);
  z-index: -1;
}

/* ==========================================================================
   a. THE MARQUEE
   --------------------------------------------------------------------------
   A spray-painted plate: near-black ground, a blue blast behind the wordmark,
   ink splatter, and drips running off the bottom edge.
   ========================================================================== */

.marquee {
  position: relative;
  flex: none;
  padding: 12px 14px 0;
  background: linear-gradient(180deg, #0E1A3C 0%, #071026 55%, #05080F 100%);
  border: var(--line) solid var(--ink);
  border-radius: 12px 12px 5px 5px;
  box-shadow: var(--sh-3);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 10px;
  overflow: hidden;
}

/* The blue blast + splatter. Layered gradients rather than one image so it
   scales with the machine instead of pixelating. */
.mq-spray {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(38% 120% at 50% 58%, rgba(27, 92, 255, .95) 0%, rgba(27, 92, 255, .45) 42%, transparent 74%),
    radial-gradient(22% 90% at 22% 40%, rgba(61, 139, 255, .5) 0%, transparent 70%),
    radial-gradient(20% 80% at 80% 62%, rgba(61, 139, 255, .42) 0%, transparent 70%);
}
/* The splatter itself: irregular blue blobs and a scatter of ink flecks. */
.mq-spray::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 150' preserveAspectRatio='none'%3E%3Cg fill='%231B5CFF'%3E%3Cpath d='M30 88 Q74 18 186 52 Q298 8 416 48 Q518 22 566 78 Q502 128 380 102 Q258 142 148 108 Q66 128 30 88Z' opacity='.62'/%3E%3Cellipse cx='118' cy='42' rx='72' ry='21' opacity='.4'/%3E%3Cellipse cx='472' cy='112' rx='82' ry='19' opacity='.36'/%3E%3Ccircle cx='300' cy='26' r='15' opacity='.5'/%3E%3Ccircle cx='548' cy='42' r='9' opacity='.45'/%3E%3Ccircle cx='58' cy='128' r='12' opacity='.42'/%3E%3C/g%3E%3Cg fill='%230B0B0C'%3E%3Ccircle cx='208' cy='16' r='4'/%3E%3Ccircle cx='226' cy='29' r='2.6'/%3E%3Ccircle cx='390' cy='138' r='3.4'/%3E%3Ccircle cx='408' cy='126' r='2.2'/%3E%3Ccircle cx='86' cy='70' r='3'/%3E%3Ccircle cx='504' cy='18' r='2.8'/%3E%3Ccircle cx='150' cy='140' r='2.4'/%3E%3Ccircle cx='460' cy='22' r='2.2'/%3E%3C/g%3E%3C/svg%3E");
  background-size: 100% 100%;
  opacity: .9;
}

/* Drips running off the marquee's bottom edge. */
.mq-drip {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 34px;
  pointer-events: none;
  overflow: hidden;
}
.mq-drip span {
  position: absolute;
  bottom: 0;
  width: 3px;
  border-radius: 0 0 2px 2px;
  background: linear-gradient(180deg, rgba(27, 92, 255, .9), rgba(27, 92, 255, .25));
}

.marquee-flank {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: 1px;
  font-family: var(--px);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .1em;
  line-height: 1.12;
  color: var(--bone);
  text-shadow: 0 2px 0 rgba(0, 0, 0, .7);
}
.marquee-flank.left  { align-items: flex-start; transform: rotate(-2.5deg); }
.marquee-flank.right { align-items: flex-end; text-align: right; transform: rotate(2deg); }

/* The wordmark: heavy pixel type in bone, outlined in ink so it reads as sprayed
   OVER the plate rather than sitting on it. */
.wordmark {
  position: relative;
  z-index: 2;
  margin: 0;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  transform: rotate(-1.5deg);
}
.wm-text {
  font-family: var(--px);
  font-size: 66px;
  font-weight: 700;
  line-height: .84;
  letter-spacing: .015em;
  color: var(--bone);
  /* A thick ink outline built from offsets — `-webkit-text-stroke` alone draws
     centred and eats the letterform at this weight. */
  text-shadow:
    3px 0 0 var(--ink), -3px 0 0 var(--ink), 0 3px 0 var(--ink), 0 -3px 0 var(--ink),
    2px 2px 0 var(--ink), -2px 2px 0 var(--ink), 2px -2px 0 var(--ink), -2px -2px 0 var(--ink),
    4px 4px 0 var(--ink), -4px 4px 0 var(--ink), 4px -4px 0 var(--ink), -4px -4px 0 var(--ink),
    0 9px 0 rgba(0, 0, 0, .5);
}

.crown {
  position: absolute;
  top: -22px;
  left: 50%;
  width: 40px;
  height: 35px;
  margin-left: -46px;
  transform: rotate(-9deg);
  filter: drop-shadow(2px 3px 0 rgba(11, 11, 12, .7));
  z-index: 3;
}

/* The neon strip — the only glowing surface on the marquee. */
.neon-strip {
  grid-column: 1 / -1;
  position: relative;
  z-index: 2;
  height: 8px;
  margin: 9px -14px 0;
  background: linear-gradient(90deg,
    var(--blue-lo) 0%, var(--blue) 14%, var(--blue-hi) 38%,
    #7FB6FF 50%, var(--blue-hi) 62%, var(--blue) 86%, var(--blue-lo) 100%);
  box-shadow: var(--glow-lg), inset 0 1px 0 rgba(255, 255, 255, .55);
}
.neon-strip::before, .neon-strip::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: 2px;
  background: var(--ink);
  opacity: .55;
}
.neon-strip::before { top: -2px; }
.neon-strip::after  { bottom: -2px; }

/* The machine's own signage — the live figures.
 *
 * ---------------------------------------------------------------------------
 * WHY IT SITS ON ITS OWN PLATE
 * ---------------------------------------------------------------------------
 * It used to sit directly on the marquee's blue spray, in a muted #8FA6CE, and
 * MEASURED that is 3.2:1 — below AA for 10px type. The dimmed "unavailable"
 * variant was worse still, at 1.9:1.
 *
 * That is what made the strip look like its letters were cut in half. They were
 * not: forcing the copy to red showed every glyph whole. The bottom of each
 * letter was simply washing out into the blue behind it, which at small sizes
 * reads as a clipped word rather than as low contrast.
 *
 * A plate fixes it at the source rather than by nudging the colour until it
 * nearly passes: the background becomes a known, flat value, so the ratio is
 * guaranteed wherever the spray happens to be strongest.
 */
.marquee-live {
  grid-column: 1 / -1;
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 3px;
  padding: 5px 10px 7px;
  background: rgba(5, 8, 15, .78);
  border-radius: 5px 5px 0 0;
  font-family: var(--px);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: #B7C6E4;
}
.marquee-live b { color: var(--bone); font-size: 11.5px; }
.ml-sep { opacity: .5; }
.ml-cell { display: inline-flex; align-items: baseline; gap: 4px; white-space: nowrap; }
.ml-cell.unavailable b { color: #8A93A6; }

/* ==========================================================================
   THE BODY — case | panel | side wall
   ========================================================================== */

.machine-body {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--panel-w) var(--wall-w);
  background: linear-gradient(180deg, #101A2E 0%, #0A1224 100%);
  border: var(--line) solid var(--ink);
  border-top: none;
  padding: 9px;
  gap: 8px;
  position: relative;
}

/* ==========================================================================
   b. THE GLASS CASE
   ========================================================================== */

.case {
  position: relative;
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: var(--deep-3);
  border: var(--line) solid var(--ink);
  border-radius: 6px;
  box-shadow:
    inset 0 0 0 3px #0D2452,
    inset 0 8px 20px rgba(0, 0, 0, .8),
    inset 0 -5px 14px rgba(0, 0, 0, .55);
  overflow: hidden;
}

/* The bright neon edge — the thing that makes the case read as lit glass. */
.case-neon {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  border-radius: 6px;
  box-shadow:
    inset 0 0 0 2px rgba(61, 139, 255, .85),
    inset 0 0 18px rgba(27, 92, 255, .45),
    0 0 16px rgba(27, 92, 255, .5);
}

/* The glass: a diagonal sheen and a vignette, over the packs, under the neon. */
.case-glass {
  position: absolute;
  inset: 3px;
  z-index: 4;
  pointer-events: none;
  border-radius: 4px;
  background:
    linear-gradient(116deg, rgba(255, 255, 255, .1) 0%, rgba(255, 255, 255, 0) 30%,
                    rgba(255, 255, 255, 0) 68%, rgba(255, 255, 255, .055) 100%),
    radial-gradient(130% 70% at 50% 0%, rgba(61, 139, 255, .1) 0%, transparent 62%),
    radial-gradient(120% 60% at 50% 104%, rgba(0, 0, 0, .5) 0%, transparent 60%);
}

.shelves {
  position: relative;
  z-index: 2;
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-rows: repeat(4, minmax(0, 1fr));
  padding: 14px 11px 10px;
}

.shelf {
  position: relative;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  align-items: end;
  padding-bottom: 8px;
}

/* The shelf line and its blue underglow. */
.shelf-line {
  position: absolute;
  left: -2px;
  right: -2px;
  bottom: 0;
  height: 4px;
  background: linear-gradient(180deg, #1E4C93 0%, #0A1B3E 100%);
  border-top: 1px solid rgba(61, 139, 255, .5);
  box-shadow: 0 4px 12px rgba(27, 92, 255, .5), 0 0 22px rgba(27, 92, 255, .3);
  border-radius: 2px;
}

/* ==========================================================================
   THE PACKS
   ========================================================================== */

/* ==========================================================================
   THE PACKS

   The hover is built from two halves that are deliberately kept apart:

     · CSS owns the DISCRETE states — the lift, the straightening, the scale and
       the glow — because `:hover` and `:focus-visible` already express those,
       including for keyboard users, which a pointer script cannot.
     · JS owns the CONTINUOUS one — the lean toward the cursor and the light that
       follows it (assets/js/tilt.js). A transition can only move between two
       fixed values; a card that keeps responding while the pointer is on it
       needs a value recomputed per frame.

   So the transform is split across CSS's independent properties, which compose
   in a fixed order — translate, then rotate, then scale, then `transform`:

     translate / rotate / scale   transitioned here; discrete, state-driven
     transform                    NOT transitioned; written every frame by JS,
                                  and a transition would fight the follow and
                                  turn a damped lean into mush
   ========================================================================== */

.pack {
  position: relative;
  z-index: 1;
  aspect-ratio: 5 / 6.1;
  margin: 0 -2px;
  background: var(--deep-2);
  border: 2px solid var(--ink);
  border-radius: 5px;
  overflow: hidden;
  box-shadow: 0 3px 0 rgba(0, 0, 0, .6);

  /* The four values JS writes, and the two CSS drives. */
  --tilt-x: 0deg;
  --tilt-y: 0deg;
  --sheen-x: 50%;
  --sheen-y: 50%;
  --lift: 0px;
  --lean: 0deg;
  --pop: 1;

  /* Just above the base, so a lift reads as the pack rising off the shelf and a
     tilt reads as it leaning out of it. */
  transform-origin: 50% 96%;
  transform: perspective(640px) rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
  translate: 0 var(--lift);
  rotate: var(--lean);
  scale: var(--pop);

  transition:
    translate 380ms var(--ease),
    rotate 380ms var(--ease),
    scale 380ms var(--ease),
    box-shadow 380ms var(--ease),
    filter 380ms var(--ease);
  cursor: pointer;
  padding: 0;
}

/* Alternating lean, so a shelf does not read as a printed sheet. Expressed as a
   custom property rather than a whole `transform`, so the hover rule below can
   straighten it without having to restate the perspective and the tilt. */
.shelf .pack:nth-child(odd)  { --lean: -1.4deg; }
.shelf .pack:nth-child(even) { --lean: 1.2deg; }
.shelf .pack:nth-child(3n)   { --lean: -.3deg; }

/* The hover state. `.shelf` is repeated so this outranks the `nth-child` lean
   above — same specificity, later in the file, so it wins. */
@media (hover: hover) {
  .shelf .pack:hover {
    --lift: -10px;
    --lean: 0deg;
    --pop: 1.045;
  }
}

/* Keyboard focus gets the identical lift and glow — it just has no cursor to
   lean toward, so the tilt stays flat. */
.shelf .pack:focus-visible {
  --lift: -10px;
  --lean: 0deg;
  --pop: 1.045;
}

.shelf .pack:hover,
.shelf .pack:focus-visible {
  z-index: 6;
  box-shadow:
    0 15px 0 rgba(0, 0, 0, .5),
    0 0 0 2px rgba(61, 139, 255, .5),
    0 0 26px rgba(61, 139, 255, .55);
}
.shelf .pack:focus-visible { outline: 2px solid var(--blue-hi); outline-offset: 1px; }

.pack-art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  image-rendering: pixelated;
  background: hsl(var(--hue, 220) 55% 20%);
}

/* The foil sheen. */
.pack::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background: linear-gradient(103deg,
    rgba(255, 255, 255, .34) 0%, rgba(255, 255, 255, .05) 20%,
    rgba(255, 255, 255, 0) 42%, rgba(0, 0, 0, .2) 100%);
  pointer-events: none;
}

/* The crimped top edge. */
.pack::after {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 6px;
  z-index: 2;
  background:
    repeating-linear-gradient(90deg, rgba(11,11,12,.78) 0 2px, transparent 2px 5px),
    linear-gradient(180deg, rgba(11,11,12,.6), transparent);
  pointer-events: none;
}

.pack-crimp {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 6px;
  z-index: 2;
  background:
    repeating-linear-gradient(90deg, rgba(11,11,12,.78) 0 2px, transparent 2px 5px),
    linear-gradient(0deg, rgba(11,11,12,.6), transparent);
  pointer-events: none;
}

/* The price pill. */
.pack-price {
  position: absolute;
  left: 3px;
  bottom: 8px;
  z-index: 3;
  padding: 1px 5px 2px;
  background: rgba(11, 11, 12, .92);
  border: 1px solid rgba(255, 255, 255, .2);
  border-radius: 4px;
  font-family: var(--px);
  font-size: 10.5px;
  font-weight: 700;
  color: var(--bone);
  line-height: 1.15;
  white-space: nowrap;
}

/* The light that travels with the cursor.
 *
 * A separate element rather than a layer on `.pack::before`, because `::before`
 * and `::after` are already the standing foil sheen and the crimped edge — and
 * because an element can be faded with `opacity`, which transitions reliably,
 * where a custom property used as a gradient alpha only animates if it is
 * registered with @property and does not reliably inherit into a pseudo-element.
 *
 * It sits at the pack's own z-index band, under the price pill, and fades in on
 * hover. The position comes from `--sheen-x/--sheen-y`, which tilt.js updates. */
.pack-sheen {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transition: opacity 380ms var(--ease);
  background: radial-gradient(
    145px 145px at var(--sheen-x) var(--sheen-y),
    rgba(255, 255, 255, .5) 0%,
    rgba(255, 255, 255, .16) 40%,
    rgba(255, 255, 255, 0) 74%);
}
@media (hover: hover) {
  .shelf .pack:hover .pack-sheen { opacity: 1; }
}
.shelf .pack:focus-visible .pack-sheen { opacity: 1; }

.pack.is-vended .pack-art { filter: saturate(.3) brightness(.6); }
.pack.is-vended::after { opacity: .6; }

.pack-flag {
  position: absolute;
  top: 7px; left: 3px;
  z-index: 3;
  padding: 1px 4px;
  background: var(--gold);
  border: 1px solid var(--ink);
  border-radius: 3px;
  font-family: var(--px);
  font-size: 8.5px;
  font-weight: 700;
  color: #2A1D00;
  letter-spacing: .04em;
}

/* An empty slot. It must be the SAME box as a pack — same ratio, same negative
   margin — or a shelf with a gap stops lining up with a full one. A faint
   dashed outline says the shelf has room; nothing is drawn that could be
   mistaken for stock. */
.pack-slot-gap {
  aspect-ratio: 5 / 6.1;
  margin: 0 -2px;
  border-radius: 5px;
  border: 1px dashed rgba(111, 168, 255, .09);
}

/* ==========================================================================
   c. THE CONTROL PANEL
   ========================================================================== */

.panel {
  position: relative;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 8px 7px 9px;
  background: linear-gradient(180deg, #0C1B3E 0%, #060D20 100%);
  border: var(--line) solid var(--ink);
  border-radius: 6px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07);
  overflow: hidden;
}

/* ---- the screen ---- */

.screen {
  position: relative;
  flex: none;
  padding: 7px 7px 8px;
  background: #03060E;
  border: 2px solid #123064;
  border-radius: 5px;
  box-shadow: inset 0 0 16px rgba(27, 92, 255, .3), inset 0 2px 7px rgba(0, 0, 0, .85);
  overflow: hidden;
}

.screen-brand {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  padding-bottom: 5px;
  margin-bottom: 5px;
  border-bottom: 1px solid rgba(61, 139, 255, .22);
}
.sb-word {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: .06em;
  color: var(--blue-hi);
  text-shadow: var(--glow-text);
}
.sb-face { width: 30px; height: 26px; flex: none; }

.screen-body {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-family: var(--px);
  font-weight: 700;
  line-height: 1.34;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--blue-hi);
  text-shadow: var(--glow-text);

  /*
   * ---------------------------------------------------------------------------
   * A FIXED HEIGHT, AND A FONT THAT SCALES WITH THE DISPLAY
   * ---------------------------------------------------------------------------
   * MEASURED, before this: this box was `min-height` and its content decided how
   * tall it was. Across the machine's own states it ran from 36px to 106px — so
   * every phase change resized the screen and shoved everything below it down
   * the panel. "RANDOM MEME" was the one that got noticed, because it is the
   * resting state and it wraps, but twelve of the sixteen states did it.
   *
   * `height`, not `min-height`, is the fix: the display is a fixed part of the
   * machine, so its size must not depend on what it happens to be saying.
   *
   * The font then has to fit that box at every size the machine is drawn at.
   * The panel is 21% of the machine, which is 21% of (100vh - 64) — so at
   * 1280x720 the display is only 80px wide and only about ten characters fit at
   * 11.5px. Container units make the font track the display instead of the
   * viewport, so the character count per line stays roughly constant and the
   * text shrinks on a small machine rather than wrapping.
   *
   * The floor matters as much as the ceiling: below about 9px the pixel font
   * stops being legible, so the display would rather show less than show
   * something unreadable. The copy is kept short enough that it does not have to.
   *
   * The rates are derived from the longest line each size has to carry, measured
   * rather than guessed. A character is about 0.62 of the font size at the base
   * weight and about 0.73 at the big one, letter-spacing included:
   *
   *   base  14 chars  ->  W / (14 x 0.62)  = 11.5cqw
   *   big   12 chars  ->  W / (12 x 0.73)  = 10.8cqw   ("OUT OF ORDER")
   */
  container-type: inline-size;
  height: 62px;
  overflow: hidden;
  font-size: clamp(9px, 11cqw, 11.5px);
}
.screen-line { display: flex; align-items: baseline; gap: 5px; }
.screen-line .k { color: var(--blue-ink); opacity: .8; }
.screen-line .v { color: var(--blue-hi); }
.screen-line.big { font-size: clamp(9px, 10.8cqw, 15px); letter-spacing: .06em; }
.screen-line.warn { color: var(--warn); text-shadow: 0 0 8px rgba(255,176,32,.6); }
.screen-line.bad  { color: var(--bad);  text-shadow: 0 0 8px rgba(255,77,61,.6); }
.screen-line.ok   { color: var(--ok);   text-shadow: 0 0 8px rgba(47,211,122,.55); }
.screen-line.dimline { color: var(--blue-ink); opacity: .5; }

/* Scanlines, so it reads as a display. */
.screen::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(180deg, rgba(61,139,255,.05) 0 1px, transparent 1px 3px);
}

/* ---- the PULL button ---- */

.insert {
  position: relative;
  flex: none;
  width: 100%;
  min-height: 46px;
  padding: 6px 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: linear-gradient(180deg, #0B2350 0%, #061229 100%);
  border: 2px solid var(--blue-hi);
  border-radius: 5px;
  box-shadow: 0 0 12px rgba(61, 139, 255, .55), inset 0 0 14px rgba(27, 92, 255, .3);
  font-family: var(--px);
  color: var(--bone);
  transition: transform var(--fast) var(--ease), box-shadow var(--fast) var(--ease), filter var(--fast);
}
.insert:hover:not(:disabled) {
  box-shadow: 0 0 20px rgba(61, 139, 255, .85), inset 0 0 18px rgba(27, 92, 255, .45);
  filter: brightness(1.12);
}
.insert:active:not(:disabled) { transform: translateY(3px); box-shadow: 0 0 8px rgba(61, 139, 255, .5); }
.insert:disabled { filter: saturate(.3) brightness(.6); box-shadow: none; cursor: not-allowed; }

.insert-txt { display: flex; flex-direction: column; align-items: center; line-height: 1.02; }
.insert-label  { font-size: 16px; font-weight: 700; letter-spacing: .09em; }
.insert-label2 { font-size: 16px; font-weight: 700; letter-spacing: .09em; }
.insert-mark { width: 17px; height: 14px; flex: none; }

.insert-sub {
  flex: none;
  text-align: center;
  font-family: var(--px);
  font-size: 8.5px;
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--blue-ink);
  opacity: .85;
  margin-top: -2px;
}

/* ---- currency glyphs ---- */

.pay-glyphs {
  flex: none;
  display: grid;
  /* Four across, so the eight glyphs read as two rows of payment marks rather
     than a block — the brief asks for "a row of small currency glyphs", and a
     tall grid also steals the height the keypad needs. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 3px;
}
.pay-glyphs span {
  /* `aspect-ratio` gives the cell its shape on the narrow desktop panel, and
     `max-height` stops it from ballooning on mobile, where the panel is full
     width and an unclamped ratio turns each glyph into a 90px tile. */
  aspect-ratio: 1.15;
  max-height: 30px;
  display: grid;
  place-items: center;
  background: linear-gradient(180deg, #0E2454 0%, #061127 100%);
  border: 1px solid #14336E;
  border-radius: 4px;
  font-family: var(--px);
  font-size: 11px;
  font-weight: 700;
  color: var(--blue-ink);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .06);
}

/* ---- the keypad ---- */

/*
 * 4 x 3, glowing keys. Decorative — the block is aria-hidden and nothing is
 * bound to a key — but it is the single element that most makes the panel read
 * as a machine rather than a column of buttons, so it earns its space.
 *
 * The keys light under the cursor. That is the whole interaction, and it is
 * deliberately the only one: a key that looked pressed-in and did something
 * would be promising a function this machine does not have.
 */
.keypad {
  flex: none;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 3px;
}
.keypad .key {
  aspect-ratio: 1.5;
  max-height: 24px;   /* same reason as the glyphs: bounded on a wide panel */
  display: grid;
  place-items: center;
  font-family: var(--px);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .02em;
  color: var(--blue-ink);
  background: linear-gradient(180deg, #0E2454 0%, #061127 100%);
  border: 1px solid #14336E;
  border-radius: 4px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .06), 0 2px 0 rgba(0, 0, 0, .55);
  cursor: pointer;
  transition: color var(--fast) var(--ease), background var(--fast) var(--ease),
              box-shadow var(--fast) var(--ease), transform var(--fast) var(--ease);
}
.keypad .key:hover {
  color: var(--bone);
  background: linear-gradient(180deg, #12306C 0%, #0A1C40 100%);
  box-shadow:
    0 0 13px rgba(61, 139, 255, .75),
    inset 0 0 11px rgba(27, 92, 255, .45),
    0 2px 0 rgba(0, 0, 0, .55);
  transform: translateY(-1px);
}

/* ---- card slot, coin slot ---- */

.slots {
  display: flex;
  align-items: flex-end;
  gap: 6px;
}
.slot {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  min-width: 0;
}
.slot-lip {
  width: 100%;
  height: 8px;
  background: #010306;
  border-radius: 2px;
  box-shadow:
    inset 0 2px 6px rgba(0, 0, 0, .95),
    inset 0 0 0 1px rgba(61, 139, 255, .22);
}
/* A coin slot is a vertical mouth, not a card's wide one. */
.slot-coin .slot-lip {
  width: 15px;
  height: 21px;
  border-radius: 3px;
}
.slot-tag {
  font-family: var(--px);
  font-size: 7.5px;
  font-weight: 700;
  letter-spacing: .14em;
  color: #4A6E9E;
  line-height: 1;
}

/* ---- card reader ---- */

.reader {
  flex: none;
  margin-top: auto;
  padding: 6px;
  background: linear-gradient(180deg, #0A1530 0%, #04080F 100%);
  border: 2px solid #12295A;
  border-radius: 5px;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.reader-screen {
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  padding: 0 6px;
  background: #03060E;
  border-radius: 3px;
  box-shadow: inset 0 0 9px rgba(27, 92, 255, .3);
}
.rs-txt {
  font-family: var(--px);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .1em;
  color: var(--blue-hi);
  text-shadow: var(--glow-text);
}
.rs-led {
  width: 7px; height: 7px;
  border-radius: 99px;
  background: var(--ok);
  box-shadow: 0 0 7px rgba(47, 211, 122, .85);
  animation: pulse 2.2s infinite;
}
.reader-num {
  font-family: var(--mono);
  font-size: 8.5px;
  letter-spacing: .1em;
  color: #4A6E9E;
  text-align: right;
}

/* ==========================================================================
   d. THE SIDE WALL
   ========================================================================== */

.sidewall {
  position: relative;
  min-width: 0;
  background: linear-gradient(180deg, #E9E4D6 0%, #D9D2BE 100%);
  border: var(--line) solid var(--ink);
  border-radius: 6px;
  overflow: hidden;
}

.sw-graffiti { position: absolute; inset: 0; }
/*
 * The drawing occupies the lower part of the wall, leaving a clear band at the
 * top for the copy. Without this the crown and the smiley sit directly behind
 * "GOOD MEMES GOOD VIBES" and the two fight each other, which is what made the
 * text look staggered — the letters were fine, the line art behind them was not.
 */
.sw-graffiti svg {
  width: 100%;
  height: calc(100% - 88px);
  margin-top: 88px;
}

.sw-hand {
  position: absolute;
  font-family: var(--px);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .03em;
  color: var(--ink);
  line-height: 1;
  transform: rotate(-4deg);
  opacity: .92;
  /*
   * A bone halo, so the copy stays legible where it crosses the line art behind
   * it. The graffiti SVG is scaled to cover the wall, which puts its crown and
   * smiley outline directly under these words — the last letter of each line was
   * merging into a black stroke and reading as a truncated word. The halo is
   * invisible against the bone and knocks the art out from behind the text.
   */
  text-shadow:
    0 0 3px var(--bone), 0 0 3px var(--bone),
    0 0 6px var(--bone), 0 0 6px var(--bone);
}
.sw-hand--b { font-size: 14px; transform: rotate(3deg); opacity: .8; }

/* The mascot, leaning on the machine's right edge.
 *
 * It used to be 165% wide at `right: -40%`, which pushed it 45px past the wall
 * and 12px below it — and the wall clips, so the frog lost its right side and
 * its feet. Sized to the wall instead: the lean is still there in the pose, and
 * nothing is cut. */
.mascot {
  position: absolute;
  right: -2%;
  bottom: 0;
  width: 104%;
  transform-origin: 50% 100%;
  animation: bob 4.2s ease-in-out infinite;
  filter: drop-shadow(3px 5px 0 rgba(0, 0, 0, .5));
  pointer-events: none;
}
.mascot img { width: 100%; height: auto; }

@keyframes bob {
  0%, 100% { transform: translateY(0) rotate(-1deg); }
  50%      { transform: translateY(-6px) rotate(1deg); }
}

/* ==========================================================================
   e. THE BASE
   ========================================================================== */

.base {
  position: relative;
  flex: none;
  padding: 10px 12px 12px;
  background: linear-gradient(180deg, #EFEAE0 0%, #DCD5C4 100%);
  border: var(--line) solid var(--ink);
  border-top: none;
  border-radius: 0 0 10px 10px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* A faint spray halo on the bone, so the skirt is painted rather than plain. */
.base-splatter {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(24% 60% at 12% 30%, rgba(27, 92, 255, .13) 0%, transparent 70%),
    radial-gradient(20% 50% at 88% 62%, rgba(27, 92, 255, .1) 0%, transparent 70%),
    radial-gradient(30% 40% at 50% 100%, rgba(0, 0, 0, .07) 0%, transparent 72%);
}

.base-graffiti {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* The warm rim light along the bottom edge — the thing that stops the machine
   floating. */
.base::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 3px;
  background: linear-gradient(90deg, transparent 0%, rgba(255, 138, 40, .85) 20%,
              rgba(255, 190, 110, 1) 50%, rgba(255, 138, 40, .85) 80%, transparent 100%);
  filter: blur(1px);
}

/* ---- the dispenser tray ---- */

.tray {
  position: relative;
  z-index: 2;
  align-self: center;
  width: 46%;
  margin-top: 6px;
  padding: 7px;
  background: linear-gradient(180deg, #0A1A3C 0%, #050C1E 100%);
  border: 2px solid var(--ink);
  border-radius: 8px;
  box-shadow: 0 5px 0 rgba(11, 11, 12, .55);
}

.tray-mouth {
  position: relative;
  height: 96px;
  background: #01030A;
  border-radius: 5px;
  box-shadow:
    inset 0 10px 22px rgba(0, 0, 0, .97),
    inset 0 0 0 2px rgba(61, 139, 255, .55),
    inset 0 0 26px rgba(27, 92, 255, .22);
  overflow: hidden;
  display: grid;
  place-items: center;
  transition: box-shadow var(--slow) var(--ease);
}

/* The neon outline around the mouth — the brightest thing in the base. */
.tray-glow {
  position: absolute;
  inset: 7px;
  border-radius: 5px;
  pointer-events: none;
  box-shadow: 0 0 14px rgba(27, 92, 255, .6), inset 0 0 10px rgba(27, 92, 255, .35);
}

.tray-flap {
  position: absolute;
  left: 3px; right: 3px; top: 0;
  height: 20px;
  background: linear-gradient(180deg, #12244F 0%, #08122C 100%);
  border-bottom: 1px solid var(--ink);
  transform-origin: 50% 0;
  transition: transform var(--slow) var(--ease);
  z-index: 2;
}
.machine[data-state="dispensed"] .tray-flap { transform: rotateX(-74deg); }

.tray-label {
  font-family: var(--px);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: rgba(111, 168, 255, .45);
}

.tray-pack {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 72px;
  aspect-ratio: 5 / 6.1;
  margin: -40px 0 0 -36px;
  opacity: 0;
  transform: translateY(-130px) rotate(-15deg) scale(.85);
  pointer-events: none;
  z-index: 3;
}
.tray-pack .pack { margin: 0; cursor: default; }
.tray-pack .pack:hover { transform: none; box-shadow: 0 3px 0 rgba(0,0,0,.6); }

/* The dispense — real weight: it falls, hits, bounces once, settles. */
@keyframes dispense {
  0%   { opacity: 0; transform: translateY(-140px) rotate(-17deg) scale(.8); }
  8%   { opacity: 1; }
  50%  { transform: translateY(0) rotate(-5deg) scale(1.03); }
  61%  { transform: translateY(-14px) rotate(2deg) scale(.99); }
  75%  { transform: translateY(0) rotate(1deg) scale(1); }
  87%  { transform: translateY(-3px) rotate(0deg); }
  100% { opacity: 1; transform: translateY(0) rotate(0deg) scale(1); }
}
.machine[data-state="dispensed"] .tray-pack {
  opacity: 1;
  animation: dispense 900ms var(--ease-drop) both;
}

.machine[data-state="vending"] .tray-mouth {
  box-shadow:
    inset 0 10px 22px rgba(0, 0, 0, .97),
    inset 0 0 0 2px rgba(61, 139, 255, .95),
    inset 0 0 34px rgba(27, 92, 255, .55),
    0 0 22px rgba(27, 92, 255, .6);
}
.machine[data-state="vending"] .case { filter: brightness(.7) saturate(.82); }
.machine[data-state="vending"] .case,
.machine[data-state="dispensed"] .case { transition: filter var(--slow) var(--ease); }
.machine[data-state="dispensed"] .case { filter: brightness(.93); }

/* ---- the result actions ---- */

.result-actions {
  position: relative;
  z-index: 3;
  display: none;
  gap: 7px;
  align-self: center;
  margin-top: 7px;
  width: 62%;
}
.machine[data-state="dispensed"] .result-actions { display: grid; grid-template-columns: 1fr 1fr; }
.result-actions .btn { min-height: 36px; font-size: 12px; padding: 0 6px; }

/* ---- the feet ---- */

.feet {
  position: relative;
  z-index: 2;
  display: flex;
  justify-content: center;
  gap: 62%;
  margin-top: 9px;
  pointer-events: none;
}
.feet span {
  display: block;
  width: 34px;
  height: 12px;
  background: var(--ink);
  border-radius: 0 0 5px 5px;
  box-shadow: 0 3px 0 rgba(0, 0, 0, .35);
}

/* ==========================================================================
   States
   ========================================================================== */

.case-notice {
  position: absolute;
  inset: 3px;
  z-index: 6;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 20px;
  text-align: center;
  background: rgba(4, 10, 26, .93);
}
.machine[data-state="empty"] .case-notice,
.machine[data-state="out_of_order"] .case-notice { display: flex; }

.cn-title {
  font-family: var(--px);
  font-size: 22px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--bone);
}
.machine[data-state="out_of_order"] .cn-title { color: var(--bad); text-shadow: 0 0 14px rgba(255,77,61,.55); }
.cn-body {
  font-size: 12.5px;
  color: #9FB2D8;
  max-width: 30ch;
  line-height: 1.55;
}

/* Out of order: the machine loses its glow entirely. */
.machine[data-state="out_of_order"] .neon-strip {
  background: #24242C;
  box-shadow: none;
}
.machine[data-state="out_of_order"] .case-neon {
  box-shadow: inset 0 0 0 2px rgba(120, 120, 130, .5);
}
.machine[data-state="out_of_order"] .shelf-line {
  background: #1A1A22;
  border-top-color: #33333D;
  box-shadow: none;
}
.machine[data-state="out_of_order"] .insert {
  border-color: #4A4A56;
  box-shadow: none;
  filter: saturate(0) brightness(.6);
}
.machine[data-state="out_of_order"] .screen-line { color: var(--sold); text-shadow: none; }
.machine[data-state="out_of_order"] .sb-word { color: var(--sold); text-shadow: none; }
.machine[data-state="out_of_order"] .tray-glow { box-shadow: none; }
.machine[data-state="out_of_order"] .rs-led { background: #4A4A56; box-shadow: none; animation: none; }

/* Empty: the case is genuinely bare, so the neon stays but nothing is in it. */
.machine[data-state="empty"] .shelf-line {
  box-shadow: 0 4px 12px rgba(27, 92, 255, .22);
}

/* ==========================================================================
   Responsive
   --------------------------------------------------------------------------
   Mobile is the best version of this design, so it is designed first: the
   machine fills the screen and the control column moves BELOW the glass case.

   The tray animation is the one thing that must not be cut at any size. If
   something has to go, a graffiti detail goes.
   ========================================================================== */

@media (max-width: 1100px) {
  .machine { --panel-w: 24%; --wall-w: 13%; }
}

@media (max-width: 940px) {
  .stage {
    padding: 14px 12px 0;
    min-height: 0;
    place-items: start center;
  }
  .machine {
    width: 100%;
    max-width: 600px;
    /* A fixed ratio on a tall thin screen crushes the panel, so on mobile the
       machine takes its natural height instead. */
    aspect-ratio: auto;
  }
  .machine-body {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1.2fr) auto auto;
    gap: 9px;
    padding: 9px;
  }
  .case { min-height: 300px; }
  .sidewall { min-height: 78px; }
  .mascot { right: 2%; bottom: -8%; width: 92px; }
  .sw-hand { display: none; }
  .panel {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas:
      "screen screen"
      "insert insert"
      "sub    sub"
      "glyphs reader"
      "keypad keypad";
    align-items: start;
    gap: 8px;
  }
  .screen { grid-area: screen; }
  .insert { grid-area: insert; }
  .insert-sub { grid-area: sub; margin-top: 0; }
  .pay-glyphs { grid-area: glyphs; }
  /* Without an explicit area the keypad auto-places into an implicit row and
     lands on top of the reader — which is exactly what happened before it was
     named here. */
  .keypad { grid-area: keypad; }
  .reader { grid-area: reader; margin-top: 0; }
  .tray { width: 68%; }
  .result-actions { width: 82%; }
  .feet { gap: 48%; }
}

@media (max-width: 620px) {
  .stage { padding: 10px 8px 0; }
  .marquee { padding: 10px 11px 0; }
  .wm-text {
    font-size: 42px;
    text-shadow:
      2px 0 0 var(--ink), -2px 0 0 var(--ink), 0 2px 0 var(--ink), 0 -2px 0 var(--ink),
      2px 2px 0 var(--ink), -2px 2px 0 var(--ink), 2px -2px 0 var(--ink), -2px -2px 0 var(--ink),
      0 6px 0 rgba(0, 0, 0, .5);
  }
  .crown { width: 26px; height: 23px; top: -14px; margin-left: -30px; }
  .marquee-flank { font-size: 8px; letter-spacing: .06em; }
  .marquee-live { font-size: 8.5px; gap: 4px; }
  .marquee-live b { font-size: 10px; }

  .machine-body { padding: 8px; gap: 8px; }
  .shelves { padding: 10px 8px 8px; }
  .pack-price { font-size: 8.5px; padding: 1px 3px 2px; left: 2px; bottom: 7px; }

  .panel {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "screen" "insert" "sub" "glyphs" "keypad" "reader";
  }
  /* The display sizes itself from its own width (see .screen-body), so it needs
     no override here — on a phone the panel is full width and the clamp lands on
     its ceiling by itself. A fixed size here would override the clamp and make
     the text stop tracking the display. */
  .insert { min-height: 42px; }
  .insert-label, .insert-label2 { font-size: 14px; }
  .tray { width: 82%; padding: 6px; }
  .tray-mouth { height: 84px; }
  .tray-pack { width: 60px; margin: -33px 0 0 -30px; }
  .result-actions { width: 100%; }
  .feet { gap: 40%; margin-top: 7px; }
  .feet span { width: 26px; height: 10px; }
}

/* ==========================================================================
   Short windows — the control panel's height budget
   --------------------------------------------------------------------------
   The panel is a fixed-height cell in a grid, and its children are sized, so
   its content has a hard minimum. When the window is shorter than that minimum
   the LAST control is simply cut off, because the panel used to clip.

   MEASURED, before this: the panel needed 440px and the machine gave it 364px
   at 1366x768, 396px at 1280x800, and 316px at 1280x720. Those are ordinary
   laptop sizes, and the control that vanished was the card and coin slots —
   which is the bottom third of the panel.

   The budget, at a given window height:

     panel height  =  100vh - 68 (reserve) - 137 (marquee) - 147 (base) - 24

   so 100vh - 376. Against a 440px need, that runs out at about 816px. Hence
   the tiers below, each one taking the panel down to meet the room it has, and
   a scroll fallback for windows shorter than any tier can save.

   The thresholds are the arithmetic above with a little margin, not round
   numbers picked by eye. If the panel's contents change, re-derive them —
   `tools/shot.mjs` reports the overflow for every state it captures.
   ========================================================================== */

/* Tier 1 — from 840px down. Saves ~72px, so the panel needs ~368px. */
@media (max-height: 840px) and (min-width: 861px) {
  .panel { gap: 4px; }
  .screen { padding: 6px; }
  .screen-brand { padding-bottom: 4px; margin-bottom: 4px; }
  .insert { min-height: 42px; }
  .insert-label, .insert-label2 { font-size: 14px; }
  .pay-glyphs span { max-height: 22px; font-size: 10px; }
  .keypad .key { max-height: 15px; font-size: 10px; }
  .reader { padding: 5px; gap: 4px; }
  .reader-screen { height: 22px; }
  .slot-lip { height: 7px; }
  .slot-coin .slot-lip { width: 13px; height: 18px; }
  .reader-num { font-size: 8px; }

  /* The base gives room back too — the tray is the one thing that must not be
     cut, but it does not need its full height on a short window. */
  .tray-mouth { height: 78px; }
  .wm-text { font-size: 54px; }
}

/* Tier 2 — from 760px down. Saves another ~52px, so the panel needs ~316px. */
@media (max-height: 760px) and (min-width: 861px) {
  .panel { gap: 2px; padding: 6px 6px 7px; }
  .insert { min-height: 34px; }
  .pay-glyphs span { max-height: 16px; }
  .keypad .key { max-height: 12px; }
  .reader { padding: 3px; gap: 3px; }
  .reader-screen { height: 20px; }
  .reader-num { display: none; }
  .tray-mouth { height: 66px; }
}

/*
 * The last resort. Below this the panel cannot be made to fit by shrinking it,
 * and a control that cannot be reached is worse than a scrollbar — so the panel
 * becomes scrollable. It only actually scrolls when the content exceeds the
 * box, so nothing changes on a normal screen.
 */
@media (max-height: 700px) and (min-width: 861px) {
  .panel { overflow-y: auto; }
  .panel::-webkit-scrollbar { width: 6px; }
  .panel::-webkit-scrollbar-thumb { background: #2A2A33; border-radius: 6px; }
  .panel::-webkit-scrollbar-track { background: transparent; }
}
