/* ============================================================
   Ashish & Priyanshi — 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. */
.intro {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #d2e6ed;
  overflow: hidden;
}

.intro__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: #d2e6ed;
}

.intro__open {
  position: absolute;
  left: 50%;
  bottom: clamp(26px, 8vh, 76px);
  transform: translateX(-50%) translateZ(0);
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  margin: 0;
  padding: 0.72em 1.5em;
  border: 0;
  border-radius: 999px;
  background: rgba(20, 52, 62, 0.55);
  box-shadow: 0 8px 26px rgba(0, 0, 0, 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: lift + dissolve; JS removes the element on transitionend. */
.intro.is-leaving {
  opacity: 0;
  transform: scale(1.06);
  pointer-events: none;
  transition:
    opacity 720ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 720ms cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform, opacity;
}

/* 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 300ms ease;
  }
}

/* ============================================================
   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;
}
.sng-cta {
  position: absolute;
  left: 50%;
  bottom: 5.5%;
  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: translateX(-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: translateX(-50%) translateY(0); }
  50%      { opacity: 1; transform: translateX(-50%) translateY(-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 #PRIttyISHq 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");
}

/* 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;
}

@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 #PRIttyISHq — 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: "AP Playfair", Georgia, "Times New Roman", serif;
  font-weight: 500;
  letter-spacing: 0.02em;
  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-vellum: 380ms;    --apd-vellum: 1500ms;
  --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;
}

/* ---- Vellum sheet: the card opens as the paper cover dissolves ---- */
.ap-vellum {
  position: absolute;
  inset: 0;
  z-index: 40;
  pointer-events: none;
  opacity: 1;
  transform: translateZ(0);
  background:
    repeating-linear-gradient(
      118deg,
      rgba(123, 108, 84, 0.02) 0 1.5px,
      rgba(255, 255, 255, 0) 1.5px 6px
    ),
    repeating-linear-gradient(
      26deg,
      rgba(123, 108, 84, 0.012) 0 1px,
      rgba(255, 255, 255, 0) 1px 7px
    ),
    linear-gradient(180deg, rgba(255, 253, 248, 0.94), rgba(250, 243, 234, 0.92));
  box-shadow: inset 0 0 90px rgba(190, 170, 140, 0.2);
  will-change: transform, opacity, filter;
}

.stage--cover.ap-cover-playing .ap-vellum {
  animation: ap-vellum-clear var(--apd-vellum) cubic-bezier(0.4, 0, 0.2, 1)
    var(--apt-vellum) both;
}

@keyframes ap-vellum-clear {
  0% {
    opacity: 1;
    transform: translate3d(0, 0, 0) scale(1);
    filter: blur(0);
  }
  100% {
    opacity: 0;
    transform: translate3d(0, -2.4%, 0) scale(1.02);
    filter: blur(3px);
  }
}

/* ---- 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: "AP Playfair", Georgia, "Times New Roman", serif;
  font-weight: 500;
  line-height: 1;
  color: #c0436a;
}

.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-vellum,
  .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 — "Slide to open the curtains".
   Two ivory drapes cover the scene, parted by a thin center gap
   so the Bhaat illustration peeks through. A guest drags them
   apart (setupBhaatCurtains in animations.js tracks the finger
   and writes inline transforms while .is-dragging kills the
   transition); release past the midpoint adds .bhaat-opened and
   the panels glide off-screen, 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 drag. touch-action
   keeps vertical scrolling free while we own the horizontal gesture. */
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 0.82s cubic-bezier(0.22, 1, 0.36, 1),
    opacity 0.5s ease 0.32s;
}
.bhaat-curtain--l {
  left: 0;
}
.bhaat-curtain--r {
  right: 0;
}
html.anim-ready .stage--bhaat.bhaat-armed .bhaat-curtain {
  will-change: transform;
}

/* 1:1 finger tracking — JS writes the inline transform each frame. */
.bhaat-curtains.is-dragging .bhaat-curtain {
  transition: none;
}

/* 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. Gated :not(.is-dragging) so
   grabbing instantly cancels the keyframe and the inline drag transform
   takes over with no fight. */
html.anim-ready .stage--bhaat.bhaat-armed.bhaat-peek:not(.bhaat-opened)
  .bhaat-curtains:not(.is-dragging) .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:not(.is-dragging) .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.06s, 0.32s;
}
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: "Slide 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;
  /* A slide that starts on this button now parts the drapes (the drag is
     bound to the slide); pan-y keeps vertical scrolling free, matching the
     curtains, while the horizontal part stays ours. */
  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__arrow {
  font-size: 1.1em;
  line-height: 1;
  color: #ffd79a;
}
/* The arrows drift outward — a small live cue for the "part" direction. */
.bhaat-open.is-visible .bhaat-open__arrow--l {
  animation: bhaat-arrow-l 1.4s ease-in-out infinite;
}
.bhaat-open.is-visible .bhaat-open__arrow--r {
  animation: bhaat-arrow-r 1.4s 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); }
}
@keyframes bhaat-arrow-l {
  0%, 100% { transform: translateX(0); opacity: 0.55; }
  50%      { transform: translateX(-4px); opacity: 1; }
}
@keyframes bhaat-arrow-r {
  0%, 100% { transform: translateX(0); opacity: 0.55; }
  50%      { transform: translateX(4px); opacity: 1; }
}

/* 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;
  }
}
