/* ============================================================
   Vaibhav & Unnati — Motion layer (additive, JS-guarded)
   Loaded after styles.css. animations.js adds `.anim-ready` to
   <html> and a `data-role` to each layer; only then do the
   hidden/initial states below apply. If the script never runs
   (or motion is reduced), the invitation renders exactly as
   it did before — no blank screen.

   Property budget: transform / opacity / filter only.
   Reveal owns opacity (+transform briefly); the looped
   ambient animation is delayed past the reveal so the two
   never fight for `transform`. Lights/names use `filter`,
   which composes freely with everything else.
   ============================================================ */

:root {
  --ease-rev: cubic-bezier(0.16, 1, 0.3, 1);
  --rev-dur: 0.9s;
  --rev-dur-hero: 1.2s;
  --bob-dur: 5.6s;
  --sway-dur: 6.4s;
  --flicker-dur: 3.4s;
  --glow-dur: 4.6s;
}

/* Hidden until observed (only once JS has tagged the layers). */
html.anim-ready .invite .ly[data-role] {
  opacity: 0;
}

/* Background / texture — fade only, so `transform` stays at the
   PSD default and registered layer seams never shift. */
html.anim-ready .ly[data-role="bg"].is-in {
  animation: ap-fade var(--rev-dur) ease var(--d, 0ms) both;
}

/* Solid objects (pots, vases, mandap, arch, drum…) — quiet settle. */
html.anim-ready .ly[data-role="object"].is-in {
  animation: ap-settle var(--rev-dur) var(--ease-rev) var(--d, 0ms) both;
}

/* Readable content (titles, dates, venue, blessings, RSVP…) — fade-rise. */
html.anim-ready .ly[data-role="content"].is-in {
  animation: ap-rise var(--rev-dur) var(--ease-rev) var(--d, 0ms) both;
}

/* Hero names — a taller, earned rise, then a slow warm glow. */
html.anim-ready .ly[data-role="name"].is-in {
  animation:
    ap-rise-hero var(--rev-dur-hero) var(--ease-rev) var(--d, 0ms) both,
    ap-name-glow var(--glow-dur) ease-in-out
      calc(var(--d, 0ms) + var(--rev-dur-hero)) infinite;
}

/* Figures (couple, illustrations) — settle, then a gentle vertical bob. */
html.anim-ready .ly[data-role="figure"].is-in {
  animation:
    ap-settle var(--rev-dur) var(--ease-rev) var(--d, 0ms) both,
    ap-bob var(--bob-dur) ease-in-out
      calc(var(--d, 0ms) + var(--rev-dur)) infinite;
}

/* Decor (florals, garlands, curtains, leaves) — settle, then sway. */
html.anim-ready .ly[data-role="decor"].is-in {
  transform-origin: 50% 92%;
  animation:
    ap-settle var(--rev-dur) var(--ease-rev) var(--d, 0ms) both,
    ap-sway var(--sway-dur) ease-in-out
      calc(var(--d, 0ms) + var(--rev-dur)) infinite;
}

/* Lights (candles, sparkles) — settle, then a warm flicker (filter only). */
html.anim-ready .ly[data-role="light"].is-in {
  animation:
    ap-settle var(--rev-dur) var(--ease-rev) var(--d, 0ms) both,
    ap-flicker var(--flicker-dur) ease-in-out
      calc(var(--d, 0ms) + var(--rev-dur)) infinite;
}

@keyframes ap-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes ap-settle {
  from { opacity: 0; transform: translate3d(0, 10px, 0) scale(0.985); }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}

@keyframes ap-rise {
  from { opacity: 0; transform: translate3d(0, 28px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@keyframes ap-rise-hero {
  from { opacity: 0; transform: translate3d(0, 44px, 0) scale(0.93); }
  60%  { opacity: 1; }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}

@keyframes ap-bob {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50%      { transform: translate3d(0, -6px, 0); }
}

@keyframes ap-sway {
  0%, 100% { transform: rotate(-1.3deg); }
  50%      { transform: rotate(1.3deg); }
}

@keyframes ap-flicker {
  0%, 100% { filter: brightness(1) drop-shadow(0 0 0 rgba(255, 190, 120, 0)); }
  45%      { filter: brightness(1.22) drop-shadow(0 0 7px rgba(255, 188, 116, 0.85)); }
  70%      { filter: brightness(1.05) drop-shadow(0 0 4px rgba(255, 188, 116, 0.5)); }
}

@keyframes ap-name-glow {
  0%, 100% { filter: drop-shadow(0 0 0 rgba(201, 120, 92, 0)); }
  50%      { filter: drop-shadow(0 1px 10px rgba(201, 120, 92, 0.55)); }
}

@keyframes ap-pulse {
  0%, 100% { transform: translateZ(0) scale(1); }
  50%      { transform: translateZ(0) scale(1.045); }
}

/* ---- One-time bloom of light behind the couple's names ---- */
.ap-bloom {
  position: absolute;
  z-index: 0;
  pointer-events: none;
  border-radius: 50%;
  background: radial-gradient(
    circle,
    rgba(255, 232, 200, 0.85),
    rgba(255, 208, 170, 0.35) 45%,
    transparent 70%
  );
  mix-blend-mode: screen;
  opacity: 0;
  transform: scale(0.4);
  animation: ap-bloom 1.9s ease-out forwards;
}

@keyframes ap-bloom {
  0%   { opacity: 0; transform: scale(0.4); }
  35%  { opacity: 0.9; }
  100% { opacity: 0; transform: scale(1.55); }
}

/* ---- Drifting rose petals (canvas overlay) ---- */
.ap-petals {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 60;
  pointer-events: none;
  opacity: 0.55;
}

/* ---- Wedding ceremony fireworks (stage-local canvas) ---- */
.stage--wedding .ap-wedding-fireworks {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 1;
  mix-blend-mode: screen;
  filter: saturate(1.42) brightness(1.16);
}

/* ---- Scroll-progress thread ---- */
.ap-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  z-index: 80;
  pointer-events: none;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: linear-gradient(90deg, #c79a4e, #e8c887 45%, #c1446c);
  box-shadow: 0 0 8px rgba(199, 154, 78, 0.55);
}

/* ---- Opening envelope video (full-screen intro) ----
   Covers the screen by default so there is no flash before
   animations.js wires it up; <noscript> hides it when JS is off.
   The exit animates transform + opacity only, then JS removes the
   node and frees the video so it never returns. */
/* The envelope clip is pukhraj-poorva's (no names, seal baked in). Its
   final-frame edge colour is the overlay background, so an object-fit crop
   or the dissolve never shows a mismatched band. */
.intro {
  --intro-bg: #a4958c;
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--intro-bg);
  overflow: hidden;
}

.intro__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--intro-bg);
}

.intro__open {
  position: absolute;
  /* Centred with inset + auto margins, not translateX: the ap-pulse
     keyframes own `transform` and would drop a centring translate. */
  left: 0;
  right: 0;
  width: max-content;
  bottom: clamp(26px, 8vh, 76px);
  transform: translateZ(0);
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  margin: 0 auto;
  padding: 0.72em 1.5em;
  border: 0;
  border-radius: 999px;
  background: rgba(74, 52, 36, 0.55);
  box-shadow: 0 8px 26px rgba(40, 24, 12, 0.28);
  color: #fff8f0;
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(0.95rem, 3.6vw, 1.15rem);
  letter-spacing: 0.04em;
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  will-change: transform;
  animation: ap-pulse 2.6s ease-in-out infinite;
}

.intro__open-dot {
  width: 0.6em;
  height: 0.6em;
  border-radius: 50%;
  background: #ffd9a8;
  box-shadow: 0 0 10px rgba(255, 201, 132, 0.9);
}

.intro__open:focus-visible {
  outline: 3px solid #fff8f0;
  outline-offset: 3px;
}

/* Once playback starts, retire the affordance (opacity only). */
.intro.is-playing .intro__open {
  opacity: 0;
  pointer-events: none;
  animation: none;
  transition: opacity 240ms ease;
}

/* Exit — ported 1:1 from pukhraj-poorva (css/intro.css): a slow 2000ms
   dissolve on a curve that keeps light in the frame for the whole fade (not
   front-loaded), a gentle scale push-through toward the guest, and a warm
   ivory bloom that swells once so the envelope opens INTO light. The cover
   fades up and settles underneath at the same moment (.hn-enter below).
   JS removes the node on transitionend (opacity) with a 2600ms fallback that
   MUST outlast --intro-fade — change one, change both. */
.intro {
  --intro-fade: 2000ms;
  transform: scale(1);
  isolation: isolate;
}

.intro::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(
    ellipse at 50% 42%,
    rgba(255, 247, 232, 0.95),
    rgba(255, 247, 232, 0) 70%
  );
}

.intro.is-leaving::before {
  animation: hn-intro-bloom var(--intro-fade) ease-in-out both;
}

@keyframes hn-intro-bloom {
  0%   { opacity: 0; }
  30%  { opacity: 0.85; }
  100% { opacity: 0; }
}

.intro.is-leaving {
  opacity: 0;
  transform: scale(1.07);
  pointer-events: none;
  transition:
    opacity var(--intro-fade) cubic-bezier(0.45, 0.05, 0.25, 1),
    transform var(--intro-fade) cubic-bezier(0.22, 1, 0.36, 1);
}

/* The cover waits unseen under the envelope, then fades up out of a slight
   overscale as the envelope melts (pukhraj-poorva's pp-hero-enter). Opacity is
   full by 45%, so the long tail is a pure scale settle. The origin is the
   first screen's centre, not the tall stage's, so the settle happens where
   the guest is looking. */
html.intro-lock .stage--cover {
  opacity: 0;
}

.stage--cover.hn-enter {
  transform-origin: 50% calc(var(--viewport-height, 100vh) / 2);
  animation: hn-cover-enter 2.8s cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes hn-cover-enter {
  from { opacity: 0; transform: scale(1.05); }
  45%  { opacity: 1; }
  to   { opacity: 1; transform: scale(1); }
}

/* Logo page — once the envelope has opened, the couple's logo fades and
   settles in on the clip's own final frame, holds, then the whole overlay
   takes the 2000ms dissolve above.
   Sits over the video/poster, under the ::before bloom (z 3). */
.intro__logo-screen {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Transparent: the logo sits on the video's own held final frame, so
     there is no seam to match (a still never matches — Chrome draws video
     frames ~12 levels brighter than ffmpeg/canvas exports of the same frame). */
  background: transparent;
  opacity: 0;
  pointer-events: none;
  transition: opacity 700ms ease;
}

.intro__logo {
  display: block;
  width: min(74vw, 44vh, 460px);
  height: auto;
  opacity: 0;
  transform: scale(0.94);
  transition:
    opacity 1100ms cubic-bezier(0.22, 1, 0.36, 1) 200ms,
    transform 1600ms cubic-bezier(0.22, 1, 0.36, 1) 200ms;
}

/* No played video behind it (reduced motion, or the clip failed): use a
   still of the final frame instead, so the sealed poster never shows. */
.intro__logo-screen.is-standalone {
  background: var(--intro-bg) url("assets/envelope-end-pp.jpg") center / cover no-repeat;
}

.intro.is-logo .intro__logo-screen {
  opacity: 1;
  pointer-events: auto;
}

.intro.is-logo .intro__logo {
  opacity: 1;
  transform: scale(1);
}

/* Sealed-envelope <img> stand-in for the video's poster: Chrome drops the
   poster at play() ~430ms before the first frame presents, which flashes the
   background. JS hides this the frame the video paints (no transition). */
.intro__poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

.intro__poster.is-hidden {
  display: none;
}

/* Disable page scroll while the intro owns the screen. */
html.intro-lock,
html.intro-lock body {
  overflow: hidden;
  height: 100%;
  overscroll-behavior: none;
  touch-action: none;
}

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

  .intro.is-leaving {
    transform: none;
    transition: opacity 250ms ease;
  }

  .intro__logo {
    transform: none;
  }

  .intro::before,
  .intro.is-leaving::before {
    animation: none;
    opacity: 0;
  }

  .stage--cover.hn-enter {
    animation: none;
  }
}

/* ============================================================
   Sangeet — "Tap to illuminate the evening".
   The scene loads dim & intimate, then a tap runs a staggered
   lights-up: spark (fairy strings) → chandelier bloom → ambient
   warmth (venue + lamp glow) → grand reveal (couple + text).
   setupSangeet() (animations.js) strips these layers' data-role
   so the scroll cascade ignores them, adds .sng-armed, then
   .sng-lit on tap. If JS never runs / motion is reduced, none
   of the classes below are applied, so the slide stays fully
   lit. Budget: opacity / transform / filter only — every beat
   is compositor-friendly for 60fps on mobile.
   ============================================================ */
html.anim-ready {
  --sng-ease: cubic-bezier(0.25, 1, 0.5, 1);
}

/* Per-group transitions carry the timeline (duration + delay). */
html.anim-ready .stage--sangeet .ly.sng-venue {
  transition: filter 0.5s var(--sng-ease) 0.4s; /* T+0.4 warmth */
}
html.anim-ready .stage--sangeet .ly.sng-string {
  transition: opacity 0.12s linear 0s; /* T+0.0 spark */
}
html.anim-ready .stage--sangeet .ly.sng-chandelier {
  transform-origin: 50% 0;
  transition:
    opacity 0.6s var(--sng-ease) 0.2s, /* T+0.2 bloom */
    transform 0.6s var(--sng-ease) 0.2s;
}
html.anim-ready .stage--sangeet .sng-lamp-glow {
  transition:
    opacity 0.5s var(--sng-ease) 0.4s, /* T+0.4 warmth */
    transform 0.5s var(--sng-ease) 0.4s;
}
html.anim-ready .stage--sangeet .ly.sng-couple,
html.anim-ready .stage--sangeet .ly.sng-text {
  transition:
    opacity 0.7s var(--sng-ease) 0.6s, /* T+0.6 grand reveal */
    transform 0.7s var(--sng-ease) 0.6s;
}

/* ---- Armed: the dim, intimate "before" state ---- */
html.anim-ready .stage--sangeet.sng-armed .ly.sng-venue {
  filter: brightness(0.34) saturate(0.72);
}
/* Pre-promote the venue's filter layers while the armed slide is on
   screen (and through the lit warmth beat), so the tap itself never
   pays the one-off compositing cost. Released by .sng-done below. */
html.anim-ready .stage--sangeet.sng-armed.sng-inview .ly.sng-venue,
html.anim-ready .stage--sangeet.sng-lit .ly.sng-venue {
  will-change: filter;
}
html.anim-ready .stage--sangeet.sng-armed .ly.sng-chandelier {
  opacity: 0;
  transform: scale(0.9);
  will-change: opacity, transform;
}
html.anim-ready .stage--sangeet.sng-armed .ly.sng-couple,
html.anim-ready .stage--sangeet.sng-armed .ly.sng-text {
  opacity: 0;
  transform: translate3d(0, 20px, 0);
  will-change: opacity, transform;
}
html.anim-ready .stage--sangeet.sng-armed .sng-lamp-glow {
  opacity: 0;
  transform: scale(0.9);
  will-change: opacity, transform;
}
html.anim-ready .stage--sangeet.sng-armed .ly.sng-string {
  opacity: 0.4;
}
/* Fairy strings breathe only while the slide is on screen & waiting. */
html.anim-ready .stage--sangeet.sng-armed.sng-inview .ly.sng-string {
  animation: sng-breathe 2s ease-in-out infinite;
}
@keyframes sng-breathe {
  0%, 100% { opacity: 0.4; }
  50%      { opacity: 0.7; }
}

/* ---- Lit: the illuminated "after" state ---- */
html.anim-ready .stage--sangeet.sng-lit .ly.sng-venue {
  filter: brightness(1) saturate(1);
}
html.anim-ready .stage--sangeet.sng-lit .ly.sng-string {
  opacity: 1;
}
html.anim-ready .stage--sangeet.sng-lit .ly.sng-chandelier {
  opacity: 1;
  transform: scale(1);
}
html.anim-ready .stage--sangeet.sng-lit .sng-lamp-glow {
  opacity: 0.7;
  transform: scale(1);
}
html.anim-ready .stage--sangeet.sng-lit .ly.sng-couple,
html.anim-ready .stage--sangeet.sng-lit .ly.sng-text {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* ---- Settled ambient life, once the reveal has finished ---- */
html.anim-ready .stage--sangeet.sng-lit .ly.sng-chandelier {
  animation: ap-chandelier-glow 5.5s ease-in-out 1.1s infinite;
}
html.anim-ready .stage--sangeet.sng-lit .ly.sng-string {
  animation: ap-twinkle 4.2s ease-in-out 0.7s infinite;
}
/* Drop the compositor hints once everything has settled. */
html.anim-ready .stage--sangeet.sng-done .ly.sng-venue,
html.anim-ready .stage--sangeet.sng-done .ly.sng-chandelier,
html.anim-ready .stage--sangeet.sng-done .ly.sng-couple,
html.anim-ready .stage--sangeet.sng-done .ly.sng-text,
html.anim-ready .stage--sangeet.sng-done .sng-lamp-glow {
  will-change: auto;
}

@keyframes ap-chandelier-glow {
  0%, 100% { filter: brightness(1) drop-shadow(0 0 2px rgba(255, 208, 142, 0)); }
  50%      { filter: brightness(1.13) drop-shadow(0 0 11px rgba(255, 204, 138, 0.55)); }
}

@keyframes ap-twinkle {
  0%, 100% { opacity: 0.92; filter: brightness(1); }
  35%      { opacity: 1; filter: brightness(1.22); }
  70%      { opacity: 0.85; filter: brightness(0.97); }
}

/* ---- Warm lamp-glow accents (no baked glow layer exists, so the
   lamps/candelabras get a soft pool that blooms on with the warmth) ---- */
.stage--sangeet .sng-glows {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}
.stage--sangeet .sng-lamp-glow {
  position: absolute;
  top: 50%;
  width: 36%;
  height: 30%;
  border-radius: 50%;
  background: radial-gradient(
    closest-side,
    rgba(255, 214, 156, 0.85),
    rgba(255, 183, 120, 0.32) 48%,
    transparent 72%
  );
  mix-blend-mode: screen;
}
.stage--sangeet .sng-lamp-glow--l { left: -7%; }
.stage--sangeet .sng-lamp-glow--r { right: -7%; }

/* ---- Call-to-action: "Tap to illuminate the evening" ----
   A real button kept outside the role="img" stage, so it stays
   in the a11y tree and tab order; visibility (not just opacity)
   is toggled so it leaves the tab order when hidden. */
.sng-slide {
  position: relative;
}
/* Centred on the slide, in the dark archway, where a guest looks first. */
.sng-cta {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 5;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4em;
  margin: 0;
  padding: 0.5em 0.9em;
  border: 0;
  background: transparent;
  color: #f4e8d0;
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(0.82rem, calc(var(--stage-width, 360px) * 0.036), 1.05rem);
  font-style: italic;
  letter-spacing: 0.05em;
  line-height: 1.3;
  text-align: center;
  white-space: nowrap;
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translate(-50%, -50%);
  text-shadow:
    0 1px 10px rgba(0, 0, 0, 0.6),
    0 0 22px rgba(255, 206, 150, 0.3);
  transition:
    opacity 0.6s ease,
    visibility 0s linear 0.6s;
}
.sng-cta.is-visible {
  opacity: 0.92;
  visibility: visible;
  pointer-events: auto;
  transition:
    opacity 0.6s ease,
    visibility 0s linear 0s;
  animation: sng-cta-pulse 2.8s ease-in-out infinite;
}
.sng-cta__arrow {
  font-size: 1.15em;
  line-height: 1;
  color: #ffd79a;
}
.sng-cta:focus-visible {
  outline: 2px solid rgba(244, 232, 208, 0.9);
  outline-offset: 6px;
  border-radius: 8px;
}
@keyframes sng-cta-pulse {
  0%, 100% { opacity: 0.72; transform: translate(-50%, -50%); }
  50%      { opacity: 1; transform: translate(-50%, calc(-50% - 4px)); }
}

/* Safety: should these classes ever be present under reduced
   motion, show the scene fully and drop the hint. */
@media (prefers-reduced-motion: reduce) {
  html.anim-ready .stage--sangeet .ly.sng-venue {
    filter: none;
  }
  html.anim-ready .stage--sangeet .ly.sng-chandelier,
  html.anim-ready .stage--sangeet .ly.sng-string,
  html.anim-ready .stage--sangeet .ly.sng-couple,
  html.anim-ready .stage--sangeet .ly.sng-text {
    opacity: 1;
    transform: none;
    animation: none;
  }
  .sng-cta {
    display: none;
  }
}

/* ============================================================
   Haldi — "Throw the colors": tap-to-fire color shots that rise
   BEHIND the stage. The canvas is inserted below the backdrop and
   everything after it, so it never sits over the art or text; it is
   pointer-transparent so taps reach the slide's handler. The hint is
   a real button (a11y tree + tab order), shown only while in view.
   ============================================================ */
.haldi-slide {
  position: relative;
  /* Tap-to-throw fires on every tap, so suppress the browser's double-tap
     zoom across the whole slide while keeping scroll + pinch-zoom. */
  touch-action: manipulation;
}
.haldi-colorshots {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
/* A readable frosted "chip" so the hint lifts off the busy art, with a
   tap-ripple (a contact dot + expanding rings) that reads as "tap here". */
.haldi-cta {
  position: absolute;
  left: 50%;
  bottom: 5.5%;
  z-index: 5;
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  margin: 0;
  padding: 0.5em 0.95em 0.5em 0.78em;
  border: 1px solid rgba(255, 255, 255, 0.78);
  border-radius: 999px;
  background: rgba(255, 250, 245, 0.74);
  -webkit-backdrop-filter: blur(7px) saturate(1.1);
  backdrop-filter: blur(7px) saturate(1.1);
  color: #a3275f;
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(0.84rem, calc(var(--stage-width, 360px) * 0.037), 1.06rem);
  font-style: italic;
  letter-spacing: 0.015em;
  line-height: 1.25;
  text-align: center;
  white-space: nowrap;
  cursor: pointer;
  /* Rapid taps must fire color shots, not the browser's double-tap-to-zoom
     gesture (which also swallowed every second tap on this small target). */
  touch-action: manipulation;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateX(-50%) translateY(6px);
  box-shadow:
    0 6px 18px rgba(120, 30, 70, 0.18),
    inset 0 1px 0 rgba(255, 255, 255, 0.7);
  transition:
    opacity 0.6s ease,
    transform 0.6s cubic-bezier(0.22, 1, 0.36, 1),
    visibility 0s linear 0.6s;
}
.haldi-cta.is-visible {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateX(-50%) translateY(0);
  transition:
    opacity 0.6s ease,
    transform 0.6s cubic-bezier(0.22, 1, 0.36, 1),
    visibility 0s linear 0s;
  animation: haldi-cta-pulse 2.6s ease-in-out 0.7s infinite;
}
.haldi-cta__text {
  text-shadow: 0 1px 1px rgba(255, 255, 255, 0.7);
}
/* Tap-ripple affordance */
.haldi-cta__tap {
  position: relative;
  flex: none;
  width: 1.5em;
  height: 1.5em;
}
.haldi-cta__dot {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0.5em;
  height: 0.5em;
  margin: -0.25em 0 0 -0.25em;
  border-radius: 50%;
  background: #e0407f;
  box-shadow: 0 0 6px rgba(224, 64, 127, 0.55);
}
.haldi-cta__ring {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 1.5em;
  height: 1.5em;
  margin: -0.75em 0 0 -0.75em;
  border-radius: 50%;
  border: 1.5px solid rgba(224, 64, 127, 0.85);
  transform: scale(0.34);
  opacity: 0;
}
.haldi-cta.is-visible .haldi-cta__ring {
  animation: haldi-tap-ripple 2.6s ease-out infinite;
}
.haldi-cta.is-visible .haldi-cta__ring:nth-child(2) {
  animation-delay: 1.3s;
}
.haldi-cta:focus-visible {
  outline: 2px solid rgba(224, 64, 127, 0.95);
  outline-offset: 4px;
}
@keyframes haldi-cta-pulse {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50%      { transform: translateX(-50%) translateY(-3px); }
}
@keyframes haldi-tap-ripple {
  0%   { transform: scale(0.34); opacity: 0; }
  18%  { opacity: 0.9; }
  75%  { opacity: 0; }
  100% { transform: scale(1.5); opacity: 0; }
}

/* Safety: under reduced motion the effect is never built in JS, but
   should these classes ever appear, hide the canvas + hint entirely. */
@media (prefers-reduced-motion: reduce) {
  .haldi-colorshots,
  .haldi-cta {
    display: none;
  }
}

/* ============================================================
   Cover — the opening "logo" beat: AP monogram, the couple's
   names, and the configured hashtag. A bespoke, ordered entrance
   that plays once the logo scrolls into view. animations.js retags
   these three layers data-role="logo" (which has no generic rule),
   so only the rules below apply — no double animation, and no
   perpetual loop afterward: the page stays calm and only the
   ambient petals keep moving.
   ============================================================ */

/* Playfair Display matches the baked didone artwork; the hashtag
   is re-typeset as live text so each letter can spring in. */
@font-face {
  font-family: "AP Playfair";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("assets/fonts/PlayfairDisplay-latin-500.woff2") format("woff2");
}

/* Cover mark and mantra — fade in before the monogram settles. */
html.anim-ready .ly.ap-logo--ganesha.is-in {
  animation: ap-logo-mantra-in 0.8s var(--ease-rev) 0.2s both;
}

html.anim-ready .template-text.ap-logo--mantra.is-in {
  animation: ap-logo-mantra-in 0.8s var(--ease-rev) 0.35s both;
}

/* AP monogram — fade in with a slight scale settle. */
html.anim-ready .ly.ap-logo--monogram.is-in {
  animation: ap-logo-monogram-in 1s var(--ease-rev) both;
}

/* Couple's names — fade up from below, just after the monogram. */
html.anim-ready .ly.ap-logo--names.is-in {
  animation: ap-logo-names-in 0.9s var(--ease-rev) 0.5s both;
}

html.anim-ready .template-text.ap-logo--names.is-in {
  animation: ap-logo-names-in 0.9s var(--ease-rev) 0.5s both;
}

@keyframes ap-logo-mantra-in {
  from { opacity: 0; transform: translate3d(0, -12px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@keyframes ap-logo-monogram-in {
  from { opacity: 0; transform: scale(0.9); }
  to   { opacity: 1; transform: scale(1); }
}

@keyframes ap-logo-names-in {
  from { opacity: 0; transform: translate3d(0, 26px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

/* Live hashtag — box + font-size are set inline by JS so it
   sits exactly where the baked image did and scales with the stage. */
.ap-hashtag {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--hn-serif);
  font-style: italic;
  font-weight: 400;
  letter-spacing: 0;
  line-height: 1;
  white-space: nowrap;
  color: #28441e;
  pointer-events: none;
}

.ap-hashtag .ap-hl {
  display: inline-block;
  white-space: pre;
}

/* Letters wait, hidden, until the scene is revealed. */
html.anim-ready .ap-hashtag .ap-hl {
  opacity: 0;
  will-change: transform, opacity;
}

/* Then each springs up in turn — letter by letter. */
html.anim-ready .ap-hashtag.is-in .ap-hl {
  animation: ap-hl-in 0.55s cubic-bezier(0.34, 1.45, 0.5, 1)
    calc(2.2s + var(--i, 0) * 0.06s) both;
}

@keyframes ap-hl-in {
  from { opacity: 0; transform: translate3d(0, 0.5em, 0) scale(0.72); }
  55%  { opacity: 1; }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}

/* A few petals drift across the hashtag area during the pause. */
.ap-logo-petals {
  position: absolute;
  pointer-events: none;
  z-index: 5;
}

.ap-logo-petal {
  position: absolute;
  border-radius: 100% 0 100% 0;
  opacity: 0;
  will-change: transform, opacity;
  animation: ap-logo-petal-drift 3.2s ease-in-out var(--pd, 1.5s) both;
}

@keyframes ap-logo-petal-drift {
  0%   { opacity: 0; transform: translate3d(0, 0, 0) rotate(0deg); }
  12%  { opacity: 0.95; }
  80%  { opacity: 0.75; }
  100% { opacity: 0;
         transform: translate3d(var(--px, 100px), var(--py, 0), 0)
                    rotate(var(--pr, 160deg)); }
}

/* A tiny gold -> pink sparkle blooms at the end of the hashtag. */
.ap-logo-sparkle {
  position: absolute;
  width: 0.85em;
  height: 0.85em;
  margin-left: -0.05em;
  margin-top: -0.18em;
  pointer-events: none;
  opacity: 0;
  z-index: 6;
  will-change: transform, opacity, filter;
  animation: ap-logo-sparkle-pop 1.2s ease-out var(--sd, 3.3s) both;
}

.ap-logo-sparkle svg {
  display: block;
  width: 100%;
  height: 100%;
}

@keyframes ap-logo-sparkle-pop {
  0%   { opacity: 0; transform: scale(0) rotate(-30deg);
         filter: drop-shadow(0 0 0 rgba(232, 200, 135, 0)); }
  40%  { opacity: 1; transform: scale(1.25) rotate(15deg);
         filter: drop-shadow(0 0 6px rgba(232, 200, 135, 0.9)); }
  70%  { opacity: 1; transform: scale(0.95) rotate(2deg); }
  100% { opacity: 0; transform: scale(0.7) rotate(8deg);
         filter: drop-shadow(0 0 2px rgba(217, 122, 160, 0.35)); }
}

@media (prefers-reduced-motion: reduce) {
  html.anim-ready .invite .ly[data-role] {
    opacity: 1 !important;
    animation: none !important;
  }
  .ap-petals,
  .ap-wedding-fireworks,
  .ap-bloom,
  .ap-logo-petals,
  .ap-logo-sparkle {
    display: none;
  }
  html.anim-ready .ap-hashtag .ap-hl {
    opacity: 1;
    animation: none !important;
  }
}

/* ============================================================
   Cover — "Blooming Vellum Garden"
   Cinematic intro, ambient breeze, microinteractions & a
   restrained parallax. Appended last so these rules layer over
   the baseline cover beats above. Everything is scoped to
   .stage--cover and gated by JS classes (.ap-cover-playing /
   .is-in) plus elements animations.js builds only when motion is
   allowed — so the reduced-motion & no-JS paths keep the original
   static cover untouched. Animated properties: transform /
   opacity / filter / mask only. Beat delays/durations come from
   CSS custom props set in animations.js (single source of truth:
   the COVER timing object); the fallbacks below mirror it.
   ============================================================ */

.stage--cover {
  --apt-floral: 520ms;    --apd-floral: 1100ms;
  --apt-monogram: 1180ms; --apd-monogram: 1150ms;
  --apt-foil: 2080ms;     --apd-foil: 1000ms;
  --apt-names: 1700ms;    --apd-names: 1050ms;
  --apt-hash: 2300ms;     --apd-hash: 1150ms;
}

/* ---- Hero-frame florals: staggered bloom, then a desynced breeze.
   These layers are excluded from the generic scroll cascade in
   animations.js and driven entirely from here. --d (stagger),
   --bx/--by/--br (entry offset) and --sway-dur/--sway-rot
   (per-cluster breeze) are set per layer in JS. --ap-px/--ap-py
   carry the optional pointer parallax and compose with the sway. */
html.anim-ready .stage--cover .ly.ap-cover-floral {
  transform-origin: 50% 88%;
  opacity: 0;
}

html.anim-ready .stage--cover.ap-cover-playing .ly.ap-cover-floral {
  animation:
    ap-floral-bloom var(--apd-floral) var(--ease-rev) var(--d, 0ms) both,
    ap-breeze var(--sway-dur, 12s) ease-in-out
      calc(var(--d, 0ms) + var(--apd-floral) + 220ms) infinite;
}

@keyframes ap-floral-bloom {
  0% {
    opacity: 0;
    transform: translate3d(var(--bx, 0), var(--by, 0), 0) scale(0.93)
      rotate(var(--br, 0deg));
  }
  100% {
    opacity: 1;
    transform: translate3d(0, 0, 0) scale(1) rotate(0deg);
  }
}

@keyframes ap-breeze {
  0%,
  100% {
    transform: translate3d(var(--ap-px, 0px), var(--ap-py, 0px), 0)
      rotate(calc(var(--sway-rot, 0.8deg) * -1));
  }
  50% {
    transform: translate3d(var(--ap-px, 0px), var(--ap-py, 0px), 0)
      rotate(var(--sway-rot, 0.8deg));
  }
}

/* ---- Sky: the faintest parallax drift (opacity stays with ap-fade) ---- */
html.anim-ready .stage--cover .ly.ap-cover-sky {
  transform: translate3d(var(--ap-px, 0px), var(--ap-py, 0px), 0);
}

/* ---- AP monogram: faint + blurred + scaled → sharp, like embossed foil ---- */
html.anim-ready .stage--cover .ly.ap-logo--monogram.is-in {
  animation: ap-monogram-emboss var(--apd-monogram) var(--ease-rev)
    var(--apt-monogram) both;
}

@keyframes ap-monogram-emboss {
  0% {
    opacity: 0;
    transform: scale(0.9) translateZ(0);
    filter: blur(7px);
  }
  55% {
    opacity: 1;
  }
  100% {
    opacity: 1;
    transform: scale(1) translateZ(0);
    filter: blur(0);
  }
}

/* A single diagonal light sweep, masked to the monogram's shape. The
   gradient lives on a child that translates across (compositor-only);
   the parent masks it to the AP glyphs via --ap-foil-src (set in JS). */
.ap-foil {
  position: absolute;
  z-index: 6;
  pointer-events: none;
  overflow: hidden;
  -webkit-mask-image: var(--ap-foil-src);
  mask-image: var(--ap-foil-src);
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

.ap-foil__glint {
  position: absolute;
  inset: -25% -60%;
  opacity: 0;
  background: linear-gradient(
    112deg,
    rgba(255, 247, 214, 0) 42%,
    rgba(255, 248, 222, 0.92) 50%,
    rgba(226, 192, 120, 0.6) 53%,
    rgba(255, 247, 214, 0) 60%
  );
  transform: translate3d(-70%, 0, 0);
  mix-blend-mode: screen;
  will-change: transform, opacity;
}

.ap-foil.is-sweeping .ap-foil__glint {
  animation: ap-foil-sweep var(--apd-foil) ease-in-out both;
}

@keyframes ap-foil-sweep {
  0% {
    opacity: 0;
    transform: translate3d(-70%, 0, 0);
  }
  20% {
    opacity: 1;
  }
  80% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: translate3d(70%, 0, 0);
  }
}

/* ---- Names: re-typeset as live Playfair text so the ink can "settle":
   fade up, blur → sharp, letter-spacing tightens. The word "and"
   presses in a fraction later and a touch softer. The baked image is
   hidden by JS and remains the reduced-motion / no-JS fallback. ---- */
.ap-names {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  pointer-events: none;
  font-family: var(--hn-serif);
  font-weight: 400;
  line-height: 1;
  color: #28441e;
}

html.anim-ready .stage--cover .ap-names {
  opacity: 0;
  will-change: transform, opacity;
}

html.anim-ready .stage--cover.ap-cover-playing .ap-names {
  animation: ap-name-press var(--apd-names) var(--ease-rev) var(--apt-names) both;
}

.ap-names .ap-nw {
  display: inline-block;
  letter-spacing: 0.02em;
}

.ap-names .ap-nand {
  display: inline-block;
  margin: 0 0.34em;
  font-style: italic;
  font-weight: 500;
}

html.anim-ready .ap-names .ap-nw,
html.anim-ready .ap-names .ap-nand {
  opacity: 0;
}

html.anim-ready .stage--cover.ap-cover-playing .ap-names .ap-nw {
  animation: ap-name-press var(--apd-names) var(--ease-rev) var(--apt-names) both;
}

html.anim-ready .stage--cover.ap-cover-playing .ap-names .ap-nand {
  animation: ap-name-press var(--apd-names) var(--ease-rev)
    calc(var(--apt-names) + 240ms) both;
}

@keyframes ap-name-press {
  0% {
    opacity: 0;
    transform: translate3d(0, 0.42em, 0);
    letter-spacing: 0.18em;
    filter: blur(5px);
  }
  60% {
    opacity: 1;
  }
  100% {
    opacity: 1;
    transform: translate3d(0, 0, 0);
    letter-spacing: 0.02em;
    filter: blur(0);
  }
}

/* ---- Hashtag: a soft left → right mask reveal (the hidden signature),
   replacing the per-letter spring from the baseline cover rules. ---- */
.stage--cover .ap-hashtag {
  -webkit-mask-image: linear-gradient(
    90deg,
    #000 0,
    #000 42%,
    rgba(0, 0, 0, 0) 72%
  );
  mask-image: linear-gradient(90deg, #000 0, #000 42%, rgba(0, 0, 0, 0) 72%);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 280% 100%;
  mask-size: 280% 100%;
  -webkit-mask-position: 100% 0;
  mask-position: 100% 0;
}

html.anim-ready .stage--cover .ap-hashtag .ap-hl {
  opacity: 1;
}

html.anim-ready .stage--cover .ap-hashtag.is-in .ap-hl {
  animation: none;
}

html.anim-ready .stage--cover .ap-hashtag.is-in {
  animation: ap-hash-wipe var(--apd-hash) cubic-bezier(0.22, 1, 0.36, 1)
    var(--apt-hash) both;
}

@keyframes ap-hash-wipe {
  0% {
    opacity: 0.85;
    -webkit-mask-position: 100% 0;
    mask-position: 100% 0;
  }
  12% {
    opacity: 1;
  }
  100% {
    opacity: 1;
    -webkit-mask-position: 0% 0;
    mask-position: 0% 0;
  }
}

/* ---- "Copied" confirmation toast (tap the hashtag) ---- */
.ap-copied {
  position: absolute;
  z-index: 9;
  pointer-events: none;
  padding: 0.3em 0.75em;
  border-radius: 999px;
  font-family: "AP Playfair", Georgia, serif;
  font-size: calc(var(--stage-width, 100vw) * 0.032);
  letter-spacing: 0.05em;
  color: #5c4a2a;
  background: rgba(255, 251, 243, 0.95);
  border: 1px solid rgba(200, 170, 110, 0.5);
  box-shadow: 0 4px 16px rgba(150, 120, 80, 0.18);
  white-space: nowrap;
  opacity: 0;
  transform: translate3d(-50%, 6px, 0);
  will-change: transform, opacity;
  animation: ap-copied-pop 1.9s ease both;
}

@keyframes ap-copied-pop {
  0% {
    opacity: 0;
    transform: translate3d(-50%, 6px, 0);
  }
  16% {
    opacity: 1;
    transform: translate3d(-50%, 0, 0);
  }
  78% {
    opacity: 1;
    transform: translate3d(-50%, 0, 0);
  }
  100% {
    opacity: 0;
    transform: translate3d(-50%, -5px, 0);
  }
}

/* ---- Interactive affordances on the cover ---- */
.stage--cover .ly.ap-logo--monogram,
.stage--cover .ap-hashtag,
.stage--cover .ap-tap {
  pointer-events: auto;
  cursor: pointer;
}

.stage--cover .ap-hashtag {
  cursor: copy;
}

.stage--cover .ap-tap {
  position: absolute;
  z-index: 7;
  background: transparent;
  border: 0;
}

.stage--cover .ly.ap-logo--monogram:focus-visible,
.stage--cover .ap-hashtag:focus-visible {
  outline: 3px solid rgba(193, 68, 108, 0.85);
  outline-offset: 4px;
  border-radius: 6px;
}

/* ---- Defensive reduced-motion: if any cover enhancement element
   exists, neutralize its motion and show the resolved state. ---- */
@media (prefers-reduced-motion: reduce) {
  .ap-foil,
  .ap-copied {
    display: none !important;
  }
  html.anim-ready .stage--cover .ly.ap-cover-floral,
  html.anim-ready .stage--cover .ly.ap-logo--monogram.is-in,
  html.anim-ready .stage--cover .ap-names .ap-nw,
  html.anim-ready .stage--cover .ap-names .ap-nand {
    animation: none !important;
    opacity: 1 !important;
    filter: none !important;
    transform: none !important;
  }
  html.anim-ready .stage--cover .ap-hashtag.is-in {
    animation: none !important;
    -webkit-mask-image: none !important;
    mask-image: none !important;
  }
}

/* ============================================================
   Scratch to reveal — the couple breathes (shared figure rule), the
   pink card shimmers a "scratch me" invitation, and uncovering the
   date is a small celebration with a living-clock tick on the countdown.
   Scoped to .stage--scratch, gated by .anim-ready + JS classes,
   transform / opacity / filter only. The static card, date and digits
   are untouched on the no-JS / reduced-motion paths.
   ============================================================ */

/* Idle sheen swept across the pink scratch card. Sits above the
   canvas (so it reads through) but is pointer-transparent, and fades
   away the instant the date is revealed. */
.ap-scratch-sheen {
  position: absolute;
  z-index: 3;
  pointer-events: none;
  overflow: hidden;
  border-radius: calc(var(--stage-width) * 0.026);
  opacity: 0;
  transition: opacity var(--motion-medium, 480ms) var(--ease-standard, ease);
}

.stage--scratch.ap-scratch-live .ap-scratch-sheen {
  opacity: 1;
}

.stage--scratch.is-scratch-complete .ap-scratch-sheen {
  opacity: 0;
}

.ap-scratch-sheen__glint {
  position: absolute;
  inset: 0;
  transform: translate3d(-130%, 0, 0);
  background: linear-gradient(
    105deg,
    rgba(255, 255, 255, 0) 40%,
    rgba(255, 255, 255, 0.5) 50%,
    rgba(255, 244, 249, 0) 60%
  );
  mix-blend-mode: screen;
  will-change: transform;
}

.stage--scratch.ap-scratch-live .ap-scratch-sheen__glint {
  animation: ap-scratch-sheen 4s ease-in-out 1.1s infinite;
}

/* Sweep across, then hold off-card for the rest of the cycle, so the
   shimmer is an occasional invitation rather than a constant strobe. */
@keyframes ap-scratch-sheen {
  0%   { transform: translate3d(-130%, 0, 0); }
  22%  { transform: translate3d(130%, 0, 0); }
  100% { transform: translate3d(130%, 0, 0); }
}

/* The uncovered date settles in: blur -> sharp with a soft scale, so
   the reveal feels earned rather than a hard cut. */
.scratch-date-card.ap-date-reveal {
  animation: ap-date-reveal 0.72s var(--ease-rev) both;
}

@keyframes ap-date-reveal {
  0%   { opacity: 0.25; transform: scale(0.92); filter: blur(6px); }
  55%  { opacity: 1; }
  100% { opacity: 1; transform: scale(1); filter: blur(0); }
}

/* Living-clock tick on each countdown digit as it changes. */
.countdown-value.ap-tick {
  animation: ap-tick 0.34s var(--ease-rev) both;
}

@keyframes ap-tick {
  0%   { opacity: 0.4; transform: translate3d(0, -3px, 0); }
  100% { opacity: 1; transform: translate3d(0, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .ap-scratch-sheen {
    display: none !important;
  }
  .scratch-date-card.ap-date-reveal,
  .countdown-value.ap-tick {
    animation: none !important;
  }
}

/* ============================================================
   Bhaat — "Tap to open the curtains".
   Two ivory drapes cover the scene, parted by a thin center gap
   so the Bhaat illustration peeks through. A tap anywhere on the
   slide (setupBhaatCurtains in animations.js) adds .bhaat-opened
   and the panels glide off-screen over ~2s, then .bhaat-done
   removes them. The drapes only cover once JS has armed them
   (.bhaat-armed), so with no JS / reduced motion the slide is
   simply uncovered. Budget: transform / opacity / filter only.
   ============================================================ */
.bhaat-slide {
  position: relative;
}

.bhaat-curtains {
  --bhaat-gap: 3.6%;
  position: absolute;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
}

/* Armed: drapes cover the scene and accept the tap. pan-y keeps
   vertical scrolling free while the slide is in view. */
html.anim-ready .stage--bhaat.bhaat-armed .bhaat-curtains {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  touch-action: pan-y;
}

.bhaat-curtain {
  position: absolute;
  top: 0;
  height: 100%;
  width: calc(50% - var(--bhaat-gap) / 2);
  transform: translateX(0);
  transform-origin: top center;
  -webkit-user-drag: none;
  user-select: none;
  transition:
    transform 2s cubic-bezier(0.22, 1, 0.36, 1),
    opacity 0.6s ease 1.4s;
}
.bhaat-curtain--l {
  left: 0;
}
.bhaat-curtain--r {
  right: 0;
}
html.anim-ready .stage--bhaat.bhaat-armed .bhaat-curtain {
  will-change: transform;
}

/* Peek nudge: a couple of springs that self-demonstrate the drapes
   move, so guests notice them. setupBhaatCurtains toggles .bhaat-peek
   while the slide is in view & unopened. */
html.anim-ready .stage--bhaat.bhaat-armed.bhaat-peek:not(.bhaat-opened)
  .bhaat-curtains .bhaat-curtain--l {
  animation: bhaat-peek-l 1.1s cubic-bezier(0.22, 1, 0.36, 1) 2;
}
html.anim-ready .stage--bhaat.bhaat-armed.bhaat-peek:not(.bhaat-opened)
  .bhaat-curtains .bhaat-curtain--r {
  animation: bhaat-peek-r 1.1s cubic-bezier(0.22, 1, 0.36, 1) 2;
}
@keyframes bhaat-peek-l {
  0%, 100% { transform: translateX(0); }
  40%      { transform: translateX(-10%) rotate(-1deg) scaleX(0.985); }
}
@keyframes bhaat-peek-r {
  0%, 100% { transform: translateX(0); }
  40%      { transform: translateX(10%) rotate(1deg) scaleX(0.985); }
}

/* Open: glide each panel off its own side with a soft fabric sway. */
html.anim-ready .stage--bhaat.bhaat-opened .bhaat-curtain--l {
  transform: translateX(-112%) rotate(-2.5deg) scaleX(0.9);
  opacity: 0;
}
html.anim-ready .stage--bhaat.bhaat-opened .bhaat-curtain--r {
  transform: translateX(112%) rotate(2.5deg) scaleX(0.9);
  opacity: 0;
  transition-delay: 0.15s, 1.4s;
}
html.anim-ready .stage--bhaat.bhaat-opened .bhaat-curtain-grip {
  opacity: 0;
  transition: opacity 0.3s ease;
}

/* Once the glide settles, drop the drapes (and their compositor hints). */
html.anim-ready .stage--bhaat.bhaat-done .bhaat-curtains {
  display: none;
}

/* A soft luminous grab handle floating in the center gap. */
.bhaat-curtain-grip {
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(3px, calc(var(--stage-width, 360px) * 0.011));
  height: calc(var(--stage-width, 360px) * 0.12);
  transform: translate(-50%, -50%);
  border-radius: 999px;
  background: rgba(255, 252, 246, 0.82);
  box-shadow:
    0 0 calc(var(--stage-width, 360px) * 0.03) rgba(255, 224, 168, 0.55),
    0 0 calc(var(--stage-width, 360px) * 0.012) rgba(120, 70, 30, 0.4);
  pointer-events: none;
}

/* ---- Hint: "Tap to open" — a real button kept outside the
   role="img" stage so it stays in the a11y tree and tab order.
   Activating it (click / Enter / Space) opens the curtains too. */
.bhaat-open {
  position: absolute;
  left: 50%;
  bottom: 12%;
  z-index: 7;
  display: inline-flex;
  flex-direction: row;
  align-items: center;
  gap: 0.5em;
  margin: 0;
  padding: 0.5em 1.05em;
  border: 1px solid rgba(255, 240, 215, 0.35);
  border-radius: 999px;
  background: rgba(46, 26, 12, 0.55);
  color: #fff4e0;
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(0.82rem, calc(var(--stage-width, 360px) * 0.038), 1.08rem);
  letter-spacing: 0.04em;
  line-height: 1.2;
  text-align: center;
  white-space: nowrap;
  cursor: pointer;
  /* pan-y keeps vertical scrolling free while the slide is in view; the
     tap (click) itself opens the drapes. */
  touch-action: pan-y;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateX(-50%);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
  transition:
    opacity 0.6s ease,
    visibility 0s linear 0.6s;
}
.bhaat-open.is-visible {
  opacity: 0.96;
  visibility: visible;
  pointer-events: auto;
  transition:
    opacity 0.6s ease,
    visibility 0s linear 0s;
  animation: bhaat-open-pulse 2.6s ease-in-out infinite;
}
.bhaat-open:focus-visible {
  outline: 2px solid rgba(246, 236, 216, 0.95);
  outline-offset: 4px;
}
@keyframes bhaat-open-pulse {
  0%, 100% { opacity: 0.78; transform: translateX(-50%) translateY(0); }
  50%      { opacity: 1; transform: translateX(-50%) translateY(-3px); }
}

/* Safety: under reduced motion the setup never runs — keep the
   drapes and hint out entirely so the scene is simply visible. */
@media (prefers-reduced-motion: reduce) {
  .bhaat-curtains {
    display: none;
  }
  .bhaat-open {
    display: none;
  }
}

/* ============================================================
   Send your blessings — motion layer.
   Tokens press, the thrown token arcs onto the crest and blooms.
   Static layout lives in styles.css. All motion is GPU-composited
   (transform / opacity / filter) and disabled under reduced motion.
   ============================================================ */

/* Press / hover affordance on the disc — calm, no perpetual loop. */
.bless-token__disc {
  transition:
    transform 160ms var(--ease-standard, ease),
    box-shadow 160ms ease;
}
@media (hover: hover) {
  .bless-token:hover .bless-token__disc {
    transform: translateY(-3px) scale(1.05);
    box-shadow:
      0 9px 20px rgba(150, 95, 60, 0.22),
      inset 0 1px 0 rgba(255, 255, 255, 0.85);
  }
}
.bless-token:active .bless-token__disc {
  transform: scale(0.93);
}

/* This screen rests still — the perpetual loops are dropped here. The
   tokens keep only their press / hover affordance (on the disc, below),
   and the bottom-flower garland keeps its one-time reveal but not the
   looping sway that other decor layers across the invite still use. */
html.anim-ready .ly.bless-garland[data-role="decor"].is-in {
  animation: ap-settle var(--rev-dur) var(--ease-rev) var(--d, 0ms) both;
}

/* Secondary Share button — quiet by default, a faint wash on interaction. */
.bless-share {
  transition:
    background-color 160ms ease,
    border-color 160ms ease,
    transform 120ms var(--ease-standard, ease);
}
@media (hover: hover) {
  .bless-share:hover {
    background-color: rgba(120, 90, 50, 0.07);
    border-color: rgba(150, 112, 66, 0.7);
  }
}
.bless-share:active {
  transform: scale(0.98);
}

/* The controls fade up once the slide scrolls into view — but only when
   "armed" (JS present and motion allowed). Otherwise they are simply visible. */
html.anim-ready .bless-armed .bless-panel {
  opacity: 0;
  transform: translateX(-50%) translateY(14px);
  transition:
    opacity 0.7s ease,
    transform 0.7s var(--ease-standard, ease);
}
html.anim-ready .bless-armed .bless-panel.is-in {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* A thrown token: launched at the tapped token's centre, arcs to the crest.
   --tx / --ty (px deltas to the crest) are set inline by JS. */
.bless-projectile {
  position: absolute;
  z-index: 4;
  width: clamp(26px, calc(var(--stage-width, 360px) * 0.1), 52px);
  height: clamp(26px, calc(var(--stage-width, 360px) * 0.1), 52px);
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
  animation: bless-arc 880ms cubic-bezier(0.36, 0.45, 0.45, 1) forwards;
}
.bless-projectile svg {
  display: block;
  width: 100%;
  height: 100%;
  filter: drop-shadow(0 3px 5px rgba(120, 60, 40, 0.28));
}

@keyframes bless-arc {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.6) rotate(0deg);
  }
  12% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1) rotate(18deg);
  }
  60% {
    opacity: 1;
    transform: translate(
        calc(-50% + (var(--tx, 0px) * 0.6)),
        calc(-50% + (var(--ty, 0px) * 0.6) - 34px)
      )
      scale(0.9) rotate(170deg);
  }
  100% {
    opacity: 0.85;
    transform: translate(
        calc(-50% + var(--tx, 0px)),
        calc(-50% + var(--ty, 0px))
      )
      scale(0.4) rotate(264deg);
  }
}

/* A warm glow blooms on the crest as each token lands. JS spawns one
   per impact at the crest centre and removes it on animationend. */
.bless-burstglow {
  position: absolute;
  z-index: 2;
  width: 40%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(
    circle,
    rgba(255, 232, 155, 0.85) 0%,
    rgba(255, 210, 120, 0.34) 45%,
    rgba(255, 210, 120, 0) 70%
  );
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
  animation: bless-burstglow-pop 900ms ease-out forwards;
}

@keyframes bless-burstglow-pop {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.4);
  }
  30% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(1.25);
  }
}

@media (prefers-reduced-motion: reduce) {
  .bless-projectile,
  .bless-burstglow {
    display: none;
  }
  .bless-token__disc {
    transition: none;
  }
  .bless-token {
    animation: none !important;
  }
  .bless-share {
    transition: none;
  }
  /* Panel is never "armed" under reduced motion, so it stays fully visible. */
}

/* ---- Floating music toggle (revealed once the envelope opens) ----
   Pauses / resumes the looping background instrumental. A pulse ring and a
   diagonal slash mark the paused state; ♪ notes drift up while playing. */
.ap-music-toggle {
  position: fixed;
  right: max(16px, calc(env(safe-area-inset-right) + 16px));
  bottom: max(16px, calc(env(safe-area-inset-bottom) + 16px));
  z-index: 1000;
  isolation: isolate;
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border: 1px solid rgba(156, 47, 83, 0.22);
  border-radius: 999px;
  color: #9c2f53;
  background: rgba(255, 251, 247, 0.9);
  box-shadow: 0 10px 28px rgba(58, 31, 40, 0.18);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.ap-music-toggle[hidden] {
  display: none;
}

.ap-music-toggle svg {
  position: relative;
  z-index: 2;
  width: 24px;
  height: 24px;
}

/* Soft pulse ring behind the button while music is paused. */
.ap-music-toggle::before {
  position: absolute;
  inset: -6px;
  z-index: -1;
  border: 1px solid rgba(193, 68, 108, 0.34);
  border-radius: inherit;
  opacity: 0;
  content: "";
  transform: scale(0.84);
  will-change: opacity, transform;
}

.ap-music-toggle:not(.is-playing)::before {
  animation: ap-music-pulse 2.4s ease-in-out infinite;
}

/* Diagonal slash across the note icon when paused. */
.ap-music-toggle:not(.is-playing)::after {
  position: absolute;
  z-index: 3;
  width: 28px;
  height: 2px;
  background: currentColor;
  content: "";
  transform: rotate(-42deg);
  transform-origin: center;
}

.ap-music-note {
  position: absolute;
  right: calc(50% - 4px);
  bottom: 50%;
  z-index: 1;
  color: rgba(193, 68, 108, 0.7);
  font: 700 14px/1 Georgia, "Times New Roman", serif;
  opacity: 0;
  pointer-events: none;
  transform: translate3d(0, 0, 0) scale(0.74) rotate(-8deg);
  will-change: opacity, transform;
}

.ap-music-toggle.is-playing .ap-music-note {
  animation: ap-music-note-float 2.8s ease-in-out infinite;
  animation-delay: calc(var(--note-index) * 520ms);
}

.ap-music-toggle:focus-visible {
  outline: 4px solid rgba(193, 68, 108, 0.82);
  outline-offset: 5px;
}

@keyframes ap-music-pulse {
  0% {
    opacity: 0.38;
    transform: scale(0.84);
  }
  70%,
  100% {
    opacity: 0;
    transform: scale(1.38);
  }
}

@keyframes ap-music-note-float {
  0% {
    opacity: 0;
    transform: translate3d(0, 0, 0) scale(0.74) rotate(-8deg);
  }
  18% {
    opacity: 0.72;
  }
  78% {
    opacity: 0.2;
  }
  100% {
    opacity: 0;
    transform: translate3d(calc(-10px + (var(--note-index) * 7px)), -48px, 0)
      scale(1.05) rotate(10deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .ap-music-toggle::before,
  .ap-music-note {
    animation: none;
    opacity: 0;
  }
}
