/* ============================================================
   DADDY MOOJ — link hub
   Vice City neon sunset x 8-bit arcade
   ============================================================ */

/* The GTA display face, bundled locally. Used for the wordmark, the gate
   prompt, section kickers and the radio station name. */
@font-face {
  font-family: 'Pricedown';
  src: url('assets/pricedown.woff') format('woff');
  font-weight: 400 900;
  font-style: normal;
  font-display: swap;
}

:root {
  --ink:      #0b0116;
  --night:    #12021f;
  --grape:    #3a0a52;
  --magenta:  #d01f7a;
  --pink:     #ff2d95;
  --coral:    #ff5f6d;
  --orange:   #ff8a3d;
  --gold:     #ffc94d;
  --cream:    #ffe9b0;
  --cyan:     #00eaff;

  /* Where the ground meets the sky. Everything in the scene keys off this. */
  --horizon: 63vh;
  /* One "pixel" of the artwork. Scales the whole 8-bit layer with the viewport. */
  --px: clamp(2px, 0.3vw, 3.4px);

  --font-display: 'Pricedown', Impact, Haettenschweiler, sans-serif;
  --font-pixel:   'Press Start 2P', 'Courier New', monospace;
  --font-mono:    'Space Mono', ui-monospace, SFMono-Regular, Menlo, 'Courier New', monospace;

  /* Pointer position, -1..1 on each axis. Driven by script.js; every parallax
     plane multiplies these by its own depth. */
  --mx: 0;
  --my: 0;
  --sy: 0; /* scroll offset, in viewport heights */
  --wx: 0; /* weather, 0 = clear dusk, 1 = storm. Driven by scroll. */
  --radio-h: 0px; /* the player sits in the card now; nothing to reserve */

  /* Audio reactive, both 0..1, written by the player each frame. --beat spikes
     on a kick and decays; --bass follows the sustained low end. Both stay 0
     while the track is paused. */
  --beat: 0;
  --bass: 0;
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; overflow: hidden; height: 100%; }

html:has(body.is-gate) {
  overflow: hidden;
  height: 100%;
}

body {
  margin: 0;
  height: 100%;
  height: 100dvh;
  overflow: hidden;
  background: var(--night);
  color: #fff;
  font-family: var(--font-mono);
  -webkit-font-smoothing: antialiased;
}

body.is-gate {
  overflow: hidden;
  height: 100dvh;
}

.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

::selection { background: var(--pink); color: #fff; }


/* ============================================================
   SCENE — fixed, decorative, never interactive
   ============================================================ */
.scene {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

.fallback { position: absolute; inset: 0; }

/* Once the photo has decoded the CSS scene is redundant — drop it entirely so
   its clouds, grid and beacons stop animating behind an opaque image. */
body.has-photo .fallback { display: none; }


/* ============================================================
   PARALLAX PLANES
   Each plane is oversized so translation and rotation never expose an edge.
   ============================================================ */
.plx {
  position: absolute;
  inset: -4%;
  will-change: transform;
}

/* The kick punches the city forward a hair and lifts its exposure. Kept tiny —
   at this scale a 1.5% push reads as a thump, anything more reads as a glitch. */
.plx--photo {
  transform:
    perspective(1200px)
    translate3d(calc(var(--mx) * 12px), calc(var(--my) * 10px + var(--sy) * -90px), 0)
    rotateY(calc(var(--mx) * 1.6deg))
    rotateX(calc(var(--my) * -1.4deg))
    scale(calc(1.08 + var(--beat) * 0.007));
  filter:
    saturate(calc(1 - var(--wx) * 0.55 + var(--beat) * 0.05))
    brightness(calc(1 - var(--wx) * 0.22 + var(--beat) * 0.035))
    hue-rotate(calc(var(--wx) * -18deg));
}

.plx--photo picture,
.plx--photo img {
  display: block;
  width: 100%;
  height: 100%;
}

.plx--photo img { object-fit: cover; object-position: 50% 36%; }

.grade {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(180deg, rgba(9, 1, 20, .66) 0%, rgba(12, 2, 26, .2) 24%, rgba(10, 1, 22, .24) 58%, rgba(7, 0, 16, .72) 100%),
    radial-gradient(115% 85% at 50% 48%, transparent 30%, rgba(7, 0, 16, .55) 100%);
}

/* On a phone the card covers the middle, so the only photo you actually see is
   the top and bottom — exactly where the grade is heaviest. Ease off there. */
@media (max-aspect-ratio: 3/4) {
  .grade {
    background-image:
      linear-gradient(180deg, rgba(9, 1, 20, .44) 0%, rgba(12, 2, 26, .12) 26%, rgba(10, 1, 22, .16) 62%, rgba(7, 0, 16, .5) 100%),
      radial-gradient(125% 70% at 50% 50%, transparent 36%, rgba(7, 0, 16, .42) 100%);
  }
}

/* Soft neon bloom sitting between the photo and the foreground. */
.plx--haze {
  background-image:
    radial-gradient(34vw 26vw at 22% 62%, rgba(255, 45, 149, .16), transparent 70%),
    radial-gradient(30vw 24vw at 78% 55%, rgba(0, 234, 255, .13), transparent 70%),
    radial-gradient(46vw 22vw at 50% 88%, rgba(255, 138, 61, .12), transparent 72%);
  transform: translate3d(calc(var(--mx) * 26px), calc(var(--my) * 20px + var(--sy) * -130px), 0);
}

/* Nearest plane: pixel motes catching the neon. */
.plx--dust {
  background-image:
    radial-gradient(2px 2px at 30px 40px,   rgba(255, 255, 255, .85) 50%, transparent 51%),
    radial-gradient(3px 3px at 148px 96px,  rgba(0, 234, 255, .7)   50%, transparent 51%),
    radial-gradient(2px 2px at 233px 172px, rgba(255, 110, 190, .8) 50%, transparent 51%),
    radial-gradient(3px 3px at 88px 236px,  rgba(255, 210, 120, .6) 50%, transparent 51%);
  background-size: 311px 269px, 397px 331px, 449px 389px, 353px 293px;
  transform: translate3d(calc(var(--mx) * 52px), calc(var(--my) * 40px + var(--sy) * -200px), 0);
  animation: motes 34s linear infinite;
}

@keyframes motes {
  to { background-position: 40px -269px, -60px -331px, 30px -389px, -50px -293px; }
}

/* --- weather --------------------------------------------------------
   --wx is 0 on the first screen (clear dusk) and 1 once you have scrolled
   a full viewport (storm). All of this is opacity/filter, so the photo itself
   never swaps — the city stays, the conditions change. */
.wx {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.wx--tint {
  opacity: var(--wx);
  background:
    linear-gradient(180deg, rgba(8, 18, 38, .55), rgba(4, 10, 24, .28) 40%, rgba(2, 6, 16, .62)),
    radial-gradient(90% 70% at 50% 0%, rgba(70, 110, 170, .22), transparent 60%);
}

.wx--mist {
  opacity: calc(var(--wx) * .4);
  background:
    radial-gradient(70% 40% at 50% 100%, rgba(160, 180, 210, .22), transparent 70%),
    radial-gradient(50% 30% at 20% 80%, rgba(140, 160, 190, .14), transparent 70%);
}

/* The canvas is painted by script.js — real beads with refraction, not streaks.
   Opacity stays at 1 so each drop reads as glass; intensity is drop count. */
.wx--glass {
  display: block;
  width: 100%;
  height: 100%;
}

.wx--sheen {
  opacity: calc(var(--wx) * .35);
  background:
    linear-gradient(118deg, transparent 38%, rgba(255, 255, 255, .1) 49%, transparent 58%),
    radial-gradient(120% 40% at 50% 0%, rgba(200, 220, 255, .12), transparent 55%);
}

/* The card drifts against the scene, which separates it from the backdrop. */
/* The content drifts counter to the scene, which is what sells the depth. It
   rides on .hud rather than .stage because .stage is exactly one viewport tall —
   a few pixels of movement there is enough to summon a scrollbar. */
.hud {
  transform: translate3d(calc(var(--mx) * -6px), calc(var(--my) * -5px), 0);
}

/* The photo is already busy; ease off the 8-bit overlay so it still reads as
   photoreal rather than mush. */
body.has-photo .dither { opacity: .2; }
body.has-photo .dither::after { opacity: .5; }
body.has-photo .scanlines { opacity: .4; }

/* --- sky + sun bloom ------------------------------------------------ */
.sky {
  position: absolute;
  inset: 0;
  background-image:
    /* bloom around the sun */
    radial-gradient(60vh 40vh at 50% var(--horizon), rgba(255, 190, 90, .4), rgba(255, 120, 90, .14) 45%, transparent 72%),
    linear-gradient(
      180deg,
      #14022a 0%,
      #24063d 14%,
      #430b5c 30%,
      #7c1268 44%,
      #b81f74 55%,
      #e8386f 64%,
      #ff6a5a 72%,
      #ff9440 80%,
      #ffc25b 88%,
      #ffe0a0 96%
    );
}

/* --- stars ---------------------------------------------------------- */
.stars {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: var(--horizon);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 42%, transparent 78%);
          mask-image: linear-gradient(180deg, #000 0%, #000 42%, transparent 78%);
}

.stars--far {
  background-image:
    radial-gradient(1.5px 1.5px at 18px 24px,  rgba(255, 255, 255, .95) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 96px 61px,  rgba(255, 255, 255, .70) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 143px 18px, rgba(0, 234, 255, .85)   50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 57px 121px, rgba(255, 255, 255, .60) 50%, transparent 51%);
  background-size: 137px 113px, 179px 151px, 223px 197px, 163px 167px;
  animation: twinkle 7s steps(3, end) infinite alternate;
}

.stars--near {
  background-image:
    radial-gradient(2px 2px at 41px 37px,   rgba(255, 255, 255, 1)    50%, transparent 51%),
    radial-gradient(2px 2px at 122px 88px,  rgba(255, 214, 240, .85) 50%, transparent 51%),
    radial-gradient(2px 2px at 199px 43px,  rgba(255, 255, 255, .75) 50%, transparent 51%);
  background-size: 197px 173px, 241px 211px, 293px 251px;
  animation: twinkle 4.4s steps(2, end) infinite alternate-reverse;
}

@keyframes twinkle {
  from { opacity: .45; }
  to   { opacity: 1; }
}

/* --- the sun: solid top, scan-banded bottom ------------------------- */
.sun {
  --size: clamp(260px, 56vw, 560px);
  position: absolute;
  top: var(--horizon);
  left: 50%;
  width: var(--size);
  height: var(--size);
  transform: translate(-50%, -63%);
  border-radius: 50%;
  background: linear-gradient(180deg, #fff6d4 0%, #ffe07a 22%, #ffb03a 46%, #ff6f4d 70%, #ff2d95 100%);
  /* Two mask layers union together: solid disc on top, hard bands below. */
  -webkit-mask-image:
    linear-gradient(180deg, #000 0 44%, transparent 44%),
    repeating-linear-gradient(180deg, #000 0 calc(var(--px) * 3), transparent calc(var(--px) * 3) calc(var(--px) * 6.5));
          mask-image:
    linear-gradient(180deg, #000 0 44%, transparent 44%),
    repeating-linear-gradient(180deg, #000 0 calc(var(--px) * 3), transparent calc(var(--px) * 3) calc(var(--px) * 6.5));
}

/* --- pixel clouds --------------------------------------------------- */
.cloudbank {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: var(--horizon);
}

.cloud {
  position: absolute;
  top: var(--y);
  left: 0;
  width: calc(var(--px) * 32);
  height: calc(var(--px) * 5);
  opacity: var(--o, .35);
  background: rgba(255, 255, 255, .9);
  transform-origin: left center;
  animation: drift var(--d, 120s) steps(56, end) infinite;
  animation-delay: var(--delay, 0s);
}

.cloud--warm { background: rgba(255, 205, 175, .9); }

.cloud::before,
.cloud::after {
  content: '';
  position: absolute;
  background: inherit;
}

.cloud::before {
  left: calc(var(--px) * 5);
  bottom: calc(var(--px) * 5);
  width: calc(var(--px) * 21);
  height: calc(var(--px) * 5);
}

.cloud::after {
  left: calc(var(--px) * 11);
  bottom: calc(var(--px) * 10);
  width: calc(var(--px) * 11);
  height: calc(var(--px) * 5);
}

@keyframes drift {
  from { transform: translateX(-34vw) scale(var(--s, 1)); }
  to   { transform: translateX(118vw) scale(var(--s, 1)); }
}

/* --- pixel skyline -------------------------------------------------- */
.skyline {
  position: absolute;
  top: var(--horizon);
  left: 50%;
  display: flex;
  align-items: flex-end;
  gap: var(--px);
  width: max-content;
  transform: translate(-50%, -100%);
  filter: drop-shadow(0 0 calc(var(--px) * 6) rgba(255, 45, 149, .35));
}

.skyline i {
  --u: calc(var(--px) * 2);
  --win: rgba(255, 201, 77, .44);
  position: relative;
  display: block;
  width: calc(var(--w) * var(--px));
  height: calc(var(--h) * var(--px));
  background-color: #180427;
  background-image:
    repeating-linear-gradient(90deg, #180427 0 var(--u), transparent var(--u) calc(var(--u) * 2)),
    repeating-linear-gradient(0deg, var(--win) 0 var(--u), transparent var(--u) calc(var(--u) * 2.4));
  box-shadow: inset 0 var(--px) 0 rgba(255, 45, 149, .45);
}

.skyline i:nth-child(3n)   { --win: rgba(0, 234, 255, .5); }
.skyline i:nth-child(4n+1) { --win: rgba(255, 255, 255, .35); }
.skyline i:nth-child(5n)   { --win: rgba(255, 120, 180, .5); }

.skyline i.tower::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 100%;
  width: var(--px);
  height: calc(var(--px) * 9);
  transform: translateX(-50%);
  background: linear-gradient(180deg, #ff3d9a 0 calc(var(--px) * 2), #180427 calc(var(--px) * 2));
  animation: beacon 2.6s steps(2, end) infinite;
}

@keyframes beacon {
  from { filter: brightness(1); }
  to   { filter: brightness(2.2); }
}

/* --- ground plane + horizon light + grid ---------------------------- */
.ground {
  position: absolute;
  top: var(--horizon);
  left: 0;
  right: 0;
  bottom: 0;
  background-image:
    /* the sun's smear on the ground */
    radial-gradient(70vw 26vh at 50% 0%, rgba(255, 150, 80, .5), transparent 70%),
    linear-gradient(180deg, #4a0d50 0%, #2a0533 26%, #170225 58%, #0b0117 100%);
}

.horizon-glow {
  position: absolute;
  top: var(--horizon);
  left: 0;
  right: 0;
  height: 3px;
  transform: translateY(-1px);
  background: linear-gradient(90deg, transparent, rgba(255, 220, 150, .95) 22%, #fff 50%, rgba(255, 220, 150, .95) 78%, transparent);
  box-shadow: 0 0 26px 8px rgba(255, 150, 90, .55);
}

/* A tall plane hinged on the bottom of the viewport. The perspective value is
   derived from the horizon so the vanishing line lands exactly on it. */
.grid {
  --u: calc(var(--px) * 15);
  --depth: calc((100vh - var(--horizon)) * 2.475); /* tan(68deg) */
  position: absolute;
  bottom: 0;
  left: -50%;
  width: 200%;
  height: 500vh;
  background-image:
    repeating-linear-gradient(90deg, rgba(0, 234, 255, .55) 0 2px, transparent 2px var(--u)),
    repeating-linear-gradient(0deg,  rgba(255, 82, 168, .5) 0 2px, transparent 2px var(--u));
  transform-origin: 50% 100%;
  transform: perspective(var(--depth)) rotateX(68deg);
  -webkit-mask-image: linear-gradient(180deg, transparent 0 18%, rgba(0, 0, 0, .35) 46%, #000 82%);
          mask-image: linear-gradient(180deg, transparent 0 18%, rgba(0, 0, 0, .35) 46%, #000 82%);
  animation: gridrun 2.6s steps(14, end) infinite;
}

@keyframes gridrun {
  to { background-position: 0 0, 0 var(--u); }
}

/* --- palms ---------------------------------------------------------- */
/* Rooted just under the horizon so the canopy is backlit by the sunset. */
.palm {
  position: absolute;
  bottom: calc(100% - var(--horizon) - 1.5vh);
  width: clamp(86px, 16vw, 208px);
  height: auto;
  color: #14011f;
  /* Rim light so the silhouette still separates when it overlaps the skyline. */
  filter:
    drop-shadow(0 0 calc(var(--px) * 2) rgba(255, 90, 175, .95))
    drop-shadow(0 0 calc(var(--px) * 9) rgba(255, 45, 149, .5));
}

.palm--left  { left: 1.5%; }
.palm--right { right: 1.5%; transform: scaleX(-1); }

/* --- beat reactive layers -------------------------------------------
   Two overlays over the photo, both driven straight off the CSS variables the
   player writes. Opacity only, no layout, so a dropped frame costs nothing.
   ------------------------------------------------------------------- */
.beat {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Sustained low end: a warm swell up off the street. Quiet in the verse. */
.beat--bass {
  background-image:
    radial-gradient(120% 62% at 50% 108%, rgba(255, 92, 168, .5), transparent 68%),
    radial-gradient(90% 46% at 50% 104%, rgba(255, 168, 84, .34), transparent 72%);
  mix-blend-mode: screen;
  opacity: calc(var(--bass) * .18);
}

/* Kick bloom. Soft and small until the drop raises --beat. */
.beat--flash {
  background-image:
    radial-gradient(70% 90% at 12% 46%, rgba(255, 45, 149, .62), transparent 66%),
    radial-gradient(70% 90% at 88% 42%, rgba(0, 234, 255, .5), transparent 66%),
    radial-gradient(140% 80% at 50% 50%, rgba(255, 255, 255, .1), transparent 60%);
  mix-blend-mode: screen;
  opacity: calc(var(--beat) * .12);
}

@media (prefers-reduced-motion: reduce) {
  .beat { display: none; }
}


/* --- 8-bit dithering, scanlines, vignette --------------------------- */
.dither {
  position: absolute;
  inset: 0;
  background-image: repeating-conic-gradient(rgba(0, 0, 0, .9) 0% 25%, rgba(255, 255, 255, .75) 0% 50%);
  background-size: 4px 4px;
  mix-blend-mode: overlay;
  opacity: .3;
}

/* Coarser second pass so the gradient reads as stepped colour bands. */
.dither::after {
  content: '';
  position: absolute;
  inset: 0;
  background-image:
    repeating-linear-gradient(180deg, rgba(255, 255, 255, .07) 0 3px, rgba(0, 0, 0, .07) 3px 6px),
    repeating-conic-gradient(rgba(0, 0, 0, .9) 0% 25%, rgba(255, 255, 255, .75) 0% 50%);
  background-size: auto, 8px 8px;
  mix-blend-mode: soft-light;
  opacity: .8;
}

.scanlines {
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(180deg, rgba(0, 0, 0, .32) 0 1px, transparent 1px 3px);
  opacity: .55;
  animation: flicker 5.5s steps(6, end) infinite;
}

@keyframes flicker {
  0%, 92%, 100% { opacity: .55; }
  94%           { opacity: .38; }
  96%           { opacity: .62; }
}

.vignette {
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(130% 95% at 50% 42%, transparent 38%, rgba(6, 0, 14, .78) 100%),
    linear-gradient(180deg, rgba(6, 0, 14, .5), transparent 26%);
}

/* --- CRT power-on --------------------------------------------------- */
.crt-boot {
  position: fixed;
  inset: 0;
  z-index: 99;
  background: #fff;
  pointer-events: none;
  animation: boot .8s steps(7, end) forwards;
}

.crt-boot::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 4px;
  background: #fff;
  box-shadow: 0 0 30px 10px rgba(255, 255, 255, .9);
  animation: sweep .8s ease-in forwards;
}

@keyframes boot {
  0%   { opacity: .9; }
  35%  { opacity: .12; }
  62%  { opacity: .3; }
  100% { opacity: 0; visibility: hidden; }
}

@keyframes sweep {
  from { top: 0; opacity: 1; }
  to   { top: 100%; opacity: 0; }
}


/* ============================================================
   STAGE — stacked full-viewport panels, so a little scroll opens
   another social list without leaving the scene.
   ============================================================ */
.stage {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: stretch;
}

.panel {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  min-height: 100dvh;
  padding:
    clamp(20px, 4vw, 40px)
    clamp(14px, 4vw, 24px)
    calc(clamp(24px, 4vw, 36px) + var(--radio-h) + env(safe-area-inset-bottom, 0px));
}

body.is-gate .stage,
body.is-gate .radio {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

body.is-entering .stage,
body.is-in .stage,
body.is-entering .radio,
body.is-in .radio {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}

body.is-entering .stage {
  animation: land 1.05s cubic-bezier(.16, 1, .3, 1) both;
}

@keyframes land {
  from { opacity: 0; transform: scale(1.12); filter: blur(16px); }
  to   { opacity: 1; transform: none; filter: none; }
}

/* Not a card. A floating rounded panel is the link-in-bio house style and has no
   equivalent anywhere in GTA, which puts its UI straight onto the artwork. So no
   border, no radius, no blur. */
.hud {
  position: relative;
  isolation: isolate;
  width: min(100%, 31rem);
  padding: clamp(20px, 4.5vw, 28px) clamp(16px, 4vw, 26px) clamp(14px, 3vw, 18px);
}

body.is-in .hud {
  animation: rise .9s cubic-bezier(.16, 1, .3, 1) both;
}

/* The scrim that buys text contrast. A single ellipse on a pseudo-element rather
   than a background on .hud itself: a vertical gradient would still have hard
   left and right edges, which is the box we are trying not to draw. Bleeding it
   past the content on every side means it reads as haze in the scene. */
.hud::before {
  content: '';
  position: absolute;
  inset: -7% -15%;
  z-index: -1;
  background: radial-gradient(closest-side ellipse at 50% 48%,
    rgba(5, 1, 12, .84), rgba(5, 1, 12, .5) 58%, rgba(5, 1, 12, 0) 100%);
  pointer-events: none;
}

/* Carries the parallax offset through, otherwise fill-mode:both would pin the
   transform to `none` once the entry finishes. */
@keyframes rise {
  from {
    opacity: 0;
    transform: translate3d(calc(var(--mx) * -6px), calc(var(--my) * -5px + 22px), 0) scale(.985);
  }
  to {
    opacity: 1;
    transform: translate3d(calc(var(--mx) * -6px), calc(var(--my) * -5px), 0);
  }
}



/* ============================================================
   HEADER
   ============================================================ */
.hud__head { text-align: center; }

/* The drift lives on the wrapper so the img keeps its own transform for the
   press-down. Two transforms on one element would clobber each other. */
.mark {
  position: relative;
  z-index: 0;
  animation: mark-drift 6.5s ease-in-out infinite;
  will-change: transform;
}

/* Biased upward: the low point barely dips below where it started and the high
   point carries well clear, so it reads as rising out of the letters rather
   than bobbing on them. */
@keyframes mark-drift {
  0%, 100% { transform: translate3d(0, -4px, 0); }
  50%      { transform: translate3d(0, -22px, 0); }
}

/* No frame, no plinth. The artwork's bottom edge is already faded out by
   make-logo.py, so it sits straight on the scene and needs nothing around it. */
.mark img {
  display: block;
  width: clamp(178px, 52vw, 252px);
  height: auto;
  /* Pulled down far enough that the character's legs are covered by the
     wordmark, so it reads as standing behind the type rather than above it.
     The drift then makes it rise out of and settle back into the letters. */
  margin: 0 auto -7%;
  cursor: pointer;
  filter:
    drop-shadow(0 10px 22px rgba(0, 0, 0, .65))
    drop-shadow(0 0 calc(var(--beat) * 10px) rgba(255, 45, 149, calc(var(--beat) * .22)));
  transition: transform .22s cubic-bezier(.2, .8, .25, 1);
}

.mark img:active { transform: scale(.97); }

/* Borderless. A pill or a tag around three words of status is packaging for its
   own sake; the type can just sit there. */
.status {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin: clamp(12px, 2.8vw, 16px) 0 0;
  font-family: var(--font-pixel);
  font-size: clamp(7px, 2.1vw, 8.5px);
  line-height: 1.5;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .7);
}

.status__dot {
  width: 11px;
  height: 11px;
  color: #ff1a1a;
  flex-shrink: 0;
  animation: live-flash .85s steps(1, end) infinite;
}

.status__dot svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: currentColor;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}

@keyframes live-flash {
  0%, 58% { opacity: 1; }
  59%, 100% { opacity: .18; }
}

/* --- the wordmark ---------------------------------------------------
   Pricedown, white, heavy black outline. That is the whole trick behind every
   GTA logo — no gradient, no glow, no colour. The scene supplies the colour; the
   type stays black and white. */
.title {
  position: relative;
  /* Above .mark so the drifting artwork passes behind the letters. */
  z-index: 1;
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(3.9rem, 21vw, 7.2rem);
  line-height: .82;
  letter-spacing: .01em;
  text-transform: uppercase;
  filter:
    drop-shadow(0 2px 0 #000)
    drop-shadow(0 5px 0 rgba(0, 0, 0, .85))
    drop-shadow(0 10px 0 rgba(0, 0, 0, .45))
    drop-shadow(0 16px 12px rgba(0, 0, 0, .55))
    drop-shadow(0 28px 28px rgba(0, 0, 0, .4))
    drop-shadow(0 0 calc(var(--beat) * 10px) rgba(0, 234, 255, calc(var(--beat) * .18)));
}

.title span {
  position: relative;
  isolation: isolate;
  display: block;
  color: #fff;
}

.title span::before {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  z-index: -1;
  color: #000;
  -webkit-text-stroke: .12em #000;
}


.kicker {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.6rem, 13vw, 4.6rem);
  line-height: .84;
  letter-spacing: .02em;
  text-transform: uppercase;
  filter: drop-shadow(0 4px 0 rgba(0, 0, 0, .45));
}

.kicker span {
  position: relative;
  isolation: isolate;
  color: #fff;
}

.kicker span::before {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  z-index: -1;
  color: #000;
  -webkit-text-stroke: .085em #000;
}

.scrollcue {
  position: absolute;
  left: 50%;
  bottom: calc(var(--radio-h) + 14px + env(safe-area-inset-bottom, 0px));
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  transform: translateX(-50%);
  font-family: var(--font-pixel);
  font-size: 7px;
  letter-spacing: .22em;
  text-transform: uppercase;
  text-decoration: none;
  color: rgba(255, 255, 255, .55);
}

.scrollcue svg {
  width: 10px;
  transform: rotate(90deg);
  animation: bob 1.8s ease-in-out infinite;
}

@keyframes bob {
  0%, 100% { transform: rotate(90deg) translateX(0); }
  50%      { transform: rotate(90deg) translateX(5px); }
}


/* ============================================================
   THE END — GTA death-card type, nothing else on the screen.
   ============================================================ */
.panel--end {
  justify-content: center;
  text-align: center;
}

.theend {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(4.4rem, 22vw, 9.4rem);
  line-height: .82;
  letter-spacing: .04em;
  text-transform: uppercase;
  filter: drop-shadow(0 6px 0 rgba(0, 0, 0, .5));
  transform: translate3d(calc(var(--mx) * -6px), calc(var(--my) * -5px), 0);
}

.theend span {
  position: relative;
  isolation: isolate;
  display: block;
  color: #fff;
}

.theend span::before {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  z-index: -1;
  color: #000;
  -webkit-text-stroke: .08em #000;
}

.theend__sub {
  margin: 18px 0 0;
  font-family: var(--font-pixel);
  font-size: 8px;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .4);
  transform: translate3d(calc(var(--mx) * -6px), calc(var(--my) * -5px), 0);
}


/* ============================================================
   LINKS
   ============================================================ */
/* Bare marks. Clean brand glyphs, no tile, glow instead of a heavy stroke. */
.links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(18px, 4.5vw, 32px);
  margin: clamp(18px, 4vw, 26px) 0 0;
}

.link {
  --accent: var(--cyan);
  --accent-dark: var(--accent);
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 4px;
  color: #fff;
  text-decoration: none;
}

body.is-in .link {
  animation: rise-in .6s cubic-bezier(.16, 1, .3, 1) both;
  animation-delay: calc(.2s + var(--i, 0) * .075s);
}

@keyframes rise-in {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

.link--twitch    { --accent: #b48bff; --accent-dark: #772ce8; }
.link--youtube   { --accent: #ff5252; --accent-dark: #e62117; }
.link--discord   { --accent: #8ea1ff; --accent-dark: #5865f2; }
.link--x         { --accent: #ffffff; --accent-dark: #0b0116; }
.link--instagram { --accent: #ff5c9d; --accent-dark: #d6249f; }
.link--tiktok    { --accent: #69c9d0; --accent-dark: #111; }
.link--steam     { --accent: #66c0f4; --accent-dark: #1b2838; }
.link--merch     { --accent: var(--gold); --accent-dark: #9a6200; }
.link--mail      { --accent: #ffffff; --accent-dark: #0b0116; }

.link__icon {
  display: block;
  color: var(--accent);
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .75));
  transition: filter .2s ease, transform .2s ease;
}

.link__icon svg {
  display: block;
  width: clamp(30px, 8vw, 40px);
  height: clamp(30px, 8vw, 40px);
  overflow: visible;
  fill: currentColor;
}

.link__label {
  font-family: var(--font-pixel);
  font-size: clamp(7px, 2vw, 8px);
  line-height: 1.3;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .6);
  transition: color .18s ease;
}

.link:hover .link__icon,
.link:focus-visible .link__icon,
.link.is-sel .link__icon {
  transform: translateY(-4px);
  filter:
    drop-shadow(0 1px 2px rgba(0, 0, 0, .75))
    drop-shadow(0 0 7px var(--accent));
}

.link:hover .link__label,
.link:focus-visible .link__label,
.link.is-sel .link__label {
  color: #fff;
}

.link:focus-visible { outline: none; }

.link:active .link__icon { transform: translateY(-1px) scale(.96); }


/* ============================================================
   FOOTER
   ============================================================ */
.hud__foot {
  margin: clamp(14px, 3vw, 18px) 0 0;
  text-align: center;
}

.hud__foot p {
  margin: 0;
  font-family: var(--font-pixel);
  font-size: 7px;
  line-height: 1.7;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .34);
}


/* ============================================================
   TOAST (cheat-code easter egg)
   ============================================================ */
.toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--radio-h) + 18px + env(safe-area-inset-bottom, 0px));
  z-index: 60;
  padding: 11px 16px;
  border: 2px solid var(--ink);
  border-radius: 10px;
  background: linear-gradient(180deg, #ffdb6b, #ff9a3c);
  box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .3), 0 16px 34px -12px rgba(0, 0, 0, .85);
  color: var(--ink);
  font-family: var(--font-pixel);
  font-size: 8.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  white-space: nowrap;
  opacity: 0;
  transform: translate(-50%, 24px);
  pointer-events: none;
  transition: opacity .25s steps(3, end), transform .25s steps(3, end);
}

.toast.is-on { opacity: 1; transform: translate(-50%, 0); }

/* cheat mode: crank the neon */
body.cheat .scene { filter: saturate(1.4) contrast(1.08); }
body.cheat .grid  { animation-duration: 1s; }
body.cheat .mark img { filter: drop-shadow(0 10px 22px rgba(0, 0, 0, .65)) drop-shadow(0 0 26px var(--cyan)) drop-shadow(0 0 52px var(--pink)); }
body.cheat .title {
  filter:
    drop-shadow(0 2px 0 #000)
    drop-shadow(0 5px 0 rgba(0, 0, 0, .85))
    drop-shadow(0 10px 0 rgba(0, 0, 0, .45))
    drop-shadow(0 16px 12px rgba(0, 0, 0, .55))
    drop-shadow(0 0 30px var(--cyan))
    drop-shadow(0 0 60px var(--pink));
}


/* ============================================================
   TITLE SCREEN GATE
   Dark street, cop lights. No glass pane. Click / Enter / Space
   sucks the veil in and unlocks audio.
   ============================================================ */
.gate {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: grid;
  place-items: center;
}

/* Transitioned rather than swapped, so the city comes up with the iris instead
   of snapping to full brightness the instant the class is dropped. */
.scene { transition: filter .85s cubic-bezier(.5, 0, .2, 1); }

body.is-gate .scene {
  filter: brightness(.34) saturate(.5) contrast(1.15);
}

/* --iris is the radius of the transparent hole punched in the veil, as a
   percentage of the mask gradient. It sits negative at rest so the whole screen
   is opaque; the exit animation drives it out past the corners. */
.gate__frost {
  position: absolute;
  inset: -8%;
  transform-origin: 50% 48%;
  pointer-events: none;
  --iris: -30%;
  -webkit-mask-image: radial-gradient(circle at 50% 48%, transparent var(--iris), #000 calc(var(--iris) + 26%));
          mask-image: radial-gradient(circle at 50% 48%, transparent var(--iris), #000 calc(var(--iris) + 26%));
}

.gate__dark {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 70% 60% at 50% 48%, rgba(0, 0, 0, .35), rgba(0, 0, 0, .78) 70%),
    rgba(0, 0, 0, .55);
}

.gate__red,
.gate__blue {
  position: absolute;
  inset: 0;
}

.gate__red {
  background: radial-gradient(ellipse 75% 90% at 8% 50%, rgba(255, 18, 48, .7), rgba(180, 0, 30, .12) 42%, transparent 68%);
  animation: cop-red 1.45s ease-in-out infinite;
  mix-blend-mode: screen;
}

.gate__blue {
  background: radial-gradient(ellipse 75% 90% at 92% 50%, rgba(28, 78, 255, .7), rgba(10, 30, 160, .12) 42%, transparent 68%);
  animation: cop-blue 1.45s ease-in-out infinite;
  mix-blend-mode: screen;
}

@keyframes cop-red {
  0%, 42%  { opacity: 1; }
  50%, 92% { opacity: .12; }
  100%     { opacity: 1; }
}

@keyframes cop-blue {
  0%, 42%  { opacity: .12; }
  50%, 92% { opacity: 1; }
  100%     { opacity: .12; }
}

.gate__hit {
  position: relative;
  z-index: 1;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  color: inherit;
  cursor: pointer;
  text-align: center;
  -webkit-appearance: none;
  appearance: none;
}

.gate__hit:focus,
.gate__hit:focus-visible { outline: none; }

.gate__push {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(2.6rem, 11vw, 5.4rem);
  line-height: .84;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: #fff;
  -webkit-text-stroke: .08em #000;
  paint-order: stroke fill;
  filter: drop-shadow(0 5px 0 rgba(0, 0, 0, .5));
  text-shadow: 3px 0 rgba(255, 24, 56, .45), -3px 0 rgba(40, 90, 255, .45);
  animation: start-blink 2.2s steps(1, end) infinite;
}

.gate__to {
  display: block;
  margin-top: 16px;
  font-family: var(--font-pixel);
  font-size: clamp(8px, 2.4vw, 10px);
  letter-spacing: .32em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .72);
  animation: start-blink 2.2s steps(1, end) infinite;
}

@keyframes start-blink {
  0%, 70%   { opacity: 1; }
  71%, 100% { opacity: 0; }
}

/* The veil opens outward rather than collapsing inward. The old exit clipped
   the dark layer to a shrinking circle, which drew a hard black disc closing on
   the middle of the screen — the one shape you never want in a transition. Now
   a soft-edged hole grows from the centre and pushes the dark off every edge,
   so the city is revealed instead of being swallowed. */
.gate.is-suck { pointer-events: none; }

.gate.is-suck .gate__frost {
  animation: gate-open .9s cubic-bezier(.5, 0, .2, 1) forwards;
}

.gate.is-suck .gate__hit {
  animation: gate-copy-out .5s cubic-bezier(.5, 0, .2, 1) forwards;
}

/* Registering --iris is what makes it animatable. Where @property is
   unsupported the mask holds still and the veil leaves on opacity and blur
   alone, which still reads fine — just without the opening iris. */
@property --iris {
  syntax: '<percentage>';
  inherits: false;
  initial-value: -30%;
}

@keyframes gate-open {
  0% {
    --iris: -30%;
    opacity: 1;
    transform: scale(1);
    filter: blur(0);
  }
  55% { opacity: 1; }
  100% {
    --iris: 135%;
    opacity: 0;
    transform: scale(1.14);
    filter: blur(18px);
  }
}

@keyframes gate-copy-out {
  0%   { transform: scale(1);    opacity: 1; filter: blur(0); }
  100% { transform: scale(1.35); opacity: 0; filter: blur(10px); }
}


/* ============================================================
   EVENT BANNER — thin pink strip, dismissible.
   ============================================================ */
.event {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 45;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 32px;
  padding: 8px 44px 8px 16px;
  background: var(--pink);
  color: #1a0210;
  box-shadow: 0 2px 0 rgba(0, 0, 0, .35);
}

.event[hidden] { display: none; }

.event__copy {
  margin: 0;
  font-family: var(--font-pixel);
  font-size: clamp(7px, 1.8vw, 9px);
  line-height: 1.4;
  letter-spacing: .12em;
  text-transform: uppercase;
  text-align: center;
}

.event__close {
  position: absolute;
  top: 50%;
  right: 10px;
  transform: translateY(-50%);
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  background: none;
  color: #1a0210;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.event__close svg {
  display: block;
  width: 11px;
  height: 11px;
  margin: auto;
  fill: currentColor;
}

.event__close:hover,
.event__close:focus-visible {
  color: #fff;
  outline: none;
}

body.is-in .event:not([hidden]) {
  animation: event-in .45s cubic-bezier(.16, 1, .3, 1) both;
}

@keyframes event-in {
  from { transform: translateY(-100%); }
  to   { transform: none; }
}

body.has-event .agency {
  top: calc(40px + env(safe-area-inset-top, 0px));
}


/* ============================================================
   AGENCY NOTICE — top right, crossed out, not yet live.
   ============================================================ */
.agency {
  position: fixed;
  top: calc(clamp(14px, 2.6vw, 24px) + env(safe-area-inset-top, 0px));
  right: calc(clamp(14px, 2.6vw, 24px) + env(safe-area-inset-right, 0px));
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 10px 15px;
  border: 1px solid rgba(255, 255, 255, .2);
  background: rgba(6, 1, 14, .66);
  transition: top .35s cubic-bezier(.16, 1, .3, 1);
  -webkit-backdrop-filter: blur(7px);
          backdrop-filter: blur(7px);
  box-shadow: 0 8px 22px rgba(0, 0, 0, .5);
  cursor: not-allowed;
  user-select: none;
  /* Reads as disabled: colour fully drained and sitting back from everything
     else, but not so dark it disappears into the night sky behind it. */
  filter: grayscale(1) brightness(.82);
  opacity: .85;
}

.agency__label {
  display: block;
  font-family: var(--font-pixel);
  font-size: clamp(6.5px, 1.5vw, 8px);
  line-height: 1.85;
  letter-spacing: .17em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .74);
}

/* Square and unstretched, so the pixels stay pixels. Stretching it across the
   whole panel turned the diagonals into smeared rectangles. */
.agency__cross {
  flex: none;
  display: block;
  width: 18px;
  height: 18px;
  color: rgba(255, 255, 255, .4);
  fill: currentColor;
}

/* Its own keyframe rather than the shared rise-in, which would land on full
   opacity and undo the dimming. */
body.is-in .agency {
  animation: agency-in .6s .45s cubic-bezier(.16, 1, .3, 1) both;
}

@keyframes agency-in {
  from { opacity: 0;   transform: translateY(-10px); }
  to   { opacity: .85; transform: none; }
}

@media (max-width: 480px) {
  .agency { padding: 8px 11px; }
}


/* ============================================================
   PLAYER — small, sits in the card under the icon row.
   ============================================================ */
.radio {
  position: relative;
  display: block;
  width: min(210px, 68%);
  margin: clamp(16px, 3.5vw, 22px) auto 0;
  pointer-events: none;
}

body.is-entering .radio,
body.is-in .radio {
  animation: radio-up .7s .5s cubic-bezier(.16, 1, .3, 1) both;
}

@keyframes radio-up {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}

.radio__viz {
  display: block;
  width: 100%;
  height: 26px;
  margin-bottom: 8px;
  pointer-events: none;
}

.radio__row {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 4px 12px 4px 4px;
  border: 1px solid rgba(255, 255, 255, .12);
  border-radius: 999px;
  background: rgba(12, 8, 18, .4);
  -webkit-backdrop-filter: blur(14px) saturate(1.3);
          backdrop-filter: blur(14px) saturate(1.3);
}

.radio__play {
  position: relative;
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: #fff;
  color: #0b0116;
  box-shadow: none;
  cursor: pointer;
  transition: transform .18s ease, background .18s ease;
}

.radio__play:hover,
.radio__play:focus-visible {
  outline: none;
  transform: scale(1.08);
  background: #f3f3f3;
}

.radio__play:active {
  transform: scale(.94);
}

.radio__play svg {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 11px;
  height: 11px;
  fill: currentColor;
  display: block;
}

.radio__play .ico-play { display: none; transform: translateX(1px); }
.radio.is-paused .radio__play .ico-play { display: block; }
.radio.is-paused .radio__play .ico-pause { display: none; }

.radio__vol {
  flex: 1;
  display: flex;
  align-items: center;
  min-width: 0;
}

.radio__vol input {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 3px;
  margin: 0;
  background: linear-gradient(
    to right,
    #fff var(--p, 70%),
    rgba(255, 255, 255, .2) var(--p, 70%)
  );
  border-radius: 99px;
  outline: none;
  cursor: pointer;
}

.radio__vol input::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 10px;
  height: 10px;
  border: 0;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 0 2px rgba(12, 8, 18, .5);
  cursor: pointer;
}

.radio__vol input::-moz-range-thumb {
  width: 10px;
  height: 10px;
  border: 0;
  border-radius: 50%;
  background: #fff;
  cursor: pointer;
}

.radio__vol input::-moz-range-track {
  height: 3px;
  background: transparent;
  border: 0;
}


/* ============================================================
   RESPONSIVE
   ============================================================ */
@media (max-width: 400px) {
  :root { --horizon: 58vh; }
}

/* Short/landscape screens: keep the scene from crowding the card. */
@media (max-height: 620px) {
  :root { --horizon: 70vh; }
  .palm { display: none; }
}

@media (min-width: 900px) {
  :root { --horizon: 60vh; }
}


/* ============================================================
   REDUCED MOTION
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }

  .crt-boot { display: none; }
  .cloud { animation: none; transform: translateX(var(--x, 30vw)) scale(var(--s, 1)); }
  .gate__push,
  .gate__to { animation: none !important; opacity: 1 !important; }
  .gate__red,
  .gate__blue { animation: none !important; opacity: .5; }
  .scrollcue svg { animation: none; }
}
