/* ============================================================
   Birthday invite — sticker-poster scrollytelling
   ============================================================ */

:root{
  --ink:        #f6f2ea;
  --ink-soft:   rgba(246,242,234,.72);
  --ink-faint:  rgba(246,242,234,.4);
  --acid:       #d8ff3e;   /* techno accent */
  --magenta:    #ff3ea5;
  --cyan:       #3ee9ff;
  --amber:      #ffb45c;
  --paper:      #ffffff;   /* sticker borders */
  --max:        44rem;
  --ease:       cubic-bezier(.22,.61,.36,1);
  --pop:        cubic-bezier(.2,1.6,.35,1); /* overshoot for sticker pops */
  --display:    "Arial Black", "Segoe UI Black", ui-sans-serif, system-ui, sans-serif;
  --sans:       ui-sans-serif, system-ui, -apple-system, "Segoe UI", Inter, sans-serif;

  /* ═══ INTRO ANIMATION TUNING ═════════════════════════════
     The feel of each intro move. WHEN things happen (beat order,
     holds, sounds, confetti) lives in content/intro.mjs instead.
     NOTE: soundAt/confetti offsets of the 'drop' beat are timed to
     the ropes catching at 48% of --intro-drop-dur — if you change
     that duration, retime them in content/intro.mjs too. */
  --intro-swipe-dur:      .8s;    /* background wipe sweep */
  --intro-pop-dur:        .5s;    /* card pop-in */
  --intro-flip-dur:       1.15s;  /* host hinge flip-up */
  --intro-flip-overshoot: 18deg;  /* how far the flip swings past vertical */
  --intro-drop-dur:       1.25s;  /* LED board: release → settled */
  --intro-drop-stretch:   3.4vh;  /* how far the ropes stretch at the catch */
  --intro-rope-tilt:      4deg;   /* splay of the two ropes */
  --led-blink-period:     1.5s;   /* LED board blink cycle */

  /* Die-cut white border for anything with class .sticker.
     Works on emoji glyphs and on transparent PNGs alike.
     Four diagonal passes + one soft shadow — each drop-shadow is a
     full filter pass, so the chain is kept as short as it can be
     while still closing the outline. */
  --die-cut:
    drop-shadow( 2.5px  2.5px 0 var(--paper))
    drop-shadow(-2.5px  2.5px 0 var(--paper))
    drop-shadow( 2.5px -2.5px 0 var(--paper))
    drop-shadow(-2.5px -2.5px 0 var(--paper))
    drop-shadow(0 12px 14px rgba(0,0,0,.45));
}

*,*::before,*::after{ box-sizing:border-box; }

html{ scroll-behavior:smooth; }
@media (prefers-reduced-motion: reduce){ html{ scroll-behavior:auto; } }

body{
  margin:0;
  background:#0b0c12;
  color:var(--ink);
  font-family:var(--sans);
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}
body.is-locked{ overflow:hidden; }

.sr-only{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}

/* ── Stickers ──────────────────────────────────────────── */
.sticker{
  filter:var(--die-cut);
  line-height:1;
  user-select:none;
  display:inline-block;
}
/* Image stickers size like emoji ones: every sticker context sets
   font-size from --size, and width:1em rides it — so swoops, cutouts,
   intro stickers and all their responsive scaling rules apply to
   images automatically. */
img.sticker{ width:1em; height:auto; }

/* Text-label sticker: white slab, black shouty type, slightly askew */
.sticker--label{
  filter:drop-shadow(0 12px 14px rgba(0,0,0,.5));
  background:var(--paper);
  color:#0b0c12;
  font-family:var(--display);
  font-size:clamp(.8rem,2.2vw,1.05rem);
  letter-spacing:.06em;
  padding:.55em .9em;
  border-radius:.25em;
  white-space:nowrap;
}

/* ── Language switcher ─────────────────────────────────── */
.lang-switch{
  position:fixed; top:.9rem; right:.9rem; z-index:120;
  display:flex; gap:.25rem;
  padding:.25rem;
  background:rgba(10,10,16,.55);
  border:1px solid rgba(255,255,255,.16);
  border-radius:999px;
}
.lang-switch button{
  padding:.35rem .7rem;
  font-family:var(--display); font-size:.7rem; letter-spacing:.1em;
  color:var(--ink-faint);
  background:transparent; border:0; border-radius:999px; cursor:pointer;
  transition:color .2s, background .2s;
}
.lang-switch button:hover{ color:var(--ink); }
.lang-switch button.is-active{ color:#0b0c12; background:var(--acid); }

/* ── Sticky mute toggle (top left, mirrors the lang pill) ─ */
.mute-btn{
  position:fixed; top:.9rem; left:.9rem; z-index:120;
  width:2.4rem; height:2.4rem;
  display:grid; place-items:center;
  font-size:1.05rem; line-height:1;
  background:rgba(10,10,16,.55);
  border:1px solid rgba(255,255,255,.16);
  border-radius:999px; cursor:pointer;
  transition:border-color .2s, transform .15s var(--pop);
}
.mute-btn:hover{ border-color:var(--acid); transform:scale(1.08); }
.mute-btn.is-muted{ opacity:.75; }

/* ── Language chooser (step zero, above the gate) ──────── */
.chooser{
  position:fixed; inset:0; z-index:130;
  display:grid; place-items:center;
  padding:1.5rem;
  background:
    radial-gradient(900px 600px at 20% 10%,  rgba(216,255,62,.1) 0%, transparent 55%),
    radial-gradient(900px 600px at 80% 90%,  rgba(255,62,165,.12) 0%, transparent 55%),
    linear-gradient(180deg,#101018,#08080c);
  transition:opacity .5s var(--ease), visibility .5s;
}
.chooser[data-state="done"]{ opacity:0; visibility:hidden; pointer-events:none; }
.chooser__inner{ text-align:center; }
.chooser__mark{ font-size:2.5rem; margin-bottom:1rem; }
.chooser__title{
  font-family:var(--display);
  font-size:clamp(1.6rem,5vw,2.4rem); margin:0 0 2rem;
  text-transform:uppercase; letter-spacing:.02em;
}
.chooser__btns{ display:flex; flex-wrap:wrap; gap:1.25rem; justify-content:center; }
.chooser__btns button{
  border:0; cursor:pointer;
  font-size:clamp(1rem,3vw,1.3rem);
  transform:rotate(var(--tilt,0deg));
  transition:transform .18s var(--pop), background .2s;
}
.chooser__btns button:hover{
  transform:rotate(var(--tilt,0deg)) scale(1.08);
  background:var(--acid);
}

/* while choosing, the pill switcher would be redundant */
body.choosing .lang-switch{ display:none; }

/* ── Gate ──────────────────────────────────────────────── */
.gate{
  position:fixed; inset:0; z-index:100;
  display:grid; place-items:center;
  padding:1.5rem;
  background:
    radial-gradient(900px 600px at 15% 0%,   rgba(255,62,165,.18) 0%, transparent 55%),
    radial-gradient(900px 600px at 85% 100%, rgba(62,233,255,.14) 0%, transparent 55%),
    linear-gradient(180deg,#101018,#08080c);
  transition:opacity .7s var(--ease), visibility .7s;
  overflow:hidden;
}
.gate[data-state="open"]{ opacity:0; visibility:hidden; pointer-events:none; }

.gate__stickers{ position:absolute; inset:0; pointer-events:none; }
.gate__sticker{
  position:absolute;
  left:var(--x); top:var(--y);
  font-size:var(--size,4rem);
  transform:translate(-50%,-50%) rotate(var(--tilt,0deg));
  animation:bob 5s var(--ease) infinite alternate;
}
.gate__sticker:nth-child(2n){ animation-duration:6.5s; animation-delay:-2s; }
.gate__sticker:nth-child(3n){ animation-duration:5.7s; animation-delay:-4s; }
@keyframes bob{
  from{ transform:translate(-50%,-50%) rotate(var(--tilt,0deg)) translateY(-6px); }
  to  { transform:translate(-50%,-50%) rotate(var(--tilt,0deg)) translateY(6px); }
}

.gate__inner{ position:relative; width:min(24rem,100%); text-align:center; }
.gate__badge{
  display:inline-block; margin:0 0 1.5rem;
  transform:rotate(var(--tilt,-3deg));
}
.gate__title{
  font-family:var(--display);
  font-size:clamp(1.7rem,6vw,2.5rem); line-height:1.08;
  letter-spacing:-.01em; margin:0 0 .6rem;
  text-transform:uppercase;
}
.gate__hint{ margin:0 0 1.75rem; color:var(--ink-faint); font-size:.9rem; }

#gate-form{ display:flex; flex-direction:column; gap:.6rem; }

#pw{
  width:100%; padding:.9rem 1rem;
  font:inherit; font-size:1rem; text-align:center; letter-spacing:.08em;
  color:var(--ink);
  background:rgba(255,255,255,.06);
  border:2px solid rgba(255,255,255,.18);
  border-radius:.7rem;
  outline:none;
  transition:border-color .2s, background .2s;
}
#pw::placeholder{ color:var(--ink-faint); letter-spacing:normal; }
#pw:focus{ border-color:var(--acid); background:rgba(255,255,255,.09); }

#gate-submit{
  padding:.95rem 1rem;
  font-family:var(--display); font-size:1rem; letter-spacing:.08em;
  color:#0b0c12;
  background:var(--acid);
  border:0; border-radius:.7rem; cursor:pointer;
  transition:transform .15s var(--pop), filter .2s;
}
#gate-submit:hover{ filter:brightness(1.08); transform:rotate(-1deg) scale(1.02); }
#gate-submit:active{ transform:scale(.98); }
#gate-submit[disabled]{ opacity:.55; cursor:progress; }

.gate__error{ min-height:1.2rem; margin:.9rem 0 0; font-size:.85rem; color:#ff8f8f; }

.gate--shake{ animation:shake .4s var(--ease); }
@keyframes shake{
  10%,90%{ transform:translateX(-2px) } 30%,70%{ transform:translateX(4px) }
  50%{ transform:translateX(-6px) } 100%{ transform:translateX(0) }
}

/* While the reel plays (body.is-locked), everything after it —
   tickers, later regions, the route line — steps out of the frame
   and fades back once the reel settles. */
.story > :not(.region--reel){ transition:opacity .6s var(--ease); }
body.is-locked .story > :not(.region--reel){ opacity:0; }

/* ── The reel: a teaser that ends as the first section ───
   Playback state machine (driven by js/intro.js):
     .is-playing  reel running — beat items hidden until their beat
                  (.is-off), end-frame extras (.reel__endbit) hidden
     .is-done     the finished frame: every item resting, captions
                  gone, wipes faded to the page's own hero backdrop */
/* svh on purpose (NOT dvh): dvh grows when a phone's URL bar
   collapses, which reflows this section mid-scroll and makes the
   whole route (and everything below) jump. svh stays constant; the
   brief bottom gap when the bar is hidden at the very top is a
   transient state — browsers re-show the bar at scroll top. */
.region--reel.region--hero{
  min-height:100svh;
  overflow:hidden; /* wipes + off-stage items stay inside */
}

/* The wipe stack. Its base is the dark opening stage; when the reel
   is done the whole stack fades out to the real hero backdrop. */
.reel__wipes{
  position:absolute; inset:0; z-index:1;
  background:radial-gradient(1000px 700px at 50% 40%, #1c1430 0%, #0b0c12 70%);
  transition:opacity 1.2s var(--ease);
}
.region--reel.is-done .reel__wipes{ opacity:0; }

/* Beat items: resting state IS the end frame; .is-off hides them
   until their beat, the animation classes replay their entrance.
   --ty is the vertical anchor: -50% centers on --y (default);
   -100% pins the element's BOTTOM edge to --y (anchor:'bottom'
   in the storyboard — e.g. the host rising from the frame edge). */
.region--reel .sticker[data-anim]{
  position:absolute; z-index:2;
  left:var(--x,50%); top:var(--y,50%);
  font-size:var(--size,6rem);
  transform:translate(-50%,var(--ty,-50%)) rotate(var(--tilt,0deg));
}
.region--reel .sticker[data-anim].is-off{
  transform:translate(-50%,var(--ty,-50%)) rotate(var(--tilt,0deg)) scale(0);
}
.region--reel .sticker[data-anim].pop{
  animation:sticker-pop var(--intro-pop-dur) var(--pop) forwards;
}
@keyframes sticker-pop{
  0%  { transform:translate(-50%,var(--ty,-50%)) rotate(calc(var(--tilt,0deg) - 18deg)) scale(0); }
  70% { transform:translate(-50%,var(--ty,-50%)) rotate(var(--tilt,0deg)) scale(1.14); }
  100%{ transform:translate(-50%,var(--ty,-50%)) rotate(var(--tilt,0deg)) scale(1); }
}

/* Beat sizes are authored in vmin in content/intro.mjs (tracks the
   smaller viewport side everywhere); a beat can additionally carry
   sizeNarrow for phones — e.g. the host blows up to near-full width. */
@media (max-width: 700px){
  .region--reel .sticker[data-anim]{
    font-size:var(--size-narrow, var(--size, 6rem));
  }
}

/* Scene swipe: the whole stage changes — the section's REAL backdrop
   (same photo/tint/mesh rules the scrolled page uses, from the
   generated scenes.css) is revealed behind a slanted sweeping edge
   and STAYS until the next beat's wipe covers it. */
.intro-swipe{
  position:absolute; inset:0; pointer-events:none;
}
/* inside a swipe the photo layer is always on (no crossfade) */
.intro-swipe .backdrop__photo{ opacity:1; }
.intro-swipe--ltr{ animation:swipe-ltr var(--intro-swipe-dur) cubic-bezier(.7,0,.25,1) forwards; }
.intro-swipe--rtl{ animation:swipe-rtl var(--intro-swipe-dur) cubic-bezier(.7,0,.25,1) forwards; }
@keyframes swipe-ltr{
  from{ clip-path:polygon(-20% 0, 0% 0, -12% 100%, -20% 100%); }
  to  { clip-path:polygon(-20% 0, 132% 0, 120% 100%, -20% 100%); }
}
@keyframes swipe-rtl{
  from{ clip-path:polygon(100% 0, 120% 0, 120% 100%, 112% 100%); }
  to  { clip-path:polygon(-32% 0, 120% 0, 120% 100%, -20% 100%); }
}

/* The host cutout starts fully flipped over (180°, hanging upside-down
   below its hinge, off the bottom of the screen) and springs up through
   the hinge into place. */
.region--reel .sticker[data-anim].flip-up{
  transform-origin:50% 100%;
  animation:flip-up var(--intro-flip-dur) cubic-bezier(.3,.55,.35,1) forwards;
}
@keyframes flip-up{
  0%  { opacity:1; transform:translate(-50%,var(--ty,-50%)) rotate(var(--tilt,0deg)) perspective(1100px) rotateX(180deg); }
  58% { transform:translate(-50%,var(--ty,-50%)) rotate(var(--tilt,0deg)) perspective(1100px) rotateX(calc(-1 * var(--intro-flip-overshoot))); }
  76% { transform:translate(-50%,var(--ty,-50%)) rotate(var(--tilt,0deg)) perspective(1100px) rotateX(10deg); }
  90% { transform:translate(-50%,var(--ty,-50%)) rotate(var(--tilt,0deg)) perspective(1100px) rotateX(-4deg); }
  100%{ transform:translate(-50%,var(--ty,-50%)) rotate(var(--tilt,0deg)) perspective(1100px) rotateX(0deg); }
}

/* YOU'RE INVITED as an LED board on two ropes: released from above,
   it free-falls until the ropes snap taut, stretches, and settles. */
/* Bottom-anchored cutouts (anchor:'bottom') dissolve softly over
   their last stretch, so the photo's cut-off edge doesn't read as a
   hard amputation when the section boundary crosses the screen. */
.sticker--grounded{
  -webkit-mask-image:linear-gradient(180deg, #000 88%, transparent 99%);
  mask-image:linear-gradient(180deg, #000 88%, transparent 99%);
}

.led-rig{
  position:absolute; left:var(--x,50%); top:var(--y,20%);
  z-index:3; /* above the persistent background wipes */
  transform:translate(-50%,-50%); /* resting = the end frame */
}
.led-rig.is-off{
  transform:translate(-50%,-50%) translateY(-170vh); /* parked above */
}
.led-rig.release{
  animation:led-drop var(--intro-drop-dur) linear forwards;
}
@keyframes led-drop{
  0%  { transform:translate(-50%,-50%) translateY(-170vh) rotate(-5deg);
        animation-timing-function:cubic-bezier(.55,0,.85,.55); } /* free fall */
  48% { transform:translate(-50%,-50%) translateY(0) rotate(2deg);
        animation-timing-function:cubic-bezier(.2,.9,.35,1); }   /* ropes catch */
  62% { transform:translate(-50%,-50%) translateY(var(--intro-drop-stretch)) rotate(-2.5deg); } /* stretch */
  76% { transform:translate(-50%,-50%) translateY(-1.2vh) rotate(1.4deg); }
  88% { transform:translate(-50%,-50%) translateY(.6vh) rotate(-.6deg); }
  100%{ transform:translate(-50%,-50%) translateY(0) rotate(0deg); }
}

.led-rope{
  position:absolute; bottom:calc(100% - 6px); width:4px; height:180vh;
  background:repeating-linear-gradient(180deg, #c9a86e 0 7px, #8d7245 7px 14px);
  border-radius:2px;
  transform-origin:bottom center;
}
.led-rope--l{ left:14%; transform:rotate(var(--intro-rope-tilt)); }
.led-rope--r{ right:14%; transform:rotate(calc(-1 * var(--intro-rope-tilt))); }

/* Sign variant (led + anim 'deal'): no ropes — a BIG board that pops
   up in place; with z:1 in the storyboard it rises from behind the
   card fan. */
.led-rig--sign{
  transform:translate(-50%,-50%) rotate(var(--tilt,0deg));
}
.led-rig--sign.is-off{
  transform:translate(-50%,-50%) rotate(var(--tilt,0deg)) scale(0);
}
.led-rig--sign.pop{
  animation:sticker-pop var(--intro-pop-dur) var(--pop) forwards;
}
.led-rig--sign .led-board{
  font-size:clamp(1.9rem,6.5vw,3.4rem);
}

/* ── Disco rig: the finale ball on a cord ────────────────
   Drops with the led-drop keyframes; at the cord-catch moment
   (48% of --intro-drop-dur — timed to the musical drop) the ball
   starts spinning and the light comes on, pulsing with --beat. */
.disco-rig{
  position:absolute; left:var(--x,50%); top:var(--y,15%);
  z-index:2;
  transform:translate(-50%,-50%);
}
.disco-rig.is-off{ transform:translate(-50%,-50%) translateY(-170vh); }
.disco-rig.release{ animation:led-drop var(--intro-drop-dur) linear forwards; }
.disco-cord{
  position:absolute; left:50%; bottom:calc(100% - 10px);
  width:3px; height:180vh; transform:translateX(-50%);
  background:linear-gradient(180deg, rgba(255,255,255,.1), rgba(255,255,255,.3));
  border-radius:2px;
}
/* A PROPER disco ball, built from gradients (no emoji, no die-cut):
   shaded sphere + facet grid + hanger. It drops DARK (dimmed) and
   lights up on switchOn. "Rotation" is about the vertical axis: the
   facet columns slide horizontally across the face while the row
   lines stay put — how a spinning mirror ball actually reads. */
.disco-ball{
  position:relative; display:block;
  width:var(--size,8rem); height:var(--size,8rem);
  border-radius:50%;
  background:radial-gradient(circle at 32% 26%,
    #ffffff 0%, #d9dde5 15%, #a9afbb 42%, #5e6470 76%, #2f333c 100%);
  box-shadow:0 12px 26px rgba(0,0,0,.55), inset 0 -10px 20px rgba(0,0,0,.35);
  filter:brightness(.5);
  transition:filter .45s ease;
}
.disco-rig.is-lit .disco-ball,
.region--reel.is-done .disco-ball{ filter:brightness(1); }
/* hanger loop between cord and ball */
.disco-ball::before{
  content:""; position:absolute; left:50%; top:-12%;
  width:15%; height:17%; transform:translateX(-50%);
  border:3px solid #b9bdc7; border-radius:50%;
  z-index:-1;
}
.disco-ball__facets{
  position:absolute; inset:0; border-radius:50%;
  background:
    repeating-linear-gradient(90deg,
      rgba(255,255,255,.17) 0 1px, transparent 1px calc(var(--size,8rem) / 8)),
    repeating-linear-gradient(0deg,
      rgba(10,12,18,.25) 0 1px, transparent 1px calc(var(--size,8rem) / 7));
}
.disco-rig.is-lit .disco-ball__facets,
.region--reel.is-done .disco-ball__facets{
  animation:facet-slide 1.1s linear infinite;
}
@keyframes facet-slide{
  to{ background-position:calc(var(--size,8rem) / 8) 0, 0 0; }
}
/* the bright band sweeping across the face as it turns */
.disco-ball__sheen{
  position:absolute; inset:0; border-radius:50%;
  background:linear-gradient(100deg,
    transparent 32%,
    rgba(255,255,255,.30) 46%,
    rgba(255,255,255,.55) 50%,
    rgba(255,255,255,.30) 54%,
    transparent 68%);
  background-size:260% 100%;
  background-position:100% 0;
  opacity:0; pointer-events:none;
}
.disco-rig.is-lit .disco-ball__sheen,
.region--reel.is-done .disco-ball__sheen{
  opacity:.9;
  animation:sheen-slide 2.4s linear infinite;
}
@keyframes sheen-slide{
  from{ background-position:100% 0; }
  to{ background-position:0% 0; }
}

/* The reflections on the wall: evenly spaced soft light spots
   drifting horizontally across the whole stage (z1 = over the
   backdrop wipes, under the cards). Brightness rides --beat. */
.disco-lights{
  position:absolute; inset:0; z-index:1;
  overflow:hidden; pointer-events:none;
  opacity:0;
  transition:opacity .45s ease;
}
.disco-rig.is-lit + .disco-lights,
.region--reel.is-done .disco-lights{ opacity:1; }
.disco-lights::before{
  content:""; position:absolute; top:0; bottom:0; left:0; right:-560px;
  background-image:
    radial-gradient(circle, rgba(255,250,225,.20) 0 13px, transparent 30px),
    radial-gradient(circle, rgba(216,255,62,.09) 0 10px, transparent 24px);
  background-size:280px 210px, 280px 264px;
  background-position:0 44px, 146px 152px;
  filter:blur(2px);
  opacity:calc(.55 + var(--beat, 0) * .45);
}
.disco-rig.is-lit + .disco-lights::before,
.region--reel.is-done .disco-lights::before{
  /* 560px = exactly two 280px tiles → the loop is seamless */
  animation:lights-sweep 6.5s linear infinite;
}
@keyframes lights-sweep{ to{ transform:translateX(-560px); } }

/* LED street sign: heavy bezel, a visible grid of unlit LEDs across
   the whole panel, and blinking amber dot-matrix text */
.led-board{
  position:relative;
  background:#07070a;
  /* the unlit LED grid — every diode faintly there even when off */
  background-image:radial-gradient(circle, rgba(255,120,40,.14) 1.2px, transparent 1.9px);
  background-size:7px 7px;
  border:6px solid #2b2b34;
  outline:2px solid #0d0d12;
  border-radius:.5rem;
  padding:.65em .9em;
  font-family:var(--display);
  font-size:clamp(1.3rem,4.5vw,2.2rem);
  line-height:1.22; text-align:center; letter-spacing:.18em;
  box-shadow:
    inset 0 0 26px rgba(0,0,0,.9),
    0 22px 36px rgba(0,0,0,.55);
}
.led-text{
  display:block;
  color:#ffb32b;
  /* the glow throbs with the music's kick (--beat, driven by
     js/audio.js) — an LED sign wired to the sound system */
  text-shadow:
    0 0 calc(4px + var(--beat, 0) * 5px)   rgba(255,180,40,.95),
    0 0 calc(16px + var(--beat, 0) * 20px) rgba(255,140,0,calc(.6 + var(--beat, 0) * .3)),
    0 0 calc(36px + var(--beat, 0) * 34px) rgba(255,120,0,calc(.4 + var(--beat, 0) * .25));
  animation:led-blink var(--led-blink-period) steps(1,end) infinite;
}
/* street signs blink: on… off… on… */
@keyframes led-blink{
  0%, 54% { opacity:1; }
  55%, 81%{ opacity:.05; }
  82%,100%{ opacity:1; }
}
/* dot-matrix mask so the glowing letters read as individual LEDs */
.led-board::after{
  content:""; position:absolute; inset:0; border-radius:.3rem; pointer-events:none;
  background-image:radial-gradient(circle, rgba(0,0,0,.45) 1.1px, transparent 1.7px);
  background-size:5px 5px;
}

/* Title cards (beats with text:) — big headline type in the site's
   own voice (h1 style), e.g. the cold open. */
.reel__text{
  font-family:var(--display);
  font-size:var(--size,2.5rem);
  line-height:1.12;
  text-align:center; text-transform:uppercase; letter-spacing:.02em;
  color:var(--ink);
  filter:drop-shadow(0 14px 22px rgba(0,0,0,.55)); /* no die-cut on type */
}
.reel__text .acid{ color:#0b0c12; background:var(--acid); padding:0 .18em; border-radius:.08em; }

/* Transient beats pop away smoothly when the next beat starts
   (the player swaps their animation class for .is-off). */
.region--reel [data-transient]{ transition:transform .3s var(--pop); }
.region--reel.is-done [data-transient]{ display:none; }

/* Teaser captions: one text card per cut, insta-overlay style.
   Transient — they pop with their beat and are gone in the end
   frame (base state is hidden; .is-on shows the active one). */
.reel__caption{
  position:absolute; z-index:4;
  left:50%; top:80%;
  max-width:88vw; text-align:center;
  background:var(--paper); color:#0b0c12;
  font-family:var(--display);
  font-size:clamp(1rem,3.4vw,1.6rem);
  letter-spacing:.04em; text-transform:uppercase;
  padding:.5em .8em; border-radius:.3em;
  filter:drop-shadow(0 12px 14px rgba(0,0,0,.5));
  transform:translate(-50%,-50%) rotate(-2deg) scale(0);
  transition:transform .35s var(--pop);
  pointer-events:none;
}
.reel__caption.is-on{ transform:translate(-50%,-50%) rotate(-2deg) scale(1); }

/* End-frame extras (lede/date, scroll cue, replay): hidden while
   the reel plays, fading in once it settles. */
.reel__endbit{
  opacity:0; pointer-events:none;
  transition:opacity .8s var(--ease) .35s;
}
.region--reel.is-done .reel__endbit{ opacity:1; pointer-events:auto; }

/* Lower-left corner, clear of the bottom-anchored host and
   balancing the replay pill in the opposite corner. */
.reel__end{
  position:absolute; z-index:5;
  left:clamp(1rem,5vw,3rem); bottom:calc(2% + 6.5rem); /* above the cue */
  width:max-content; max-width:60vw; text-align:left;
  /* dark chip so the tagline stays readable over the white suit */
  padding:.65rem .95rem;
  background:rgba(10,10,16,.66);
  border:1px solid rgba(255,255,255,.14);
  border-radius:.8rem;
}
.reel__lede{ margin:0 0 .25rem; font-size:clamp(1.05rem,2.6vw,1.3rem); color:var(--ink); }
.reel__date{
  margin:0; font-family:var(--display);
  font-size:clamp(1rem,2.4vw,1.25rem); letter-spacing:.08em;
  text-transform:uppercase; color:var(--acid);
}
.region--reel .scroll-cue{
  position:absolute; z-index:5;
  left:clamp(1rem,5vw,3rem); bottom:2%;
  margin:0;
  width:max-content;
}
.reel__replay{
  position:absolute; z-index:6;
  right:1.1rem; bottom:1.1rem;
  padding:.5rem .85rem;
  font-family:var(--display); font-size:.68rem; letter-spacing:.12em;
  text-transform:uppercase;
  color:var(--ink-soft);
  background:rgba(10,10,16,.55);
  border:2px solid rgba(255,255,255,.22);
  border-radius:999px; cursor:pointer;
  transition:color .2s, border-color .2s, transform .15s var(--pop);
}
.reel__replay:hover{ color:var(--acid); border-color:var(--acid); transform:rotate(-1.5deg) scale(1.04); }


/* On phones the host blows up to near-full width (sizeNarrow) and
   owns the lower half — the info moves from the corner to FRONT AND
   CENTER over the cutout (z5, above his z3). */
@media (max-width: 700px){
  .reel__end{
    left:50%; bottom:10%;
    transform:translateX(-50%);
    text-align:center; max-width:92vw; width:max-content;
  }
  .region--reel .scroll-cue{
    left:50%; bottom:1.5%;
    transform:translateX(-50%);
    text-shadow:0 1px 8px rgba(0,0,0,.85);
  }
}

@media (prefers-reduced-motion: reduce){
  .region--reel .sticker[data-anim].pop,
  .region--reel .sticker[data-anim].flip-up,
  .led-rig.release, .led-rig--sign.pop,
  .disco-rig.release,
  .disco-ball__facets, .disco-ball__sheen, .disco-lights::before{ animation:none; }
  .region--reel.is-done .disco-lights{ opacity:.6; }
  .led-text{ animation:none; }
  .reel__wipes, .reel__endbit, .reel__caption{ transition:none; }
}

/* ── Backdrop moods ────────────────────────────────────── */
.backdrop{
  position:fixed; inset:0; z-index:0;
  background:#0b0c12;
  transition:background 1.1s var(--ease);
}
/* ── Blurred photo layers, duotone-tinted per scene ─────
   Heavy blur abstracts any phone photo into atmosphere; the ::after
   overlay pulls it into the region's theme color. The url() layer
   sits on top of a generated color-mesh, so a missing photo simply
   shows the mesh — same mood, zero assets required. */
.backdrop__photo{
  position:absolute; inset:-6%;
  background-size:cover; background-position:center;
  filter:blur(16px) saturate(.55) brightness(.5);
  opacity:0;
  transition:opacity 1.1s var(--ease);
  will-change:transform;
  transform:scale(1.12);
}
.backdrop__photo::after{
  content:""; position:absolute; inset:0;
  background:
    linear-gradient(180deg, rgba(11,12,18,.25) 0%, rgba(11,12,18,.72) 100%),
    var(--tint, transparent);
}
/* WHICH photo shows for which scene, plus each scene's photo/tint/
   mesh and flat ground mood, is generated into scenes.css from
   content/scenes.mjs — edit scenes there, not here. */

.backdrop__grain{
  position:absolute; inset:0; opacity:.055; pointer-events:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Per-section beat glows: each section carries its own light that
   swells with the kick (--beat, from js/audio.js) and SCROLLS WITH
   its content — the hero's is a halo from the LED board, the tram's
   its dayrave sun, the venue's its neon wall. Per-scene gradients
   come from content/scenes.mjs via the generated scenes.css. */
.scene-pulse{
  position:absolute; inset:0; z-index:0; pointer-events:none;
  opacity:min(1, calc(var(--beat, 0) * 1.4));
}

/* ── Progress bar ──────────────────────────────────────── */
.progress{
  position:fixed; top:0; left:0; right:0; height:3px; z-index:20;
  background:rgba(255,255,255,.07);
}
.progress i{
  display:block; height:100%; width:100%;
  background:linear-gradient(90deg,var(--acid),var(--cyan),var(--magenta));
  transform-origin:left;
  transform:scaleX(0);
  will-change:transform;
}

/* ── Regions ───────────────────────────────────────────── */
.site{ position:relative; }
.story{ position:relative; z-index:1; }

.region{
  position:relative;
  min-height:130svh;               /* room for cutouts to breathe */
  display:grid; place-items:center;
  padding:clamp(4rem,12vh,8rem) clamp(1.25rem,6vw,3rem);
  /* Per-region accent: same components, different theme */
  --ra:var(--acid);
}
.region--tram { --ra:var(--cyan); }
.region--venue{ --ra:var(--magenta); }
.region__inner{ width:min(var(--max),100%); position:relative; z-index:2; }

.region--hero{ text-align:center; min-height:100svh; }

.kicker{
  margin:0 0 1rem;
  font-size:.75rem; letter-spacing:.24em; text-transform:uppercase;
  color:var(--ra, var(--acid));
  font-weight:700;
}

h1{
  font-family:var(--display);
  font-size:clamp(3.2rem,14vw,8rem); line-height:.92;
  letter-spacing:-.02em; margin:0 0 1.5rem;
  text-transform:uppercase;
}
h1 .acid{ color:var(--acid); }

h2{
  font-family:var(--display);
  font-size:clamp(2.2rem,8vw,4.5rem); line-height:.95;
  letter-spacing:-.015em; margin:0 0 1.25rem;
  text-transform:uppercase;
}

/* Hollow second line of a region headline, tinted per region */
.outline{
  color:transparent;
  -webkit-text-stroke:2.5px var(--ra, var(--ink));
  text-stroke:2.5px var(--ra, var(--ink));
}
@supports not ((-webkit-text-stroke:1px #fff) or (text-stroke:1px #fff)){
  .outline{ color:var(--ra, var(--ink)); }
}

p{ margin:0 0 1.15rem; font-size:clamp(1rem,2.2vw,1.15rem); line-height:1.7; color:var(--ink-soft); }
.lede{ font-size:clamp(1.05rem,2.6vw,1.3rem); color:var(--ink); }
.region--hero .lede{ margin-inline:auto; max-width:30rem; }

.fact-card{
  margin-top:2rem;
  display:grid; gap:0;
  border:2px solid rgba(255,255,255,.14);
  border-radius:1rem;
  /* no backdrop-filter: blurring the moving page behind the card is
     one of the most expensive things a scrolling page can do */
  background:rgba(12,12,18,.78);
  overflow:hidden;
}
.fact-card > div{
  display:flex; flex-wrap:wrap; gap:.25rem 1rem;
  justify-content:space-between; align-items:baseline;
  padding:1rem 1.25rem;
}
.fact-card > div + div{ border-top:1px solid rgba(255,255,255,.1); }
.fact-card strong{
  font-family:var(--display); font-size:.75rem; letter-spacing:.14em;
  text-transform:uppercase; color:var(--ra, var(--acid));
}
.fact-card span{ color:var(--ink); font-size:.95rem; }

/* links inside content (map links etc.) */
.fact-card a, .faq p a{
  color:var(--ink);
  text-decoration:underline;
  text-decoration-color:var(--ra, var(--acid));
  text-decoration-thickness:2px;
  text-underline-offset:3px;
}
.fact-card a:hover, .faq p a:hover{ color:var(--ra, var(--acid)); }

/* Game lineup chips (venue) — reusable in any region */
.lineup{
  display:flex; flex-wrap:wrap; gap:.6rem;
  margin-top:1.75rem;
}
.lineup__chip{
  display:inline-flex; align-items:center; gap:.5rem;
  padding:.55rem 1rem;
  border:2px solid rgba(255,255,255,.16);
  border-radius:999px;
  background:rgba(255,255,255,.05);
  font-weight:600; font-size:.95rem; color:var(--ink);
  transition:border-color .2s, transform .2s var(--pop);
}
.lineup__chip:hover{ border-color:var(--ra, var(--acid)); transform:rotate(-1.5deg) scale(1.04); }
.lineup__chip em{ font-style:normal; font-size:1.25rem; line-height:1; }
/* Set-time variant (tram lineup): time slot in the region accent */
.lineup__chip strong{
  font-family:var(--display); font-size:.75rem; letter-spacing:.08em;
  color:var(--ra, var(--acid));
}

.scroll-cue{ margin-top:3rem; color:var(--ink-faint); font-size:.7rem; letter-spacing:.25em; text-transform:uppercase; }
.scroll-cue i{ display:block; width:1px; height:3rem; margin:.75rem auto 0; background:linear-gradient(var(--ink-faint),transparent); animation:cue 2.2s var(--ease) infinite; }
@keyframes cue{ 0%,100%{ opacity:.25; transform:scaleY(.6) } 50%{ opacity:1; transform:scaleY(1) } }

/* ── Marquee ticker strips between regions ─────────────── */
.ticker{
  position:relative; z-index:1;
  width:106vw; margin:clamp(-3rem,-6vh,-2rem) -3vw;
  transform:rotate(-1.6deg);
  padding:.55rem 0;
  overflow:hidden;
  border-block:2px solid rgba(255,255,255,.1);
  background:rgba(10,10,16,.45);
}
.ticker--reverse{ transform:rotate(1.4deg); }

/* The first ticker after the reel must not ride into its frame: the
   reel's end state owns the whole first viewport, so this one starts
   fully below the fold (2vw clears the lift from the rotation) and
   only shows up once you scroll. */
.region--reel + .ticker{ margin-top:2vw; }
.ticker__track{
  display:flex; width:max-content;
  animation:ticker-scroll 30s linear infinite;
}
.ticker--reverse .ticker__track{ animation-direction:reverse; }
.ticker__track span{
  font-family:var(--display);
  font-size:clamp(1.3rem,3.6vw,2.1rem);
  letter-spacing:.14em; text-transform:uppercase; white-space:nowrap;
  color:transparent;
  -webkit-text-stroke:1.5px var(--ra, var(--ink-faint));
  text-stroke:1.5px var(--ra, var(--ink-faint));
}
@supports not ((-webkit-text-stroke:1px #fff) or (text-stroke:1px #fff)){
  .ticker__track span{ color:var(--ra, var(--ink-faint)); }
}
@keyframes ticker-scroll{ to{ transform:translateX(-50%); } }

@media (prefers-reduced-motion: reduce){
  .ticker__track{ animation:none; }
}

/* ── Tram line 34: a VBZ-style network-map route drawn by the
      scroll — solid theme-colored legs on a pale casing, white stop
      dots with dark rings, Haltestelle name labels, and the line
      number badge at the departure point ─────────────────── */
.route-svg{
  position:absolute; inset:0; z-index:0;
  pointer-events:none;
}
/* the line itself stays subtle; signage (stops, plates, the riding
   badge) stays crisp at full opacity */
.route-svg .route-lines{ opacity:.7; }
/* narrow screens: the line inevitably crosses the copy — keep it a
   whisper under the text instead of competing with it */
@media (max-width: 700px){
  .route-svg .route-lines{ opacity:.25; }
  .route-svg .route-label{ display:none; }
}
.route-svg .route-stop{
  fill:#0b0c12;
  stroke:var(--paper); stroke-width:3.5;
  transition:fill .35s var(--ease), transform .35s var(--pop);
  transform-box:fill-box; transform-origin:center;
}
.route-svg .route-stop.lit{
  fill:var(--paper);
  transform:scale(1.25);
}
/* Haltestelle plates: ZVV blue, white H emblem, white station name */
.route-svg .route-label{
  opacity:0;
  transition:opacity .5s var(--ease);
}
.route-svg .route-label.lit{ opacity:1; }
.route-svg .route-label rect{
  fill:#0069b4;
  stroke:rgba(255,255,255,.85); stroke-width:1.2;
  filter:drop-shadow(0 3px 6px rgba(0,0,0,.45));
}
.route-svg .route-h-ring{
  fill:none; stroke:#fff; stroke-width:1.6;
}
.route-svg .route-h{
  font-family:var(--sans); font-weight:700;
  font-size:11px; fill:#fff;
}
.route-svg .route-name{
  font-family:var(--display);
  font-size:13px; letter-spacing:.05em;
  fill:#fff;
}
/* the carbarns at both ends of the line */
.route-svg .route-depot{
  filter:drop-shadow(0 4px 7px rgba(0,0,0,.5));
}
.route-svg .depot-roof{
  fill:var(--paper);
  stroke:#0b0c12; stroke-width:1.6;
}
.route-svg .depot-mouth{ fill:#1c2733; }
.route-svg .depot-ridge{
  stroke:#0b0c12; stroke-width:1; opacity:.22; fill:none;
}
.route-svg .depot-track{
  stroke:#5a5a66; stroke-width:2; fill:none;
}
.route-svg .depot-label{
  font-family:var(--display);
  font-size:7.5px; letter-spacing:.08em;
  fill:#0b0c12; opacity:.75;
}

/* tram 34: stylized top view riding the line, Mini-Metro style */
.route-svg .route-tram{
  filter:drop-shadow(0 5px 8px rgba(0,0,0,.55));
}
/* Flexity livery: white roof, petrol-blue sides, dark ends,
   yellow line-number box with black digits */
.route-svg .tram-body{
  fill:var(--paper);
  stroke:#1e73a2; stroke-width:3.2;
}
.route-svg .tram-joint{ fill:#0b0c12; opacity:.75; }
.route-svg .tram-glass{ fill:#10151c; }
.route-svg .tram-panto{
  fill:none; stroke:#0b0c12; stroke-width:1.3; stroke-linecap:round;
}
.route-svg .tram-plate{
  fill:#ffd60a;
  stroke:#0b0c12; stroke-width:.9;
}
.route-svg .tram-number{
  font-family:var(--display);
  font-size:8.5px; letter-spacing:.02em;
  fill:#0b0c12;
}

/* ── Swoop: one big topic cutout per region, scroll-scrubbed ─
   The transform is written by JS every frame from the scroll
   position — no transition here, the scrollbar IS the timeline. */
.swoop{
  position:absolute; z-index:1;
  left:var(--x,50%); top:var(--y,25%);
  font-size:var(--size,10rem);
  opacity:0;
  will-change:transform;
  pointer-events:none;
}
@media (max-width: 700px){
  /* Smaller and higher, so the landed cutout sits above the copy */
  .swoop{ font-size:calc(var(--size,10rem) * .6); top:calc(var(--y,25%) * .45); }
}

/* ── Cutout layer: stickers that tilt in from the sides ── */
.cutout-layer{
  position:absolute; inset:0; z-index:1; pointer-events:none;
  will-change:transform; /* parallax moves it every frame */
}
.cutout{
  position:absolute;
  left:var(--x,50%); top:var(--y,50%);
  font-size:var(--size,5rem);
  opacity:0;
  transition:transform 1s var(--pop), opacity .5s var(--ease);
}
.cutout[data-delay="1"]{ transition-delay:120ms }
.cutout[data-delay="2"]{ transition-delay:240ms }
.cutout[data-delay="3"]{ transition-delay:360ms }
.cutout[data-delay="4"]{ transition-delay:480ms }

.cutout--left { transform:translate(-50%,-50%) translateX(-55vw) rotate(calc(var(--tilt,0deg) - 30deg)); }
.cutout--right{ transform:translate(-50%,-50%) translateX(55vw)  rotate(calc(var(--tilt,0deg) + 30deg)); }
.cutout.is-in{
  opacity:1;
  transform:translate(-50%,-50%) rotate(var(--tilt,0deg));
}

/* Label stickers keep their own type size even as cutouts
   (.cutout is later in the sheet and would otherwise win) */
.cutout.sticker--label{ font-size:clamp(.85rem,2.4vw,1.15rem); }

/* Keep flying cutouts out of the copy's way on small screens.
   Labels are wide — center them so they can't clip off an edge. */
@media (max-width: 700px){
  .cutout{ font-size:calc(var(--size,5rem) * .6); }
  .cutout.sticker--label{ font-size:.8rem; left:50%; }
}

/* ── Reveal-on-scroll (copy blocks) ────────────────────── */
.reveal{
  opacity:0; transform:translateY(1.75rem);
  transition:opacity .9s var(--ease), transform .9s var(--ease);
}
.reveal[data-delay="1"]{ transition-delay:110ms }
.reveal[data-delay="2"]{ transition-delay:220ms }
.reveal[data-delay="3"]{ transition-delay:330ms }
.reveal[data-delay="4"]{ transition-delay:440ms }
.reveal.is-in{ opacity:1; transform:none; }

@media (prefers-reduced-motion: reduce){
  .reveal, .cutout{ opacity:1; transform:translate(0,0); transition:none; }
  .cutout{ transform:translate(-50%,-50%) rotate(var(--tilt,0deg)); }
  .scroll-cue i{ animation:none; }
  .backdrop{ transition:none; }
  .gate__sticker{ animation:none; }
}

/* ── Info section (the stable end) ─────────────────────── */
.info{
  position:relative; z-index:2;
  padding:clamp(4rem,10vh,7rem) clamp(1.25rem,6vw,3rem) 5rem;
  display:grid; place-items:center;
  background:linear-gradient(180deg, transparent, rgba(8,8,12,.85) 8rem);
}
.info__inner{ width:min(52rem,100%); text-align:center; }

.event-grid{
  display:grid; gap:1.25rem; margin:2.5rem 0 4rem;
  grid-template-columns:repeat(auto-fit,minmax(16rem,1fr));
  text-align:left;
}
.event-card{
  padding:1.5rem;
  border:2px solid rgba(255,255,255,.14);
  border-radius:1.1rem;
  background:rgba(255,255,255,.045);
  display:flex; flex-direction:column; gap:.4rem;
}
.event-card--tram { --ra:var(--cyan); border-color:rgba(62,233,255,.35); }
.event-card--venue{ --ra:var(--magenta); border-color:rgba(255,62,165,.35); }
.event-card__tag{
  margin:0; font-family:var(--display);
  font-size:.7rem; letter-spacing:.2em; color:var(--ra, var(--acid)); text-transform:uppercase;
}
.event-card h3{
  margin:0; font-family:var(--display);
  font-size:1.5rem; text-transform:uppercase; letter-spacing:-.01em;
}
.event-card__meta{ margin:0 0 .35rem; color:var(--ink-faint); font-size:.9rem; }
.event-card__deadline{
  margin:0 0 1rem;
  font-family:var(--display); font-size:.72rem;
  letter-spacing:.12em; text-transform:uppercase;
  color:var(--acid);
}

/* The day timeline: a fact card with times as keys — narrower and
   left-aligned inside the centered info column. */
.fact-card--day{
  margin:1.5rem auto 3.5rem;
  max-width:36rem;
  text-align:left;
}

.event-embed{
  width:100%; aspect-ratio:4/5; border:0; border-radius:.6rem;
  background:rgba(255,255,255,.04);
}

.rsvp-btn{
  margin-top:auto;
  display:inline-block; padding:.85rem 1.25rem; text-align:center;
  font-family:var(--display); font-size:.9rem; letter-spacing:.06em;
  color:#0b0c12; background:var(--paper);
  border-radius:.7rem; text-decoration:none;
  transition:transform .15s var(--pop), background .2s;
}
.rsvp-btn:hover{ background:var(--acid); transform:rotate(-1deg) scale(1.02); }

.faq-title{ margin-top:1rem; }
.faq{ text-align:left; margin-top:1.5rem; }
.faq details{
  border-bottom:1px solid rgba(255,255,255,.12);
}
.faq summary{
  cursor:pointer; list-style:none;
  padding:1.1rem .25rem;
  font-weight:600; font-size:1.05rem; color:var(--ink);
  display:flex; justify-content:space-between; align-items:center; gap:1rem;
}
.faq summary::-webkit-details-marker{ display:none; }
.faq summary::after{
  content:"+"; font-family:var(--display); color:var(--acid);
  transition:transform .25s var(--ease);
}
.faq details[open] summary::after{ transform:rotate(45deg); }
.faq details p{ padding:0 .25rem 1.25rem; margin:0; color:var(--ink-soft); }

.signoff{ margin-top:3.5rem; font-size:.95rem; color:var(--ink-faint); }

/* ── Confetti canvas — a plane INSIDE the reel section ───
   z 2 with the cards, rendered after them (so above), and below
   any beat raised with z:3 (the host) — bursts pop out from
   behind that cutout. */
.reel__confetti{
  position:absolute; inset:0; z-index:2;
  pointer-events:none;
  width:100%; height:100%;
}
