/* =====================================================================
   WIN EFFECTS — upgraded
   ---------------------------------------------------------------------
   Appended last so each rule wins over the original above. Every change
   here is about the same thing: making the ladder from a small win to
   the rarest one actually read as a ladder, instead of four tiers that
   all look like "something happened".
   ===================================================================== */

/* ---- The flash ----
   550ms was far too slow to register AS a flash. A real one is roughly
   120ms up and 250ms down; at 550 with the peak at 15% it read as the
   screen fading rather than being struck. It also expands now, so it is
   an impact rather than a wash, and it is tinted by tier from JS. */
#winFlash {
  background: radial-gradient(circle,
    var(--flash-tint, rgba(255,255,255,0.8)) 0%,
    rgba(34, 227, 255, 0.2) 45%,
    transparent 72%);
}
#winFlash.flash-active {
  animation: winFlashPulse 320ms cubic-bezier(0.1, 0.9, 0.2, 1);
}
@keyframes winFlashPulse {
  0%   { opacity: 0; transform: scale(1); }
  8%   { opacity: 1; }
  100% { opacity: 0; transform: scale(1.35); }
}

/* ---- The shake ----
   Moved off .machine-frame and onto .reels. Shaking the frame moved the
   border, the backdrop and the reels as one piece, so the whole cabinet
   slid like a loose panel; shaking only the reels inside a frame that
   stays put reads as something striking the machine.

   Linear, not ease-out: the decay is already written into the keyframe
   amplitudes, and easing it a second time flattened the end into a
   drift. The rotate term is what stops it looking like a slide. */
@keyframes machineShake {
  0%, 100% { transform: translate(0, 0) rotate(0deg); }
  10% { transform: translate(calc(var(--shake-amp, 6px) * -1), 3px) rotate(-0.6deg); }
  20% { transform: translate(var(--shake-amp, 6px), -3px) rotate(0.6deg); }
  30% { transform: translate(calc(var(--shake-amp, 6px) * -0.82), -2px) rotate(-0.45deg); }
  40% { transform: translate(calc(var(--shake-amp, 6px) * 0.82), 2px) rotate(0.45deg); }
  50% { transform: translate(calc(var(--shake-amp, 6px) * -0.62), 3px) rotate(-0.3deg); }
  60% { transform: translate(calc(var(--shake-amp, 6px) * 0.62), -3px) rotate(0.3deg); }
  70% { transform: translate(calc(var(--shake-amp, 6px) * -0.4), 0) rotate(-0.16deg); }
  80% { transform: translate(calc(var(--shake-amp, 6px) * 0.4), 0) rotate(0.16deg); }
  90% { transform: translate(calc(var(--shake-amp, 6px) * -0.18), 0) rotate(0deg); }
}
.reels.shake-active {
  animation: machineShake 560ms linear;
}
/* The frame no longer shakes. Left as an explicit no-op so an older
   cached script that still targets it cannot move the cabinet. */
.machine-frame.shake-active { animation: none; }

/* ---- The edge pulse ----
   The band now grows with the tier instead of every win over tier 3
   getting the same 120px. */
.edge-pulse {
  box-shadow: inset 0 0
    calc(90px + var(--tier, 3) * 26px)
    calc(18px + var(--tier, 3) * 8px)
    var(--pulse-color, rgba(255, 217, 77, 0.85));
}
/* The rainbow tier used to hue-rotate the whole layer — a full-screen
   filter running for 1.2s, which is one of the most expensive things
   this page can do, to recolour a shadow it could simply have been
   given. Same look, none of the cost. */
.edge-pulse[data-tone="rainbow"].active {
  animation: edgePulse 1.2s ease-out forwards, edgeTint 1.2s linear;
}
@keyframes edgeTint {
  0%   { --pulse-color: rgba(var(--violet-mid-rgb), 0.9); }
  33%  { --pulse-color: rgba(34, 227, 255, 0.9); }
  66%  { --pulse-color: rgba(255, 64, 129, 0.9); }
  100% { --pulse-color: rgba(255, 217, 77, 0.9); }
}
@property --pulse-color {
  syntax: '<color>';
  inherits: true;
  initial-value: rgba(255, 217, 77, 0.85);
}

/* ---- The winning symbols ----
   A pop on arrival, then the pulse. JS staggers these by column, so the
   win lights up along the reels in the direction it paid instead of all
   at once. */
@keyframes symbolWinPop {
  0%   { transform: scale(1); }
  55%  { transform: scale(1.22); }
  100% { transform: scale(1.07); }
}
.reel-track img.symbol-glow {
  animation:
    symbolWinPop 340ms cubic-bezier(0.16, 1.2, 0.3, 1.4) both,
    symbolWinPulse 900ms ease-in-out 340ms infinite;
  z-index: 3;
}

/* ---- Everything that did not pay ----
   grayscale(.85) brightness(.42) in 260ms was an abrupt blackout that
   also killed the machine's backdrop art behind the symbols. Lighter,
   slower, and staggered by column from JS, it reads as a spotlight
   closing on the win rather than the board switching off. */
.symbol-dim {
  filter: grayscale(0.7) brightness(0.55);
  transition: filter 420ms cubic-bezier(0.4, 0, 0.2, 1);
}

@media (prefers-reduced-motion: reduce) {
  #winFlash.flash-active { animation: none; }
  .reels.shake-active { animation: none; }
  .reel-track img.symbol-glow { animation: none; }
  .symbol-dim { transition: none; }
}

@media (max-width: 720px) {
  /* The shake is a transform on the element containing five tall
     scrolling tracks; on a phone that is a full recomposite per frame
     for half a second, immediately after the most expensive moment in
     the spin. The flash and the edge pulse already carry the impact. */
  .reels.shake-active { animation: none; }
}

/* The gain chip's icon had NO size rule anywhere in this file.
   GAIN_ART injects raw <img> tags (energy.gif, xp.png, lucky_charm.png),
   so each one rendered at its natural resolution inside the chip — which
   is why a reward chip filled the middle of the screen. It affected
   every chip in the game, not just the chest ones. */
.gain-chip img {
  width: 22px;
  height: 22px;
  object-fit: contain;
  vertical-align: middle;
}
@media (max-width: 720px) {
  .gain-chip img { width: 18px; height: 18px; }
}

/* =====================================================================
   THE VORTEX — the machine-to-machine symbol swap
   ---------------------------------------------------------------------
   The fifteen visible symbols are torn off the reels as clones, flown
   into a singularity at the centre of the machine, and the NEW machine's
   symbols are fired back out along the same paths reversed.

   WHY A SEPARATE LAYER: .machine-frame is overflow:hidden. A symbol
   spiralling toward the centre of the screen would be clipped the moment
   it crossed the frame's edge. This layer is a fixed, full-viewport
   sibling of <main>, so nothing clips it.

   NOTHING HERE IS A HARDCODED SIZE. The reels are responsive — one cell
   measures 140px in one layout and 44px in another — so JS writes two
   custom properties from live measurements and every length below is a
   calc() of one of them:
     --vx-cell   one symbol cell           (getSymbolHeight())
     --vx-fr     the machine frame's short side
   The only literal px are hairline widths, which should stay hairlines
   at any cell size.
   ===================================================================== */

.vx-layer {
  position: fixed;
  inset: 0;
  z-index: 880;             /* over the cabinet, under every overlay */
  display: none;
  pointer-events: none;
  overflow: hidden;         /* the viewport is the only clip */
  contain: strict;          /* its churn cannot invalidate the page's layout */
}
.vx-layer.active { display: block; }

/* ---- the veil: everything behind dims and desaturates ---- */
.vx-veil {
  position: absolute;
  inset: 0;
  opacity: 0;
  background: radial-gradient(
    circle at var(--vx-cx, 50%) var(--vx-cy, 50%),
    transparent 0%,
    rgba(0, 0, 0, 0.55) 55%,
    rgba(0, 0, 0, 0.82) 100%);
  transition: opacity 300ms linear;
}
.vx-layer.charged .vx-veil { opacity: 1; }

/* ---- the core: what the symbols fall into ---- */
.vx-core {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--vx-fr, 400px) * 0.10);
  height: calc(var(--vx-fr, 400px) * 0.10);
  margin-left: calc(var(--vx-fr, 400px) * -0.05);
  margin-top: calc(var(--vx-fr, 400px) * -0.05);
  border-radius: 50%;
  will-change: transform, opacity;
}

/* Alice — a bloom of light with a counter-turning ring. */
.vx-core[data-profile="alice"] {
  background: radial-gradient(circle,
    #fff 0%, var(--vx-hot) 34%, var(--vx-deep) 62%, transparent 78%);
  box-shadow: 0 0 calc(var(--vx-fr, 400px) * 0.09) var(--vx-hot);
}
.vx-core-ring {
  position: absolute;
  inset: -40%;
  border-radius: 50%;
  background: conic-gradient(from 0deg,
    transparent 0deg, var(--vx-cool) 40deg, transparent 90deg,
    transparent 180deg, var(--vx-white) 220deg, transparent 270deg);
  opacity: 0.75;
  mix-blend-mode: screen;
}

/* Fruits — a citrus wheel. No asset: twelve hard conic stops, masked to
   a disc. Cheaper than Alice by one full-screen composited image. */
.vx-core[data-profile="fruits"] {
  background: conic-gradient(
    #ffd94d 0deg 30deg, #ff8a3d 30deg 60deg, #ffd94d 60deg 90deg,
    #ff8a3d 90deg 120deg, #ffd94d 120deg 150deg, #ff8a3d 150deg 180deg,
    #ffd94d 180deg 210deg, #ff8a3d 210deg 240deg, #ffd94d 240deg 270deg,
    #ff8a3d 270deg 300deg, #ffd94d 300deg 330deg, #ff8a3d 330deg 360deg);
  -webkit-mask: radial-gradient(circle, #000 58%, transparent 72%);
  mask: radial-gradient(circle, #000 58%, transparent 72%);
  box-shadow: 0 0 calc(var(--vx-fr, 400px) * 0.08) #ffb800;
}

/* Generic — mechanical, not magical: an iris stopping down. */
.vx-core[data-profile="generic"] {
  background: radial-gradient(circle, #fff 0%, var(--vx-hot) 40%, transparent 70%);
  box-shadow: 0 0 calc(var(--vx-fr, 400px) * 0.07) var(--vx-hot);
}
.vx-blade {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--vx-fr, 400px) * 0.055);
  height: calc(var(--vx-fr, 400px) * 0.085);
  margin-left: calc(var(--vx-fr, 400px) * -0.0275);
  margin-top: calc(var(--vx-fr, 400px) * -0.0425);
  background: linear-gradient(180deg, var(--vx-cool), transparent);
  clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
  transform-origin: 50% 120%;
  opacity: 0.85;
}

/* ---- a flying symbol ---- */
.vx-flier {
  position: absolute;
  left: 0;
  top: 0;
  object-fit: contain;
  will-change: transform, opacity;
  filter: var(--vx-glow, drop-shadow(0 0 6px rgba(255, 255, 255, 0.45)));
}

/* Its trail. Pre-made and reused — never added or removed mid-flight. */
.vx-ghost {
  position: absolute;
  left: 0;
  top: 0;
  object-fit: contain;
  opacity: 0;
  will-change: transform, opacity;
}

/* ---- the current: tangent streamers ---- */
.vx-streamer {
  position: absolute;
  left: 0;
  top: 0;
  width: 2px;
  height: calc(var(--vx-cell, 100px) * 0.9);
  margin-left: -1px;
  border-radius: var(--radius-pill);
  background: linear-gradient(transparent, var(--vx-hot), transparent);
  will-change: transform, opacity;
}
.vx-layer[data-profile="fruits"] .vx-streamer {
  width: 3px;
  height: calc(var(--vx-cell, 100px) * 0.55);   /* pulp, not silk */
}
.vx-layer[data-profile="generic"] .vx-streamer {
  width: 1px;                                    /* hairline */
}

/* ---- embers thrown off as a symbol is swallowed ---- */
.vx-ember {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--vx-cell, 100px) * 0.09);
  height: calc(var(--vx-cell, 100px) * 0.09);
  border-radius: 50%;
  background: var(--vx-white);
  will-change: transform, opacity;
}
/* Fruits throws juice, so its embers are droplets, not sparks. */
.vx-layer[data-profile="fruits"] .vx-ember {
  border-radius: 60% 60% 55% 55% / 70% 70% 40% 40%;
  background: var(--vx-hot);
}
/* Generic throws sparks off a machine. */
.vx-layer[data-profile="generic"] .vx-ember {
  border-radius: 2px;
  background: var(--vx-cool);
}

/* ---- Alice's medium: the spiral the symbols travel through ---- */
.vx-medium {
  position: absolute;
  left: 0;
  top: 0;
  width: min(160vmin, 160vh);
  height: min(160vmin, 160vh);
  margin-left: min(-80vmin, -80vh);
  margin-top: min(-80vmin, -80vh);
  opacity: 0.55;
  mix-blend-mode: screen;
  pointer-events: none;
  will-change: transform;
}

/* ---- the flash at the singularity ---- */
.vx-flash {
  position: absolute;
  inset: 0;
  background: var(--vx-white);
  opacity: 0;
}

/* ---- the label ---- */
.vx-label {
  position: absolute;
  left: 0;
  right: 0;
  top: 20vh;                       /* never over the core */
  text-align: center;
  font-family: 'Orbitron', sans-serif;
  font-size: clamp(0.85rem, 2.4vw, 1.3rem);
  font-weight: 700;
  letter-spacing: 0.9em;
  text-indent: 0.9em;              /* letter-spacing pads the right only */
  color: var(--vx-white);
  text-shadow: 0 0 18px var(--vx-hot);
  opacity: 0;
  transition: opacity 320ms ease, letter-spacing 900ms cubic-bezier(0.16, 1, 0.3, 1);
}
.vx-layer.charged .vx-label {
  opacity: 1;
  letter-spacing: 0.18em;
  text-indent: 0.18em;
}

/* ---- the board while it is empty ---- */
.reel-col.vx-emptied .reel-track { opacity: 0; transition: opacity 180ms linear; }
/* The frame draws inward as it swallows. */
.machine-frame.vx-pulling {
  box-shadow: inset 0 0 calc(var(--vx-fr, 400px) * 0.14) var(--vx-hot, #fff),
              0 0 30px rgba(0, 0, 0, 0.5);
  transition: box-shadow 400ms ease;
}
/* A symbol lands. */
@keyframes vxPuff {
  from { opacity: 0.7; transform: translate(-50%, -50%) scale(0.3); }
  to   { opacity: 0;   transform: translate(-50%, -50%) scale(1.6); }
}
.vx-puff {
  position: absolute;
  width: calc(var(--vx-cell, 100px) * 0.5);
  height: calc(var(--vx-cell, 100px) * 0.5);
  border-radius: 50%;
  background: radial-gradient(circle, var(--vx-white), transparent 70%);
  animation: vxPuff 380ms ease-out forwards;
}

/* Generic only: the drain reads as a machine reading itself out. */
.vx-scanlines {
  position: absolute;
  inset: 0;
  opacity: 0.06;
  background: repeating-linear-gradient(
    180deg, var(--vx-cool) 0 1px, transparent 1px 4px);
}

/* =====================================================================
   Reduced motion — a different treatment, not a faster one.
   The JS never starts its rAF loop; the core still appears, so the moment
   still reads as a doorway rather than as a stutter.
   ===================================================================== */
@media (prefers-reduced-motion: reduce) {
  .vx-core-ring, .vx-blade, .vx-medium, .vx-scanlines { display: none; }
  .vx-puff { animation: none; opacity: 0; }
  .vx-label { transition: opacity 200ms ease; letter-spacing: 0.18em; text-indent: 0.18em; }
}

/* =====================================================================
   Phone — the effect stays; the decoration that costs frames goes.
   All fifteen symbols still fly. What is dropped is the full-screen
   blend, the per-flier drop-shadow, and the trails.
   ===================================================================== */
@media (max-width: 720px) {
  .vx-layer { --vx-glow: none; }
  .vx-flier { filter: none; }
  .vx-ghost { display: none; }
  .vx-medium { mix-blend-mode: normal; opacity: 0.35;
               width: min(120vmin, 120vh); height: min(120vmin, 120vh);
               margin-left: min(-60vmin, -60vh); margin-top: min(-60vmin, -60vh); }
  .vx-scanlines { display: none; }
  .vx-label { letter-spacing: 0.4em; text-indent: 0.4em; }
}

/* --- vortex tuning, after seeing it run --- */

/* The medium was drowning the page. At 0.55 with screen blend over a
   160vmin square it lifted every pixel behind it, so the HUD washed out
   instead of receding. Smaller and dimmer: it should be something the
   symbols travel THROUGH, not a filter over the whole site. */
.vx-medium {
  width: min(118vmin, 118vh);
  height: min(118vmin, 118vh);
  margin-left: min(-59vmin, -59vh);
  margin-top: min(-59vmin, -59vh);
  opacity: 0.34;
}

/* The veil has to actually darken, or the medium has nothing to sit
   against. Opaque at the edges, clear at the core. */
.vx-veil {
  background: radial-gradient(
    circle at var(--vx-cx, 50%) var(--vx-cy, 50%),
    rgba(0, 0, 0, 0.30) 0%,
    rgba(0, 0, 0, 0.74) 42%,
    rgba(0, 0, 0, 0.92) 100%);
}

/* 20vh put the label straight through the stats row. Below the machine
   instead, where there is nothing to collide with, and with its own
   plate so it reads over whatever is behind it. */
.vx-label {
  top: auto;
  bottom: 12vh;
  padding: 10px 0;
  background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.72) 18%,
                              rgba(0, 0, 0, 0.72) 82%, transparent);
}
@media (max-width: 720px) {
  .vx-label { bottom: 8vh; }
}

/* --- wheel: the count badge and the one line of copy --- */

/* The badge escapes the button's font-size:0 by setting its own. */
.wheel-spin-count {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 22px;
  height: 22px;
  padding: 0 5px;
  border-radius: var(--radius-pill);
  background: var(--gold-bright, #ffd94d);
  color: #1a1005;
  font-family: 'Orbitron', sans-serif;
  font-size: 12px;
  font-weight: 900;
  line-height: 22px;
  text-align: center;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.wheel-spin-count[hidden] { display: none; }

.wheel-hint[hidden] { display: none; }
.wheel-hint {
  margin: 10px 0 0;
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.6);
  text-align: center;
}

/* Disabled has to LOOK disabled — with no label on the button, the only
   thing telling the player it is dead is how it looks. */
.wheel-spin-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  filter: grayscale(0.7);
}

