/* ==========================================================================
   Theme: swap the whole look by editing this block.
   Default: emerald & gold. ========================================================================== */
:root {
  --emerald-950: #05180f;
  --emerald-900: #0a2a1e;
  --emerald-800: #103828;
  --emerald-700: #164a35;
  --emerald-600: #1d5d44;
  --emerald-500: #297155;

  --gold-200: #f7ecc9; /* highlight, for shimmer crests only */
  --gold-300: #ecd9a3;
  --gold-400: #ddc077;
  --gold-500: #c9a24a;
  --gold-600: #ad8636;
  --gold-700: #8a6a29; /* gold that still passes as text on cream */

  --cream-50: #fbf7ee;
  --cream-100: #f4ecd9;
  --cream-200: #ecdfc0;

  /* Envelope paper (champagne gold) and wax seal (wine red). */
  --champagne-100: #fbeecb;
  --champagne-200: #f3dea3;
  --champagne-300: #e9c87c;
  --champagne-400: #d9ac55;
  --wine-300: #9c3a4c;
  --wine-500: #7a2436;
  --wine-600: #5e1a29;
  --wine-700: #45111d;

  /* A fine gold diamond lattice (jaali), tiled low-opacity over the
     envelope's gold panels for a bit of Indian-wedding-card ornament. */
  --jaali-pattern: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='13' height='13'%3E%3Cpath d='M0 6.5L6.5 0L13 6.5L6.5 13Z' fill='none' stroke='%23e9c87c' stroke-width='0.35' stroke-opacity='0.28'/%3E%3C/svg%3E");

  /* Story colour journey (motion-spec.md #3), one mood per chapter on Page 1,
     resolving into the emerald/gold vars above on Page 2 (Chapter Five). */
  --mood-cold-700: #2c4054; /* Chapter One, Dating, Marienplatz: cold blue-white */
  --mood-cold-900: #16222d;
  --mood-plum-700: #4a2a47; /* Chapter Two, The Reveal, the birthday trip: dusky plum */
  --mood-plum-900: #26142a;
  --mood-amber-700: #6b4423; /* Chapter Three, Parents Edition, Kolkata: warm amber */
  --mood-amber-900: #3a2412;
  --mood-navy-700: #141a33; /* Chapter Four, The Proposal, planetarium: starfield navy */
  --mood-navy-900: #090c1a;
  /* Chapter Four's evening sky: deep indigo overhead, easing to twilight
     blue towards the horizon. */
  --sky-evening:
    radial-gradient(120% 60% at 50% 108%, rgba(96, 124, 196, 0.45) 0%, rgba(96, 124, 196, 0) 70%),
    linear-gradient(180deg, #060b26 0%, #0c1a4a 38%, #16306e 72%, #25467f 100%);

  --ink: #1c2b23;
  --ink-soft: #3c4d43;
  --on-emerald: #f4ecd9;
  --on-emerald-soft: #cddbcd;

  --font-display: "Cormorant Garamond", Georgia, "Times New Roman", serif;
  --font-body: "Jost", "Segoe UI", system-ui, -apple-system, sans-serif;

  /* Fluid type scale. Cormorant sets lighter and smaller on the body than
     Playfair did, so display steps run a little larger to hold their weight.
     --step-0 stays a hard 1rem: body text never drops below 16px on a phone. */
  --step--2: clamp(0.68rem, 0.66rem + 0.1vw, 0.72rem);
  --step--1: clamp(0.8rem, 0.78rem + 0.12vw, 0.86rem);
  --step-0: 1rem;
  --step-1: clamp(1.08rem, 1.02rem + 0.3vw, 1.25rem);
  --step-2: clamp(1.3rem, 1.18rem + 0.6vw, 1.6rem);
  --step-3: clamp(1.6rem, 1.35rem + 1.2vw, 2.2rem);
  --step-4: clamp(2rem, 1.55rem + 2.2vw, 2.9rem);
  --step-5: clamp(2.1rem, 1.3rem + 4.1vw, 4rem);

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4rem;
  --space-9: 6rem;

  --shadow-soft: 0 20px 45px -20px rgba(5, 24, 15, 0.45);
  --shadow-card: 0 12px 30px -14px rgba(5, 24, 15, 0.35);
  --shadow-lift: 0 28px 60px -28px rgba(5, 24, 15, 0.6);

  --radius-lg: 22px;
  --radius-md: 14px;

  --section-pad-y: 5.5rem;
  --section-pad-x: 1.5rem;

  /* --ease-out is kept as the alias the existing rules already reference. */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-out-soft: cubic-bezier(0.33, 1, 0.68, 1);
  --ease-emphasis: cubic-bezier(0.2, 0.8, 0.2, 1);
  --ease-in-out-soft: cubic-bezier(0.65, 0, 0.35, 1);

  --dur-fast: 0.3s;
  --dur-base: 0.6s;
  --dur-slow: 1s;
  --dur-ambient: 8s;
}

/* ==========================================================================
   Reset & base
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--cream-50);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 300;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  -webkit-tap-highlight-color: transparent;
}
body.no-scroll {
  overflow: hidden;
  height: 100vh;
  height: 100svh;
  height: 100dvh;
}
/* Film grain over the whole page. Large flat CSS gradients are the main
   reason a site like this reads digital rather than printed; a single very
   low opacity noise tile fixes that for one element and no image asset.
   Deliberately no mix-blend-mode, which is expensive to composite on phones. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 1; /* above sections, below music toggle (60) and opening (100) */
  pointer-events: none;
  opacity: 0.042;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}
h1, h2, h3 { font-family: var(--font-display); font-weight: 600; margin: 0; }
p { margin: 0; }
img { max-width: 100%; display: block; }
button { font-family: inherit; cursor: pointer; touch-action: manipulation; }
/* Horizontal padding respects safe areas without shrinking the section's
   own full-bleed background away from the edge (padding insets content,
   not the background, which still paints edge to edge). */
.section {
  padding-top: var(--section-pad-y);
  padding-bottom: var(--section-pad-y);
  padding-left: max(var(--section-pad-x), env(safe-area-inset-left));
  padding-right: max(var(--section-pad-x), env(safe-area-inset-right));
  position: relative;
  isolation: isolate;
}

/* ==========================================================================
   Section-background component
   Reusable across every section and story card (js/main.js -> applySectionBackground).
   Always painted behind the host's own content via isolation + negative z-index.
   ========================================================================== */
.section-bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: -1;
  background-size: cover;
  background-position: center;
}
.section-bg__layer {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-repeat: no-repeat;
}
/* background-attachment: fixed is historically janky on mobile browsers
   (iOS Safari in particular), so the parallax feel only kicks in on
   larger, non-touch viewports; small screens get a static cover image. */
@media (min-width: 768px) and (pointer: fine) {
  .section-bg__layer--fixed { background-attachment: fixed; }
}
.section-bg__overlay {
  position: absolute;
  inset: 0;
  background: var(--emerald-900);
}
/* blurFill (context/photo-backgrounds.md): the whole photo sits on a soft,
   darkened copy of itself. JS sizes both from the photo's real proportions;
   until then plain contain is the same as blurFill at zoom 1. */
.section-bg__layer--contain { background-size: contain; }
.section-bg__blur {
  position: absolute;
  inset: -40px;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
/* Look layers, in the spec's order after the scrim: glow, lift, wash,
   vignette, grain. main.js only creates the ones a look actually uses. */
.section-bg__glow {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-repeat: no-repeat;
  mix-blend-mode: screen;
}
.section-bg__lift,
.section-bg__wash,
.section-bg__vignette,
.section-bg__grain {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.section-bg__lift { background: #6e6a64; mix-blend-mode: lighten; }
.section-bg__vignette {
  background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0) 45%, rgba(0, 0, 0, 0.85) 100%);
}
.section-bg__grain { background-size: 180px 180px; mix-blend-mode: overlay; }

/* Where the photo was taken (background.location), bottom right. */
.bg-place {
  position: absolute;
  right: 18px;
  bottom: calc(18px + env(safe-area-inset-bottom, 0px));
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 7px;
  max-width: 70%;
  margin: 0;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 15px;
  line-height: 1.2;
  color: var(--cream-50);
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.6);
  pointer-events: none;
}
.bg-place svg {
  flex: none;
  width: 14px;
  height: 14px;
  fill: none;
  stroke: var(--gold-400);
  stroke-width: 1.6;
}
/* The caption stays down at the section's foot, level with the bottom of
   the "Scroll" cue, never up among the content. On the hero it takes the
   bottom right corner from the gold filigree, which returns whenever there
   is no caption. */
.hero.has-place .hero__corner--br { display: none; }
/* The opening screen draws a gold frame 14px in from the edge; keep the
   caption inside it. */
.opening-screen > .bg-place {
  right: 30px;
  bottom: calc(30px + env(safe-area-inset-bottom, 0px));
}
/* Photographer or artist credit (SITE.photoCredits, by photo), bottom left. */
.bg-credit {
  position: absolute;
  left: 16px;
  bottom: calc(18px + env(safe-area-inset-bottom, 0px));
  z-index: 3;
  margin: 0;
  font-size: 11px;
  letter-spacing: 0.04em;
  color: rgba(244, 239, 228, 0.7);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.55);
}
.section-bg--placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
}
.section-bg__placeholder-label {
  position: relative;
  z-index: 1;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 0.82rem;
  letter-spacing: 0.04em;
  text-align: center;
  color: var(--on-emerald-soft);
  background: rgba(11, 42, 31, 0.4);
  padding: 0.45rem 1.1rem;
  border: 1px dashed var(--gold-400);
  border-radius: 999px;
  max-width: 80%;
}

/* ==========================================================================
   Section seams
   The page flips between cream and emerald four times. A short gradient at
   the boundary, in the colour of the section above, stops each flip reading
   as a hard printed edge.
   ========================================================================== */
.seam-top::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 140px;
  pointer-events: none;
  z-index: -1;
  background: linear-gradient(to bottom, var(--seam-from, transparent) 0%, transparent 78%);
}
.seam-bottom::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 140px;
  pointer-events: none;
  z-index: -1;
  background: linear-gradient(to top, var(--seam-to, transparent) 0%, transparent 78%);
}
/* A seam is a flat colour band, which reads as a stripe across a photo.
   Photo sections meet edge to edge instead. */
.has-photo.seam-top::before,
.has-photo.seam-bottom::after { display: none; }

/* ==========================================================================
   Reveal-on-scroll
   ========================================================================== */
.reveal {
  opacity: 0;
  transform: translateY(28px);
  /* --reveal-i is the element's position among its siblings, set by
     initRevealObserver, so a group arrives in its own order rather than in
     whatever order the observer happened to batch it. */
  transition: opacity 0.85s var(--ease-out-expo), transform 0.85s var(--ease-out-expo);
  transition-delay: calc(var(--reveal-i, 0) * 80ms);
}
.reveal.in-view { opacity: 1; transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
}

/* ==========================================================================
   Opening screen + envelope
   ========================================================================== */
.opening-screen {
  position: fixed;
  inset: 0;
  /* inset:0 on a fixed element already tracks the live visual viewport in
     modern browsers; the explicit height is a belt-and-suspenders pin so it
     holds exactly as the mobile address bar shows/hides, falling back
     gracefully on browsers without svh/dvh support. */
  height: 100vh;
  height: 100svh;
  height: 100dvh;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  touch-action: manipulation;
  background:
    radial-gradient(circle at 50% 20%, var(--emerald-600) 0%, var(--emerald-800) 55%, var(--emerald-900) 100%);
  cursor: pointer;
  transition: opacity 0.7s var(--ease-out), visibility 0.7s var(--ease-out);
}
.opening-screen::before {
  content: "";
  position: absolute;
  inset: 14px;
  border: 1px solid rgba(201, 162, 74, 0.35);
  border-radius: 18px;
  pointer-events: none;
}
.opening-screen.is-hidden {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
/* Once faded out, removed from rendering entirely (setupOpeningScreen).
   Hidden with visibility alone, its shimmer (a background-position
   animation, which cannot run on the GPU) and 30-odd star and gold
   particles kept restyling and repainting on every frame for the whole
   visit, which was most of the lag while swiping the story. */
.opening-screen.is-gone { display: none; }
/* Lettering on the opening waits for the real face rather than flashing
   Georgia first (see gateOnFonts). The timeout fallback means this can never
   stay hidden. */
.opening-screen__cta,
.envelope__card-monogram,
.envelope__seal-text {
  opacity: 0;
  transition: opacity 0.6s var(--ease-out-soft);
}
.fonts-ready .opening-screen__cta,
.fonts-ready .envelope__card-monogram,
.fonts-ready .envelope__seal-text {
  opacity: 1;
}
/* Envelope placement (SITE.opening.envelopePlacement), so the opening
   photo's own moment stays visible: "sky" lifts a smaller envelope into the
   upper sky, "water" sets it low over the sea, "center" is the original. */
.opening-screen--sky,
.opening-screen--water { --env-size: min(44vw, 170px); }
.opening-screen--sky .envelope,
.opening-screen--water .envelope { --env-w: var(--env-size); }
.opening-screen--sky .opening-screen__inner,
.opening-screen--water .opening-screen__inner { gap: 1.5rem; }
.opening-screen--sky { align-items: flex-start; }
.opening-screen--sky .opening-screen__inner { margin-top: max(9vh, 9svh, env(safe-area-inset-top)); }
.opening-screen--water { align-items: flex-end; }
.opening-screen--water .opening-screen__inner { margin-bottom: max(10vh, 10svh); }
.opening-screen__inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2.25rem;
  color: var(--on-emerald);
  text-align: center;
  padding: max(1.5rem, env(safe-area-inset-top)) max(1.5rem, env(safe-area-inset-right))
    max(1.5rem, env(safe-area-inset-bottom)) max(1.5rem, env(safe-area-inset-left));
}
/* Shared gold shimmer (motion-spec.md #1 and #7): a soft ambient sweep
   across a gold gradient so a line of text reads as alive, not static.
   Used by the opening's "Tap to open" and, to mirror it, the closing line,
   so the experience has a matching beginning and end. */
.shimmer-text {
  background: linear-gradient(
    100deg,
    var(--gold-600) 20%,
    var(--gold-300) 40%,
    #fff8e3 50%,
    var(--gold-300) 60%,
    var(--gold-600) 80%
  );
  background-size: 220% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  text-shadow: 0 0 22px rgba(201, 162, 74, 0.45);
  animation: gold-shimmer 3.4s ease-in-out infinite;
}
@keyframes gold-shimmer {
  0% { background-position: 130% 0; }
  100% { background-position: -30% 0; }
}
/* Tracked capitals are a sans job. Stacking uppercase + italic + heavy
   tracking on a high contrast display serif was fighting itself. */
.opening-screen__cta {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--step-0);
  letter-spacing: 0.34em;
  text-transform: uppercase;
  /* balances the trailing letter-space so tracked text centres optically */
  text-indent: 0.34em;
}
/* Pinned below a point in the opening photo (anchorOpeningCta): taken out
   of the envelope's column and placed from the top of the screen. */
/* Pending too: once moved out of the column it must leave the flex row at
   once. Hidden but still in flow, it sat beside the envelope and pushed it
   left until the photo loaded and the line was placed. */
.opening-screen--cta-pending .opening-screen__cta,
.opening-screen--cta-anchored .opening-screen__cta {
  position: absolute;
  left: 0;
  right: 0;
  z-index: 1;
  text-align: center;
  pointer-events: none;
}
.opening-screen--cta-pending .opening-screen__cta { visibility: hidden; }
/* Over a photo, the shared shimmer's darker gold vanishes into the scene.
   Here it stays within the light golds, and a soft dark halo (a filter, so
   it works with the gradient-clipped text) lifts it off any background. */
.opening-screen__cta.shimmer-text {
  background-image: linear-gradient(
    100deg,
    var(--gold-400) 20%,
    var(--gold-200) 40%,
    #fffaf0 50%,
    var(--gold-200) 60%,
    var(--gold-400) 80%
  );
  text-shadow: none;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.55)) drop-shadow(0 0 10px rgba(0, 0, 0, 0.55));
}

/* ==========================================================================
   Motif particles
   One system, used by the opening's night field and by each story chapter's
   own motif (js/main.js -> buildMotif). Two particle shapes:
   .motif__p travels the full height of its host, .motif__s twinkles in place.
   Transform and opacity only, so it composites on the GPU.
   ========================================================================== */
/* z-index -1 puts the field above the host's own background but below its
   in-flow content, in both the opening screen and a story card (both
   establish a stacking context already). */
.motif {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: -1;
  contain: strict;
}
/* Only the chapter the guest is actually looking at animates. Reuses the
   is-showing class setupStoryEntrance() already maintains. Nothing on a
   section scrolled out of view animates either (setupOffscreenPause). */
.story-card:not(.is-showing) .motif__p,
.story-card:not(.is-showing) .motif__s,
.is-offscreen .motif__p,
.is-offscreen .motif__s,
.is-offscreen .motif__s::before,
.is-offscreen .motif__s::after,
.is-offscreen .story-card__nav-arrow,
.is-offscreen .scroll-cue__line,
.is-offscreen.hero .hero__backdrop,
.is-offscreen.hero .section-bg__layer,
.is-offscreen.hero .section-bg__blur,
.is-offscreen.hero .section-bg__glow {
  animation-play-state: paused;
}
/* Travelling particle: the element spans the host's full height so a
   translate of 100% is one host height, not one dot width. The dot itself
   is drawn on ::before at the leading edge. */
.motif__p {
  position: absolute;
  top: 0;
  left: var(--x, 50%);
  width: var(--s, 3px);
  height: 100%;
  opacity: 0;
  animation-duration: var(--d, 14s);
  animation-delay: var(--delay, 0s);
  animation-iteration-count: infinite;
  animation-timing-function: linear;
}
.motif__p::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: var(--s, 3px);
  height: var(--s, 3px);
  border-radius: 50%;
  background: currentColor;
}
.motif__s {
  position: absolute;
  left: var(--x, 50%);
  top: var(--y, 50%);
  width: var(--s, 2px);
  height: var(--s, 2px);
  border-radius: 50%;
  background: currentColor;
  opacity: 0.2;
  animation: motif-twinkle var(--d, 5s) ease-in-out var(--delay, 0s) infinite;
}
@keyframes motif-fall {
  0% { transform: translate3d(0, -6%, 0); opacity: 0; }
  12% { opacity: var(--o, 0.7); }
  88% { opacity: var(--o, 0.7); }
  100% { transform: translate3d(var(--drift, 14px), 106%, 0); opacity: 0; }
}
@keyframes motif-rise {
  0% { transform: translate3d(0, 104%, 0); opacity: 0; }
  18% { opacity: var(--o, 0.6); }
  82% { opacity: var(--o, 0.6); }
  100% { transform: translate3d(var(--drift, -12px), -6%, 0); opacity: 0; }
}
@keyframes motif-twinkle {
  0%, 100% { opacity: 0.15; transform: scale(1); }
  50% { opacity: var(--o, 0.9); transform: scale(1.45); }
}
.motif--snow { color: #eaf3ff; }
.motif--snow .motif__p { animation-name: motif-fall; }
.motif--stars { color: #e2e9ff; }
.motif--stars .motif__s { box-shadow: 0 0 5px 0.5px rgba(226, 233, 255, 0.55); }
/* The glint: two fine crossed rays through the star, drawn with gradients
   so they taper to nothing at the tips. */
.motif__s--glint {
  animation-name: motif-sparkle;
  box-shadow: 0 0 8px 1.5px rgba(226, 233, 255, 0.75);
}
.motif__s--glint::before,
.motif__s--glint::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--s, 2px) * 6);
  height: 1px;
  background: linear-gradient(to right, transparent, currentColor 50%, transparent);
  transform: translate(-50%, -50%);
}
.motif__s--glint::after { transform: translate(-50%, -50%) rotate(90deg); }
@keyframes motif-sparkle {
  0%, 100% { opacity: 0.1; transform: scale(0.5) rotate(0deg); }
  45% { opacity: var(--o, 1); transform: scale(1.35) rotate(20deg); }
  60% { opacity: calc(var(--o, 1) * 0.8); transform: scale(1) rotate(28deg); }
}
.motif--embers { color: #ffc98a; }
/* The blur sits on the dot, not on the full-height travelling element, so
   the GPU filters a few pixels rather than a strip the height of the card. */
.motif--embers .motif__p { animation-name: motif-rise; }
.motif--embers .motif__p::before { filter: blur(0.3px); }
.motif--gold { color: var(--gold-300); }
/* Chapter Two: slow, soft, candle-like lights drifting up. */
.motif--lights { color: #ffd6b0; }
.motif--lights .motif__p { animation-name: motif-rise; }
.motif--lights .motif__p::before {
  filter: blur(1.5px);
  box-shadow: 0 0 10px 2px rgba(255, 196, 140, 0.45);
}
.motif--gold .motif__p { animation-name: motif-rise; }

.envelope {
  position: relative;
  /* One width drives the whole envelope; the seal and monogram scale from
     it, so the envelope can be resized without drifting out of proportion. */
  --env-w: min(78vw, 286px);
  width: var(--env-w);
  aspect-ratio: 3 / 2;
  perspective: 1100px;
  filter: drop-shadow(0 26px 42px rgba(0, 0, 0, 0.42));
}
/* Paper: a soft cross-light so the panels read as stock, not as flat fills. */
.envelope__back {
  position: absolute;
  inset: 0;
  background:
    var(--jaali-pattern) center,
    linear-gradient(155deg, var(--champagne-100) 0%, var(--champagne-300) 52%, var(--gold-600) 100%);
  border-radius: 10px;
  border: 1px solid var(--gold-500);
}
/* A slim double gold ring, like the foil border on a formal Indian wedding
   card, framing the whole envelope. */
.envelope::before {
  content: "";
  position: absolute;
  inset: 5px;
  /* Above the flap, the card-rise animation and the seal, like a picture
     frame that stays put while the envelope opens behind it. */
  z-index: 6;
  pointer-events: none;
  border-radius: 8px;
  border: 1px solid rgba(201, 162, 74, 0.85);
  box-shadow: inset 0 0 0 3px rgba(251, 238, 203, 0.35), inset 0 0 0 4px rgba(201, 162, 74, 0.55);
}
.envelope__shadow {
  position: absolute;
  inset: 8% 6% -7% 6%;
  background: rgba(0, 0, 0, 0.3);
  filter: blur(18px);
  border-radius: 50%;
}
.envelope__card {
  position: absolute;
  left: 7%;
  right: 7%;
  top: 9%;
  bottom: 7%;
  background: linear-gradient(170deg, #fffdf7, var(--cream-50));
  border: 1px solid var(--champagne-400);
  border-radius: 5px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 12px 22px rgba(0, 0, 0, 0.18);
  z-index: 1;
}
.envelope__card-monogram {
  position: relative;
  font-family: var(--font-display);
  font-style: italic;
  font-size: calc(var(--env-w) * 0.095);
  letter-spacing: 0.04em;
  color: var(--wine-600);
}
.envelope__card-monogram::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: calc(var(--env-w) * -0.09);
  transform: translateX(-50%) rotate(45deg);
  width: 6px;
  height: 6px;
  background: var(--gold-500);
}
/* Front panel. The V notch is where the flap's point lands, so with the flap
   down the card is fully hidden, and with the flap up the card rises out
   through the notch the way it would from a real envelope. */
.envelope__front {
  position: absolute;
  inset: 0;
  background:
    var(--jaali-pattern) center,
    linear-gradient(200deg, var(--champagne-100) 0%, var(--champagne-300) 48%, var(--gold-600) 100%);
  clip-path: polygon(0 0, 50% 55%, 100% 0, 100% 100%, 0 100%);
  border-radius: 10px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
  z-index: 2;
}
.envelope__flap {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 58%;
  background:
    var(--jaali-pattern) center,
    linear-gradient(168deg, var(--champagne-100) 0%, var(--champagne-300) 48%, var(--gold-600) 100%);
  clip-path: polygon(0 0, 100% 0, 50% 100%);
  transform-origin: top center;
  transform: rotateX(0deg);
  transform-style: preserve-3d;
  transition: transform 1.05s var(--ease-emphasis);
  z-index: 3;
  border-radius: 10px 10px 0 0;
  /* A crease down the fold and a shadow where the flap meets the front. */
  box-shadow: 0 6px 10px -6px rgba(0, 0, 0, 0.45);
  backface-visibility: hidden;
}
/* Lined flap: the underside is emerald, so opening the envelope reveals a
   colour you only see once. Standard two-faced trick, both faces hidden from
   behind so exactly one shows at a time. */
.envelope__flap::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(160deg, var(--emerald-600), var(--emerald-900));
  transform: rotateX(180deg);
  backface-visibility: hidden;
}
.envelope__seal::before {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  border: 1px solid rgba(201, 162, 74, 0.85);
  box-shadow: 0 0 0 2px rgba(251, 238, 203, 0.3);
}
.envelope__seal {
  position: absolute;
  left: 50%;
  top: 47%;
  width: calc(var(--env-w) * 0.168);
  height: calc(var(--env-w) * 0.168);
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 4;
}
/* Two halves of pressed wax. Slightly irregular radii so it reads hand
   stamped rather than like a perfect CSS circle. */
.envelope__seal-half {
  position: absolute;
  inset: 0;
  border-radius: 48% 52% 51% 49% / 50% 48% 52% 50%;
  background:
    radial-gradient(circle at 34% 28%, var(--wine-300) 0%, var(--wine-500) 45%, var(--wine-700) 100%);
  box-shadow:
    inset 0 -2px 4px rgba(30, 4, 10, 0.55),
    inset 0 2px 3px rgba(230, 150, 165, 0.45),
    0 6px 14px rgba(0, 0, 0, 0.4);
  transition: transform 0.9s var(--ease-emphasis), opacity 0.7s 0.15s var(--ease-out-soft);
  animation: seal-glow 4.2s ease-in-out infinite;
}
.envelope__seal-half--l { clip-path: polygon(0 0, 51% 0, 49% 100%, 0 100%); }
.envelope__seal-half--r { clip-path: polygon(51% 0, 100% 0, 100% 100%, 49% 100%); }
.envelope__seal-text {
  position: relative;
  z-index: 1;
  font-family: var(--font-display);
  font-size: calc(var(--env-w) * 0.051);
  letter-spacing: 0.02em;
  color: var(--champagne-100);
  text-shadow: 0 1px 2px rgba(30, 4, 10, 0.7);
  transition: opacity 0.35s var(--ease-out);
}
@keyframes seal-glow {
  0%, 100% { box-shadow: inset 0 -2px 4px rgba(30,4,10,0.55), inset 0 2px 3px rgba(230,150,165,0.45), 0 6px 14px rgba(0,0,0,0.4); }
  50% { box-shadow: inset 0 -2px 4px rgba(30,4,10,0.55), inset 0 2px 3px rgba(230,150,165,0.45), 0 6px 14px rgba(0,0,0,0.4), 0 0 20px rgba(156,58,76,0.75); }
}

/* Opening: the seal cracks apart, the flap falls open, the card rises out. */
.opening-screen.is-opening .envelope__seal-half {
  animation: none;
  opacity: 0;
}
.opening-screen.is-opening .envelope__seal-half--l {
  transform: translate(-15px, 12px) rotate(-26deg);
}
.opening-screen.is-opening .envelope__seal-half--r {
  transform: translate(15px, 14px) rotate(28deg);
}
.opening-screen.is-opening .envelope__seal-text { opacity: 0; transition-duration: 0.2s; }
.opening-screen.is-opening .envelope__flap {
  transform: rotateX(175deg);
}
/* z-index is stepped inside the keyframes so the card only crosses in front
   of the envelope once it is actually clear of the notch, with no pop. */
.opening-screen.is-opening .envelope__card {
  animation: envelope-card-rise 1.25s 0.42s var(--ease-emphasis) forwards;
}
@keyframes envelope-card-rise {
  0% { transform: translateY(0) scale(1); z-index: 1; }
  30% { z-index: 5; }
  100% { transform: translateY(-66%) scale(1.04); z-index: 5; }
}

/* ==========================================================================
   Music toggle
   ========================================================================== */
.music-toggle {
  position: fixed;
  top: max(1rem, env(safe-area-inset-top));
  right: max(1rem, env(safe-area-inset-right));
  z-index: 60;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--gold-400);
  background: rgba(251, 247, 238, 0.85);
  backdrop-filter: blur(6px);
  color: var(--emerald-700);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-card);
  transition: transform var(--dur-fast) var(--ease-out-soft),
    background var(--dur-fast) var(--ease-out-soft),
    opacity 0.7s var(--ease-out-soft);
  touch-action: manipulation;
}
.music-toggle.is-idle { opacity: 0.4; }
.music-toggle:hover { transform: scale(1.06); opacity: 1; }
.music-toggle:active { transform: scale(0.95); }
/* The icon shows the CURRENT state, not the action: a crossed-out note
   means silent, a plain note means the music is playing. aria-pressed
   carries the same state for screen readers. */
.music-toggle__icon--on { display: none; }
.music-toggle__icon--off { display: block; }
.music-toggle[aria-pressed="true"] .music-toggle__icon--on { display: block; }
.music-toggle[aria-pressed="true"] .music-toggle__icon--off { display: none; }
/* display:flex above would otherwise beat the browser's [hidden] rule and
   leave a dead button on screen when music.enabled is false. */
.music-toggle[hidden] { display: none; }
/* A quiet second signal that it's playing, so state reads at a glance. */
.music-toggle.is-playing {
  background: var(--gold-400);
  color: var(--emerald-900);
  border-color: var(--gold-500);
}

/* ==========================================================================
   Hero
   ========================================================================== */
.hero {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  background: var(--emerald-800);
  color: var(--on-emerald);
  overflow: hidden;
}
/* Inserted by main.js only when no real background photo is configured
   (SITE.sectionBackgrounds.hero.type === "none"), so it never fights with
   the config-driven .section-bg layer once a real photo is added. Carries
   the same slow Ken Burns drift either way (see the #hero .section-bg__layer
   rule below) so the hero always breathes, even in the placeholder state. */
.hero__backdrop {
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(180deg, var(--emerald-900) 0%, var(--emerald-800) 55%, var(--emerald-700) 100%);
  transform-origin: center;
  animation: hero-kenburns 22s ease-in-out infinite alternate;
}
/* Scoped so this motion applies to the hero specifically, once a real photo
   is configured via SITE.sectionBackgrounds.hero (type: "image"); other
   sections get their own distinct treatment (e.g. parallax for story) in a
   later stage, not this shared background layer's default behavior. */
#hero .section-bg__layer,
#hero .section-bg__blur,
#hero .section-bg__glow {
  transform-origin: center;
  animation: hero-kenburns 22s ease-in-out infinite alternate;
}
@keyframes hero-kenburns {
  from { transform: scale(1); }
  to { transform: scale(1.09); }
}
.hero__corner {
  position: absolute;
  width: 58px;
  height: 58px;
  color: var(--gold-500);
  opacity: 0.68;
}
.hero__corner--tl { top: 20px; left: 20px; }
.hero__corner--br { bottom: 20px; right: 20px; transform: rotate(180deg); }

/* Shared ornament: a hairline rule broken by a small diamond. Replaces the
   font dingbat that was doing this job, and gives the dark sections one
   consistent piece of ornament instead of three different ideas. */
.ornament {
  width: clamp(104px, 34vw, 150px);
  height: auto;
  color: var(--gold-500);
}

/* The photo slot is a medallion above the type block, not a large disc
   wedged in among the lines where it would outweigh the names. */
.hero__medallion { margin-bottom: var(--space-5); }
.hero__names {
  /* Preferred term (11vw) stays below the floor across the whole common
     phone range (320-428px), so this genuinely scales down on narrow
     phones instead of sitting at a flat floor value. */
  font-size: clamp(1.9rem, 11vw, 3.6rem);
  line-height: 1.15;
  overflow-wrap: break-word;
}
/* Rise-and-settle: reuses the shared .reveal fade-up (opacity/transform),
   layering a letter-spacing tighten on top as the names land, per
   motion-spec.md #2. The #hero-names id keeps this specific enough to win
   over the plain .reveal rules regardless of stylesheet order. */
#hero-names {
  letter-spacing: 0.3em;
  transition: opacity 0.8s var(--ease-out), transform 0.8s var(--ease-out),
    letter-spacing 1.2s var(--ease-out);
}
#hero-names.in-view {
  letter-spacing: 0.01em;
}
.hero__subline {
  margin-top: var(--space-2);
  font-family: var(--font-display);
  font-style: italic;
  color: var(--gold-300);
  font-size: var(--step-1);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 1px 12px rgba(0, 0, 0, 0.45);
}
/* The invitation itself: quieter than the names, a touch warmer than body. */
.hero__invite {
  max-width: 19rem;
  margin-top: var(--space-2);
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  font-size: var(--step-0);
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: rgba(251, 247, 238, 0.92);
  text-wrap: balance;
  /* One soft, wide shadow: lifts it off the pale sky without a hard edge. */
  text-shadow: 0 1px 14px rgba(0, 0, 0, 0.4);
}
.hero__ornament { margin: var(--space-5) 0; }
.hero__date {
  font-family: var(--font-display);
  font-size: var(--step-2);
  letter-spacing: 0.16em;
  text-indent: 0.16em;
  color: var(--on-emerald);
  /* Fades in beneath the names once they've had a moment to settle. */
  transition-delay: 0.35s;
}
.hero__place {
  margin-top: var(--space-2);
  font-size: var(--step--2);
  letter-spacing: 0.26em;
  text-indent: 0.26em;
  text-transform: uppercase;
  color: var(--on-emerald-soft);
  opacity: 0.75;
}
.hero__tagline {
  max-width: 19rem;
  margin-top: var(--space-6);
  font-size: var(--step-0);
  line-height: 1.65;
  color: var(--on-emerald-soft);
  font-style: italic;
  font-family: var(--font-display);
}
/* "Scroll": a word and a hairline that draws itself downward, at the foot of
   each full-screen section with more below it (the hero, Chapter Five).
   Centred with auto margins rather than a transform, because the .reveal
   fade-up animates transform and would knock it off centre. */
.scroll-cue {
  position: absolute;
  bottom: max(1.5rem, env(safe-area-inset-bottom));
  left: 0;
  right: 0;
  width: fit-content;
  margin: 0 auto;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
}
.scroll-cue__label {
  font-size: var(--step--2);
  letter-spacing: 0.28em;
  text-indent: 0.28em;
  text-transform: uppercase;
  color: var(--gold-400);
  opacity: 0.7;
}
.scroll-cue__line {
  width: 1px;
  height: 34px;
  background: linear-gradient(to bottom, var(--gold-400), transparent);
  transform-origin: top center;
  animation: cue-draw 2.4s var(--ease-in-out-soft) infinite;
}
@keyframes cue-draw {
  0% { transform: scaleY(0.2); opacity: 0.2; }
  45% { transform: scaleY(1); opacity: 0.85; }
  100% { transform: scaleY(1); opacity: 0; }
}

/* image placeholder / real photo frame, shared */
.img-frame {
  overflow: hidden;
  border-radius: var(--radius-md);
  border: 1px solid var(--gold-400);
  box-shadow: var(--shadow-card);
  background: var(--cream-100);
}
.img-frame img { width: 100%; height: 100%; object-fit: cover; }
/* Zoomed out (imageVisibleHeight): the photo at full height in the middle,
   a blurred, darkened copy of itself filling the frame behind it. */
.img-frame--zoomed-out { position: relative; }
.img-frame--zoomed-out::before {
  content: "";
  position: absolute;
  inset: -20px;
  background: var(--frame-img) center / cover no-repeat;
  filter: blur(16px) brightness(0.72) saturate(1.1);
}
.img-frame--zoomed-out img {
  position: absolute;
  left: 50%;
  width: auto;
  max-width: none;
  transform: translateX(-50%);
  object-fit: fill;
  box-shadow: 0 0 18px rgba(0, 0, 0, 0.35);
}

.hero__medallion .img-frame {
  width: clamp(104px, 27vw, 132px);
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  border-color: var(--gold-500);
  box-shadow: 0 0 0 1px rgba(221, 192, 119, 0.25), 0 0 30px -8px rgba(221, 192, 119, 0.5);
}

/* ==========================================================================
   Section headings (shared)
   ========================================================================== */
.section-heading {
  text-align: center;
  font-size: clamp(1.6rem, 8vw, 2.4rem);
  color: var(--emerald-800);
}
.section-subheading {
  text-align: center;
  margin-top: var(--space-2);
  color: var(--ink-soft);
  font-style: italic;
  font-family: var(--font-display);
  font-size: var(--step-1);
}
/* An eyebrow above the heading, used where a section would otherwise repeat
   the heading + italic subheading shape for the third time running. */
.section-eyebrow {
  text-align: center;
  margin-bottom: var(--space-3);
  font-size: var(--step--2);
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  text-transform: uppercase;
  color: var(--gold-700);
}

/* ==========================================================================
   Story carousel
   ========================================================================== */
.story { background: var(--cream-50); }
.story-carousel {
  position: relative;
  margin-top: 2.5rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
/* One chapter at a time: each card is the full width of the track, and the
   gap between cards is wider than the side padding, so the next chapter
   stays fully off screen until the guest swipes. */
.story-track {
  /* The cards' offsetParent, so offsetLeft is measured from the track
     itself (dots, arrows and "Swipe for" all centre a card with it). */
  position: relative;
  display: flex;
  gap: 2rem;
  overflow-x: auto;
  /* Must be explicit: overflow-x: auto silently makes overflow-y auto too,
     and a few pixels of vertical overflow (the entrance slide-up, the
     nudge) let the track capture an up/down swipe, scroll those pixels,
     and never pass the rest on to the page. */
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  padding: 0.5rem 1rem 1.25rem;
  scrollbar-width: none;
  /* Both axes: the browser locks each gesture to one direction itself, so
     a sideways swipe turns the page and an up/down swipe scrolls the page.
     pan-x alone blocked vertical scrolling anywhere a finger landed on a
     card, which on a phone is nearly the whole screen. */
  touch-action: pan-x pan-y;
  overscroll-behavior-x: contain;
}
.story-track::-webkit-scrollbar { display: none; }
/* Full-bleed chapter panel. The mood colour and the chapter's motif own the
   whole card; the text sits on that weather, and the photo is an inset
   artifact below the telling rather than a slab on top of it. */
.story-card {
  position: relative;
  isolation: isolate;
  flex: 0 0 100%;
  scroll-snap-align: center;
  scroll-snap-stop: always;
  background: var(--emerald-900);
  border-radius: var(--radius-lg);
  border: 1px solid rgba(221, 192, 119, 0.35);
  box-shadow: var(--shadow-lift);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* Keeps each card's layout and paint work inside the card. */
  contain: layout paint;
}
.story-card.has-bg { border-color: rgba(221, 192, 119, 0.42); }
.story-card__body {
  padding: var(--space-6) var(--space-5) var(--space-6);
  display: flex;
  flex-direction: column;
  flex: 1;
}
/* Per-chapter entrance (motion-spec.md #3): image slides in from one side,
   text fades up a beat later. Re-triggered by setupStoryEntrance() each
   time a card scrolls/swipes into view, so revisiting a chapter still
   feels like turning to that page again. */
.story-card--entrance .story-card__chapter,
.story-card--entrance .story-card__title,
.story-card--entrance .story-card__prose,
.story-card--entrance .story-card__nav {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.75s var(--ease-out-expo), transform 0.75s var(--ease-out-expo);
}
.story-card--entrance .story-card__title { transition-delay: 0.08s; }
.story-card--entrance .story-card__prose { transition-delay: 0.3s; }
.story-card--entrance .story-card__nav { transition-delay: 0.5s; }
.story-card--entrance .story-card__figure {
  opacity: 0;
  transform: translateX(38px);
  transition: opacity 0.85s var(--ease-out-expo) 0.16s, transform 0.85s var(--ease-out-expo) 0.16s;
}
.story-card--entrance.is-showing .story-card__chapter,
.story-card--entrance.is-showing .story-card__title,
.story-card--entrance.is-showing .story-card__prose,
.story-card--entrance.is-showing .story-card__nav,
.story-card--entrance.is-showing .story-card__figure {
  opacity: 1;
  transform: none;
}

/* Extra scale buffer so the JS-driven horizontal parallax (setupStoryParallax)
   always has room to shift the background without ever revealing an edge.
   main.js only adds this class on desktop: on a phone, four promoted
   card-sized layers at 1.2x scale cost far more GPU memory than the effect
   is worth (motion-spec.md, "Mobile parallax that janks"). */
.section-bg--parallax { will-change: transform; }
/* Label and title stay centred (they are headings); the narrative itself is
   left aligned, because 60 words of ragged-both-edges text on a phone is
   genuinely harder to read. */
.story-card__chapter {
  text-align: center;
  font-size: var(--step--2);
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  text-transform: uppercase;
  color: var(--gold-400);
  opacity: 0.85;
}
.story-card__title {
  text-align: center;
  font-size: var(--step-3);
  font-weight: 500;
  line-height: 1.2;
  margin-top: var(--space-2);
  color: var(--on-emerald);
}
/* margin-bottom guarantees breathing room above the prompt even on the
   tallest card, where the prompt's margin-top: auto resolves to zero. */
.story-card__prose { margin-top: var(--space-5); margin-bottom: var(--space-5); }
.story-card__paragraph {
  font-size: var(--step-0);
  color: rgba(244, 236, 217, 0.86);
  line-height: 1.72;
}
.story-card__paragraph + .story-card__paragraph { margin-top: var(--space-4); }

.story-card__figure {
  margin: var(--space-5) 0 0;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.story-card__figure .img-frame {
  width: 100%;
  aspect-ratio: 3 / 2;
  border-color: rgba(221, 192, 119, 0.45);
  box-shadow: 0 14px 30px -18px rgba(0, 0, 0, 0.8);
}

/* The prompt at the foot of each card: "Swipe for Chapter Two", or on the
   last card, "Continue to Chapter Five". It sits where the reader finishes,
   and it is a real button for anyone who taps rather than swipes. The
   margin-top: auto pins it to the bottom of cards of equal height. */
.story-card__nav {
  margin-top: auto;
  align-self: center;
  max-width: 100%;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  background: none;
  border: none;
  color: var(--gold-300);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}
.story-card__nav-kicker {
  font-family: var(--font-body);
  font-size: var(--step--2);
  letter-spacing: 0.24em;
  text-indent: 0.24em;
  text-transform: uppercase;
  opacity: 0.8;
  white-space: nowrap;
}
.story-card__nav-target {
  display: inline-flex;
  align-items: center;
  gap: clamp(6px, 2vw, 12px);
  white-space: nowrap;
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--step-1);
}
/* An arrow that keeps drifting toward the next card: the prompt reads as
   "this way" even for a guest who does not stop to read it. */
.story-card__nav-arrow {
  position: relative;
  flex: none;
  width: 20px;
  height: 1px;
  background: currentColor;
  animation: nav-drift 1.8s var(--ease-in-out-soft) infinite;
}
.story-card__nav-arrow::after {
  content: "";
  position: absolute;
  right: 0;
  top: -3px;
  width: 7px;
  height: 7px;
  border-top: 1px solid currentColor;
  border-right: 1px solid currentColor;
  transform: rotate(45deg);
}
@keyframes nav-drift {
  0%, 100% { transform: translateX(0); }
  50% { transform: translateX(6px); }
}
/* The hand-over to Page 2 is the one filled button on the page. */
.story-card__nav--final {
  /* Side padding eases off on the narrowest phones so each line stays whole. */
  padding: var(--space-3) clamp(12px, 4vw, 24px);
  color: var(--emerald-900);
  background: var(--gold-400);
  border-radius: 18px;
  box-shadow: 0 10px 24px -12px rgba(0, 0, 0, 0.7);
}
.story-card__nav--final .story-card__nav-kicker { opacity: 0.75; }

/* One nudge the first time the chapters come into view, so the guest can
   see there is more to the side (setupStoryCarouselControls). */
.story-track.is-nudging .story-card {
  animation: carousel-nudge 1.2s var(--ease-in-out-soft);
}
@keyframes carousel-nudge {
  0%, 100% { transform: translateX(0); }
  40% { transform: translateX(-34px); }
  70% { transform: translateX(6px); }
}
.story-nav {
  flex: none;
  display: none;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--gold-400);
  background: #fff;
  color: var(--emerald-700);
  font-size: 1.3rem;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-card);
}
.story-dots {
  margin-top: 1rem;
  display: flex;
  justify-content: center;
  gap: 0.5rem;
}
/* Visual dot stays small; the button itself carries a full ~44px tap
   target via padding, with the dot drawn on a ::before so the hit area
   doesn't look oversized. */
.story-dots__dot {
  width: 44px;
  height: 44px;
  padding: 0;
  border: none;
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
}
.story-dots__dot::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--cream-200);
  border: 1px solid var(--gold-400);
  transition: background 0.3s, transform 0.3s;
}
.story-dots__dot.is-active::before {
  background: var(--gold-500);
  transform: scale(1.25);
}

/* ==========================================================================
   Transition beat (motion-spec.md #4), used by Chapter Five
   The first screen of Page 2: a breath between the story and the
   invitation. One full-screen moment, generous space, nothing else
   competing for attention.
   ========================================================================== */
.transition-beat {
  min-height: 100svh;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  background: var(--emerald-900);
  overflow: hidden;
}
.transition-beat::before {
  /* The through-line that the background is always doing something slow:
     a very gentle glow drifting behind the line, nowhere near as bold as
     the opening's shimmer since this moment is meant to read as a breath. */
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 60vmax;
  height: 60vmax;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(201, 162, 74, 0.16), transparent 70%);
  animation: transition-glow 8s ease-in-out infinite;
}
@keyframes transition-glow {
  0%, 100% { opacity: 0.6; transform: translate(-50%, -50%) scale(1); }
  50% { opacity: 1; transform: translate(-50%, -50%) scale(1.15); }
}
.chapter-five__inner {
  position: relative;
  max-width: 21rem;
  color: var(--on-emerald);
}
.chapter-five__label {
  font-size: var(--step--2);
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  text-transform: uppercase;
  color: var(--gold-400);
}
.chapter-five__title {
  margin-top: var(--space-3);
  font-family: var(--font-display);
  font-weight: 500;
  font-size: var(--step-4);
  line-height: 1.15;
  color: var(--on-emerald);
  outline: none;
}
/* The same hairline-and-diamond language as the hero, under the title. */
.chapter-five__title::after {
  content: "";
  display: block;
  width: 46px;
  height: 1px;
  margin: var(--space-5) auto 0;
  background: linear-gradient(to right, transparent, var(--gold-500), transparent);
}
.chapter-five__prose {
  margin-top: var(--space-5);
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--step-1);
  line-height: 1.6;
  color: rgba(244, 236, 217, 0.88);
}
.chapter-five__prose p + p { margin-top: var(--space-4); }

/* On a photo (the bright Hallstatt sky and snow), the lines get help that
   leaves the background values alone: a soft, edgeless shade behind the
   text block only, a text shadow on every line, and fully opaque, slightly
   heavier prose. */
.chapter-five.has-photo .chapter-five__inner::before {
  content: "";
  position: absolute;
  inset: -110px -60px;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(ellipse closest-side at center,
    rgba(4, 18, 28, 0.46) 0%, rgba(4, 18, 28, 0.36) 35%, rgba(4, 18, 28, 0.16) 70%, rgba(4, 18, 28, 0) 100%);
}
/* The cue sits over bright water here, so it gets the same lift. */
.chapter-five.has-photo .scroll-cue__label {
  opacity: 1;
  color: var(--gold-300);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5), 0 0 12px rgba(0, 0, 0, 0.6);
}
.chapter-five.has-photo .scroll-cue__line { filter: drop-shadow(0 0 3px rgba(0, 0, 0, 0.7)); }
.chapter-five.has-photo .chapter-five__label,
.chapter-five.has-photo .chapter-five__title,
.chapter-five.has-photo .chapter-five__prose {
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45), 0 2px 14px rgba(0, 0, 0, 0.55);
}
.chapter-five.has-photo .chapter-five__label { color: var(--gold-300); }
.chapter-five.has-photo .chapter-five__prose {
  color: var(--cream-50);
  font-weight: 500;
}

/* "Back to our story": quiet, top of Page 2, clear of the music toggle on
   the right. */
.page-back {
  position: absolute;
  top: max(1.25rem, env(safe-area-inset-top));
  left: max(var(--section-pad-x), env(safe-area-inset-left));
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  font-size: var(--step--1);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--gold-300);
  opacity: 0.85;
}
.page-back::before {
  content: "";
  width: 7px;
  height: 7px;
  border-left: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: rotate(45deg);
}

/* ==========================================================================
   Pages (CLAUDE.md, "Two-page structure")
   Two views in one document, swapped by js/main.js (showPage). A short
   crossfade on the way out and in; reduced motion swaps instantly.
   ========================================================================== */
.page { transition: opacity 0.38s var(--ease-out-soft); }
.page.is-leaving,
.page.is-entering { opacity: 0; }

/* ==========================================================================
   Details / events / timeline
   ========================================================================== */
.details { background: var(--cream-100); }

/* Live countdown (motion-spec.md #5), moved here from the hero: it belongs
   alongside the logistics it's counting down to, not the calm arrival
   moment. Styled for the details section's light background. */
.countdown {
  margin: 2.25rem auto 0;
  text-align: center;
}
.countdown__label {
  font-family: var(--font-display);
  font-style: italic;
  color: var(--emerald-700);
  font-size: 1.05rem;
}
.countdown__units {
  margin-top: var(--space-4);
  display: flex;
  justify-content: center;
}
/* Hairline rules between the units, so it reads as one considered object
   rather than four numbers sitting near each other. */
.countdown__unit {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 62px;
  padding: 0 var(--space-3);
  border-left: 1px solid rgba(173, 134, 54, 0.28);
}
.countdown__unit:first-child { border-left: none; }
.countdown__value {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 500;
  line-height: 1.1;
  color: var(--emerald-800);
  font-variant-numeric: tabular-nums;
}
.countdown__unit-label {
  font-size: var(--step--2);
  letter-spacing: 0.18em;
  text-indent: 0.18em;
  text-transform: uppercase;
  color: var(--gold-700);
  margin-top: var(--space-2);
}
.countdown__arrived {
  max-width: 22rem;
  margin: 0 auto;
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--step-2);
  line-height: 1.35;
  text-wrap: balance;
  color: var(--emerald-800);
}

/* The two event cards ARE the timeline: one hairline spine runs through
   them in date order with a node at each. The old "At a glance" block
   restated the same venue, time and date one screen further down. */
.events-list {
  position: relative;
  margin: var(--space-7) auto 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-7);
  max-width: 480px;
}
/* The connector runs behind the cards, so it only shows in the gap between
   them: two moments, in order, joined. Negative z-index keeps it under the
   card faces without needing a wrapper. */
.events-list::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 12px;
  bottom: 12px;
  width: 1px;
  transform: translateX(-50%);
  background: var(--gold-500);
  opacity: 0.4;
  z-index: -1;
}
.event-card__marker {
  position: absolute;
  left: 50%;
  top: calc(var(--space-7) / -2);
  width: 9px;
  height: 9px;
  transform: translate(-50%, -50%) rotate(45deg);
  background: var(--gold-500);
  box-shadow: 0 0 0 5px var(--cream-100);
}
/* Nothing precedes the first card, so it carries no node. */
.event-card:first-child .event-card__marker { display: none; }
.event-card {
  background: #fff;
  border: 1px solid var(--gold-400);
  border-radius: var(--radius-lg);
  padding: var(--space-5) var(--space-5) calc(var(--space-5) + 2px);
  text-align: center;
  box-shadow: var(--shadow-card);
  position: relative;
  /* Tighter than body copy: a formal card, set close, not a page of prose. */
  line-height: 1.5;
}
/* Even line breaks, so a name never sits alone on its own line. */
.event-card p { text-wrap: balance; }
.event-card::before {
  content: "";
  position: absolute;
  inset: 8px;
  border: 1px solid var(--cream-200);
  border-radius: 14px;
  pointer-events: none;
}
.event-card__label {
  font-size: 0.72rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--gold-600);
}
.event-card__hosts {
  margin-top: 0.7rem;
  font-size: 1rem;
  color: var(--ink-soft);
}
.event-card__relation {
  margin-top: 0.35rem;
  font-size: 1rem;
  color: var(--ink-soft);
}
.event-card__datetime {
  margin-top: 0.8rem;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 1.05rem;
  color: var(--ink);
}
.event-card__venue {
  margin-top: 0.3rem;
  font-size: 1rem;
  color: var(--ink-soft);
}

/* The formal wording is revealed piece by piece as the card comes into
   view (motion-spec.md #5: "reads like a formal card being presented
   rather than a block of text"), layered on top of the card's own
   .reveal fade-up so the card materializes, then the wording unfolds. */
.event-card__label,
.event-card__hosts,
.event-card__relation,
.event-card__datetime,
.event-card__venue {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out);
}
.event-card.in-view .event-card__label { opacity: 1; transform: none; transition-delay: 0.05s; }
.event-card.in-view .event-card__hosts { opacity: 1; transform: none; transition-delay: 0.15s; }
.event-card.in-view .event-card__relation { opacity: 1; transform: none; transition-delay: 0.25s; }
.event-card.in-view .event-card__datetime { opacity: 1; transform: none; transition-delay: 0.35s; }
.event-card.in-view .event-card__venue { opacity: 1; transform: none; transition-delay: 0.45s; }

/* ==========================================================================
   Map
   ========================================================================== */
.map-section { background: var(--cream-50); }
/* Segmented control: two venues, one embed. */
.venue-switch {
  margin: var(--space-6) auto 0;
  display: flex;
  gap: 4px;
  padding: 4px;
  width: fit-content;
  max-width: 100%;
  border: 1px solid rgba(173, 134, 54, 0.35);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.6);
}
.venue-switch__option {
  min-height: 44px;
  padding: 0 var(--space-5);
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-soft);
  font-family: var(--font-body);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft);
}
.venue-switch__option.is-selected {
  background: var(--emerald-800);
  color: var(--cream-50);
}
.venue-panel { margin: var(--space-5) auto 0; max-width: 560px; }
.venue-panel__address {
  text-align: center;
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--step-1);
  color: var(--ink);
}
/* Directions is the primary action here, so it gets the only filled button
   in the section. */
.venue-panel__directions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 48px;
  margin: var(--space-4) auto 0;
  max-width: 280px;
  padding: 0 var(--space-6);
  font-family: var(--font-body);
  font-size: var(--step-0);
  letter-spacing: 0.04em;
  color: var(--emerald-900);
  background: var(--gold-400);
  border-radius: 999px;
  text-decoration: none;
  box-shadow: 0 10px 24px -12px rgba(173, 134, 54, 0.9);
  transition: transform var(--dur-fast) var(--ease-out-soft), background var(--dur-fast) var(--ease-out-soft);
}
.venue-panel__directions::after {
  content: "";
  width: 7px;
  height: 7px;
  border-top: 1.5px solid currentColor;
  border-right: 1.5px solid currentColor;
  transform: rotate(45deg);
}
.venue-panel__directions:hover { background: var(--gold-500); }
.venue-panel__directions:active { transform: scale(0.97); }

.map-embed {
  position: relative;
  margin: var(--space-4) auto 0;
  max-width: 560px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--gold-400);
  box-shadow: var(--shadow-card);
  aspect-ratio: 3 / 2;
}
.map-embed iframe { width: 100%; height: 100%; border: 0; }
/* Full-cover button blocks direct touch interaction with the map until
   tapped once, so a swipe that merely passes over the map keeps scrolling
   the page instead of panning the map. The hit area is the whole tile;
   only the inner label is styled as a small centered pill. Disappears on
   tap, after which the map underneath is fully usable. */
.map-embed__activate {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  padding: 0;
  border: none;
  background: rgba(11, 42, 31, 0.18);
}
.map-embed__activate-label {
  padding: 0.6rem 1.1rem;
  font-family: var(--font-body);
  font-size: 0.85rem;
  letter-spacing: 0.02em;
  color: var(--on-emerald);
  background: rgba(11, 42, 31, 0.65);
  border: 1px solid var(--gold-400);
  border-radius: 999px;
  backdrop-filter: blur(2px);
}
.map-embed.is-active .map-embed__activate { display: none; }

/* ==========================================================================
   Footer / Close (motion-spec.md #7)
   Mirrors the opening: full screen, dark, the gold shimmer returns, slow
   fade, so the experience has a clear beginning and end.
   ========================================================================== */
.footer {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding-top: 4rem;
  padding-bottom: max(3.5rem, env(safe-area-inset-bottom));
  background: var(--emerald-900);
  color: var(--on-emerald-soft);
}
/* Slower, more deliberate than the sitewide .reveal fade-up, since this is
   the final beat, not just another section arriving. */
.footer__reveal--slow {
  transition-duration: 1.6s;
  transition-delay: 0.1s;
}
.footer__ornament { margin-bottom: var(--space-6); }
.footer__line {
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(1.3rem, 5.5vw, 1.7rem);
  letter-spacing: 0.02em;
}
.footer__actions {
  margin-top: var(--space-8);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-6);
}
.footer__action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 var(--space-2);
  font-family: var(--font-body);
  font-size: var(--step--1);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold-300);
  background: transparent;
  border: none;
  text-decoration: none;
  opacity: 0.85;
  transition: opacity var(--dur-fast) var(--ease-out-soft);
}
.footer__action:hover { opacity: 1; }
/* The hairline sits on an inner rule rather than the button box, so the
   underline hugs the text instead of the 44px tap target. */
.footer__action::after {
  content: "";
  position: absolute;
  left: var(--space-2);
  right: var(--space-2);
  bottom: 12px;
  height: 1px;
  background: rgba(221, 192, 119, 0.4);
}
.footer__action { position: relative; }

/* ==========================================================================
   Photo sections
   Story, details and map were set in dark ink for a cream page. On a photo
   under a dark scrim they switch to cream and gold. The two invitation cards
   stay solid cream, sitting on the photo like printed cards.
   ========================================================================== */
/* A soft shadow under type that sits straight on a photo, for the bright
   patches the scrim cannot fully quiet (sky, blossom, sunlit walls). */
.has-photo .section-heading,
.has-photo .section-subheading,
.has-photo .section-eyebrow,
.details.has-photo .countdown,
.map-section.has-photo .venue-panel__address,
.footer.has-photo .footer__line,
.footer.has-photo .footer__action {
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.5);
}
.footer.has-photo .footer__action { opacity: 1; }
/* On the mural photo the lower half is the building; keep the closing line
   up in the evening sky rather than centred over the rooftops. */
.footer.has-photo {
  justify-content: flex-start;
  padding-top: max(4rem, 26vh, 26svh);
}
.has-photo .section-heading { color: var(--cream-50); }
.has-photo .section-subheading { color: rgba(244, 236, 217, 0.85); }
.has-photo .section-eyebrow { color: var(--gold-300); }
.story.has-photo .story-dots__dot::before { background: rgba(244, 236, 217, 0.35); }
.story.has-photo .story-dots__dot.is-active::before { background: var(--gold-400); }

.details.has-photo .countdown__label { color: rgba(244, 236, 217, 0.9); }
.details.has-photo .countdown__value,
.details.has-photo .countdown__arrived { color: var(--cream-50); }
.details.has-photo .countdown__unit-label { color: var(--gold-300); }
.details.has-photo .countdown__unit { border-left-color: rgba(221, 192, 119, 0.35); }
/* Frosted glass: cream at --card-opacity over a blurred view of the photo
   (both set from SITE.eventCards), so the photo shows through and the
   wording stays crisp. */
.details.has-photo .event-card {
  background: rgba(251, 247, 238, var(--card-opacity, 1));
  -webkit-backdrop-filter: blur(var(--card-blur, 0px));
  backdrop-filter: blur(var(--card-blur, 0px));
}
/* See-through cards would show the full-length spine running through them,
   so on a photo the connector is drawn only in the gap between the cards. */
.details.has-photo .events-list::before { display: none; }
.details.has-photo .event-card + .event-card::after {
  content: "";
  position: absolute;
  left: 50%;
  top: calc(var(--space-7) * -1);
  width: 1px;
  height: var(--space-7);
  transform: translateX(-50%);
  background: var(--gold-400);
  opacity: 0.7;
  z-index: -1;
}
@media (min-width: 860px) {
  .details.has-photo .event-card + .event-card::after { display: none; }
}
.details.has-photo .event-card__marker { background: var(--gold-400); box-shadow: 0 0 0 5px rgba(6, 30, 22, 0.55); }

.map-section.has-photo .venue-switch {
  background: rgba(6, 30, 22, 0.45);
  border-color: rgba(221, 192, 119, 0.5);
}
.map-section.has-photo .venue-switch__option { color: rgba(244, 236, 217, 0.88); }
.map-section.has-photo .venue-switch__option.is-selected {
  background: var(--gold-400);
  color: var(--emerald-900);
}
.map-section.has-photo .venue-panel__address { color: var(--cream-50); }

/* On a phone the carousel runs to the screen edge, so each chapter card is
   nearly the full width of the screen (1rem either side). */
@media (max-width: 859px) {
  .story-carousel {
    margin-left: calc(-1 * max(var(--section-pad-x), env(safe-area-inset-left)));
    margin-right: calc(-1 * max(var(--section-pad-x), env(safe-area-inset-right)));
  }
  .story-track {
    padding-left: max(1rem, env(safe-area-inset-left));
    padding-right: max(1rem, env(safe-area-inset-right));
  }
}

/* ==========================================================================
   Desktop refinements
   ========================================================================== */
@media (min-width: 860px) {
  :root { --section-pad-x: 3rem; }
  /* Still one chapter at a time, as a centred column between the arrows. */
  .story-carousel { justify-content: center; }
  .story-track { flex: 0 1 620px; min-width: 0; }
  .story-nav { display: flex; }
  .events-list { max-width: 720px; flex-direction: row; align-items: stretch; }
  .event-card { flex: 1; }
  /* The spine reads top to bottom; side by side cards do not need it. */
  .events-list::before,
  .event-card__marker { display: none; }
}

/* ==========================================================================
   Reduced motion: calm fallback (motion-spec.md "Fallbacks")
   Deliberately placed last so it wins by source order over every
   equal-specificity rule above it, regardless of where each was declared.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .shimmer-text {
    animation: none;
    background: none;
    -webkit-text-fill-color: var(--gold-300);
    color: var(--gold-300);
    text-shadow: none;
  }
  .envelope__seal { animation: none; }
  .opening-screen__cta.shimmer-text {
    -webkit-text-fill-color: var(--gold-200);
    color: var(--gold-200);
  }
  #hero-names {
    letter-spacing: 0.01em;
    transition: opacity 0.8s var(--ease-out), transform 0.8s var(--ease-out);
  }
  .hero__backdrop,
  #hero .section-bg__layer,
  #hero .section-bg__blur,
  #hero .section-bg__glow {
    animation: none;
  }
  /* Story: skip the slide-in/fade-up choreography (everything just appears)
     and drop the parallax scale buffer; setupStoryParallax() already never
     attaches its scroll listener in this mode. */
  .story-card--entrance .story-card__chapter,
  .story-card--entrance .story-card__title,
  .story-card--entrance .story-card__prose,
  .story-card--entrance .story-card__nav,
  .story-card--entrance .story-card__figure {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .section-bg--parallax { transform: none; }
  .story-track.is-nudging .story-card { animation: none; }
  .story-card__nav-arrow { animation: none; }
  .page { transition: none; }
  /* Motif fields hold still rather than drifting. */
  .motif__p,
  .motif__s {
    animation: none;
    opacity: 0.5;
  }
  .motif__p { display: none; }
  .scroll-cue__line { animation: none; opacity: 0.6; }
  .envelope__seal-half { animation: none; }
  /* Transition beat: keep the breath, drop the drifting glow. */
  .transition-beat::before { animation: none; opacity: 0.6; transform: translate(-50%, -50%); }
  /* Details: the formal wording just appears with the card, no cascade. */
  .event-card__label,
  .event-card__hosts,
  .event-card__relation,
  .event-card__datetime,
  .event-card__venue {
    opacity: 1;
    transform: none;
    transition: none;
  }
}
