/* ============================================================
   SATORIA — Wedding Website Redesign
   Design System & Styles
   ============================================================ */

/* --- Font Declarations --- */
@font-face {
  font-family: 'Palmore';
  /* woff2 subset (Latin + punctuation), 40KB — was a 212KB .otf. The OTF
     preloaded High and starved the LCP image (sky) on mobile; woff2 here +
     no preload (see index.html) keeps it off the critical first-paint path.
     The preloader gates dismissal on document.fonts.ready, so no FOUT. */
  src: url('assets/Palmore-Light.woff2') format('woff2');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

/* Quicksand — self-hosted to eliminate the fonts.googleapis.com round
   trip. The latin subset of Quicksand v37 is a single 28KB woff2 that
   contains all three weights we use (300/400/500); we declare each
   @font-face with the same src and the browser renders the right
   weight from one file (one HTTP fetch, three logical fonts). */
@font-face {
  font-family: 'Quicksand';
  src: url('assets/fonts/quicksand-latin.woff2') format('woff2');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Quicksand';
  src: url('assets/fonts/quicksand-latin.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Quicksand';
  src: url('assets/fonts/quicksand-latin.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}





/* --- Design Tokens --- */
:root {
  /* Palette (legacy — kept for backwards-compat during paper-card migration) */
  --color-box-bg: rgba(16, 7, 4, 0.95);
  --color-box-border: rgba(200, 180, 150, 0.12);
  --color-title-gold: rgba(240, 235, 229, 0.75);
  --color-text: #F0EBE5;
  --color-text-muted: rgba(240, 235, 229, 0.75);
  --color-text-muted-soft: rgba(240, 235, 229, 0.55);
  /* Two-tone page background — vertical gradient applied via body with
     background-attachment: fixed. The "fixed" backdrop trick is the
     standard mobile-web fix for "a position:fixed layer occasionally
     fails to cover the visible viewport during iOS URL-bar transitions
     or first-load layout settling, exposing a strip of body bg at the
     bottom of the screen." A single solid bg can't match both ends:
     gaps at the top should read as sky, gaps at the bottom as earth.
     The gradient handles both by colouring the body relative to the
     viewport — the gap picks up the right tone automatically.
     - Top tone (#DFD8C1) = sky horizon, matches scene-sky fallback.
     - Bottom tone (#7A6F55) = earth/foliage shadow, matches the
       painting's lower edge. */
  --color-page-bg-top: #DFD8C1;
  /* Darker / more foliage-toned to actually blend with the painting's
     bottom edge (autumn trees, dark water reflections). The previous
     #7A6F55 read as a lighter beige against the painting's deeper
     foliage colours — the gap, if visible, stood out as a "tan bar"
     rather than continuing into the painting. */
  --color-page-bg-bottom: #2E2A1B;
  /* Kept for any one-off usage; not used by body anymore. */
  --color-page-bg: #DFD8C1;

  /* Paper-card design tokens (ink-on-cream, two-color system) */
  --cream:        #FAF6EE;       /* Crisp warm cream — not A4 white, not aged */
  --ink:          #2A1F18;       /* Warm dark — never pure black */
  --ink-72:       rgba(42, 31, 24, 0.72);
  --ink-58:       rgba(42, 31, 24, 0.58);
  --ink-46:       rgba(42, 31, 24, 0.46);
  --ink-30:       rgba(42, 31, 24, 0.30);
  --ink-22:       rgba(42, 31, 24, 0.22);
  --ink-12:       rgba(42, 31, 24, 0.12);
  /* Error accent, pulled from the painting's terracotta. Used only by the
     RSVP field-level errors (visible only after a failed submit). */
  --accent-terracotta: #B0492F;

  /* Focus ring — ink, with cream offset, for visibility on the cream surface */
  --focus-ring:   0 0 0 2px var(--cream), 0 0 0 4px var(--ink);

  /* Wedding palette — rich and saturated. 4 swatches: deep midnight
     navy, olive-leaning sage, warm terracotta, earthy clay. Previous
     values were grayed and read as washed-out. */
  --swatch-navy:       #0C172E;
  --swatch-sage:       #7B8550;  /* olive-leaning sage */
  --swatch-terracotta: #B5523A;
  --swatch-clay:       #95604A;
  /* Aliases kept so legacy references compile; --swatch-olive maps to
     sage now since the two greens were combined. */
  --swatch-olive:      var(--swatch-sage);
  --swatch-dark:       #2F1B13;

  /* ──────────────────────────────────────────────────────────────
     TYPE SYSTEM — one row per visual role. If a new element needs
     text, pick the role it belongs to and use the matching token.
     Don't invent new sizes inline.

     Family rules:
       Palmore (display) for headlines, statements, item names.
       Quicksand (body)  for labels, buttons, footnotes, inputs.

     Weight rules:
       Palmore 300 only. (Italic is a style, not a weight.)
       Quicksand 300 for body + footnote.
       Quicksand 500 for labels and buttons (tracked uppercase).

     Role → token reference (mobile floor / desktop ceiling):
       L1  Chapter title             1.9rem  → 2.6rem    (Palmore 300)
       L2  Chapter statement         1.35rem → 1.6rem    (Palmore 300 italic)
       L3  Item name / body emphasis 1rem    → 1.2rem    (Palmore 300)
       L4  Italic sub                13px    → 14px      (Palmore 300 italic)
       L5  Footnote                  12px                (Quicksand 300)
       LB  Label / button            11px                (Quicksand 500, ALL CAPS)
       IN  Form input                16px                (Quicksand 300, ≥16px = no iOS zoom)
     ────────────────────────────────────────────────────────────── */
  --font-display: 'Palmore', 'Playfair Display', serif;
  --font-body: 'Quicksand', 'Montserrat', sans-serif;

  /* Legacy generic scale — kept for any incidental UI that doesn't
     map to a role above. Don't use for chapter copy. */
  --type-xs: clamp(0.6rem, 1vw, 0.7rem);
  --type-sm: clamp(0.7rem, 1.2vw, 0.85rem);
  --type-base: clamp(0.85rem, 1.5vw, 1rem);
  --type-lg: clamp(1.1rem, 2vw, 1.35rem);
  --type-xl: clamp(1.4rem, 3vw, 1.8rem);
  --type-2xl: clamp(1.8rem, 4vw, 2.4rem);

  /* Spacing (4px base grid) */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 1rem;
  --space-4: 1.5rem;
  --space-5: 2.5rem;
  --space-6: 4rem;

  /* Box geometry */
  --box-radius: 16px;
  --box-border-width: 1px;
  --box-padding: var(--space-5);

  /* Motion */
  --ease-cinema: cubic-bezier(0.25, 0.46, 0.45, 0.94);
  --ease-smooth: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
}




/* --- Reset --- */
*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Solid deep-earth to match body — iOS Safari uses html's bg-color
     for rubber-band overscroll past the viewport edges. Previously
     a gradient with background-attachment: fixed, but Chrome iOS
     doesn't honor attachment:fixed (the gradient gets scroll-attached
     and leaks the cream gradient top at the viewport bottom on first
     load). Same root cause as the body fix — see memory:
     chrome-ios-bg-attachment. */
  background-color: var(--color-page-bg-bottom);
  /* Note: overflow-x: hidden lives on <body> only (not here). Setting
     it on BOTH html and body created a scrolling-root ambiguity that
     broke vertical scroll engagement on Android Chrome. */
  /* Hide scrollbar for Firefox, IE, and Edge */
  scrollbar-width: none;
  -ms-overflow-style: none;
}

/* Hide scrollbar for Chrome, Safari, and Opera */
html::-webkit-scrollbar {
  display: none;
}

body {
  font-family: var(--font-body);
  font-weight: 300;
  color: var(--color-text);
  /* Solid deep-earth backdrop matching .scene-ground (#2E2A1B). Any
     gap in the fixed scene layers (Chrome iOS URL-bar quirks, lvh
     first-load underreport, position:fixed bottom anchoring) shows
     dark earth — which blends with the painting's lower tones and
     scene-ground — instead of cream. Previously this used a two-tone
     gradient with background-attachment: fixed, but Chrome iOS
     doesn't honor attachment:fixed reliably — the gradient was
     getting scroll-attached and showing cream (gradient top) at the
     viewport bottom on first load. Solid dark-earth body bg is the
     fail-safe; scene-sky still paints sky on top (z:1) where it
     should. See memory: chrome-ios-bg-attachment. */
  background-color: var(--color-page-bg-bottom);
  overflow-x: hidden;
  overscroll-behavior-y: none;
  /* `manipulation` allows vertical pan + pinch-zoom and only disables
     the 300ms double-tap-zoom delay. Safer than `pan-y` on Android
     Chrome, which sometimes refuses to engage scroll when the touch
     starts on a sibling fixed-position layer. */
  touch-action: manipulation;
}

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

/* Skip link reveals on focus — keyboard users get a visible jump target.
   Anchored top-left, high contrast, lives above all parallax layers. */
a.sr-only:focus,
a.sr-only:focus-visible {
  position: fixed;
  top: 1rem;
  left: 1rem;
  width: auto;
  height: auto;
  padding: 0.6rem 1rem;
  margin: 0;
  clip: auto;
  overflow: visible;
  white-space: normal;
  background: #100704;
  color: #F0EBE5;
  border: 1px solid rgba(240, 235, 229, 0.6);
  border-radius: 6px;
  font-family: var(--font-body);
  font-size: 0.8rem;
  letter-spacing: 0.05em;
  z-index: 10000;
  outline: 2px solid #F0EBE5;
  outline-offset: 2px;
}

/* Visually-hidden helper for live regions that should announce but not render */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Display utility: prefer `.is-hidden` over inline style="display:none"
   in markup so initial state is class-driven. JS that toggles via
   element.style.display still wins (inline style beats class cascade). */
.is-hidden { display: none; }

/* ============================================================
   SCENE LAYERS — Parallax painting system
   Stack: map(0) → sky(1) → hero(2) → mountains(3) → foliage(4) → tree(26) → narrator(35)
   ============================================================ */

/* Hero SVG title — static, centered, gets covered by the rising scene. */
.hero-layer {
  position: fixed;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
}

.hero-layer img {
  width: clamp(340px, 85vw, 1300px);
  height: auto;
  display: block;
}

/* ============================================================
   LAYERED SCENE — two layers (sky + scene).

   Sky is a fixed div with background-image (single asset, no
   variants); scene is a fixed div containing a <picture><img>
   that picks scene-small.webp on mobile, scene.webp on desktop.

   The scene's transform is driven directly by JS (translate3d
   per scroll frame) — previously a CSS calc with var(--progress)
   that forced a layout-aware recompute every frame.

   Intrinsic height comes from aspect-ratio + width:100%, not
   from vh, so iOS URL bar toggles can't shift the layers.

   See assets/scene/README.md for the painter brief.
   ============================================================ */
.scene-stack {
  /* Structural wrapper. No positioning / no z-index so it does NOT
     create a stacking context — sky and scene each set their own
     position:fixed + z-index and stack against the root stacking
     context. (Using `display: contents` would break the [hidden]
     attribute so we leave the wrapper as a normal-flow element with
     no dimensions of its own.) */
  display: block;
}

.scene-sky,
.scene {
  position: fixed;
  pointer-events: none;
  left: 0;
  right: 0;
}

/* will-change only on the animating scene layer — the sky is static
   (it never translates with --progress) so promoting it just costs GPU
   memory without payoff. */
.scene {
  will-change: transform;
}

.scene-sky {
  /* Static atmospheric backdrop. z:1 puts it behind the hero title
     (z:2). Height locked to 100lvh + 240px buffer — lvh for iOS URL
     bar stability, +240px for the URL-bar collapse animation.

     The sky bitmap now ships as a real <picture><img> child (was a
     CSS background-image). The reason: background-image with
     background-size:cover gets RE-RASTERIZED every time the layer's
     box changes — on Chrome iOS the bottom-toolbar collapse triggers
     a viewport resize that re-rasters the bg, exposing the bg-color
     fallback as a contrasting strip during the re-paint window. A
     real <img> is rasterized ONCE at decode time and after that the
     browser only composites/transforms it — viewport changes don't
     re-rasterize, so no flash.

     bg-color #2E2A1B is now only a defense-in-depth fallback for the
     first-paint window before the <img> decodes. */
  top: 0;
  height: calc(100lvh + 240px);
  z-index: 1;
  background-color: #2E2A1B;
}

.scene-sky > picture,
.scene-sky > picture img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center bottom;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* Mobile: portrait viewport on a landscape sky image. With bg-size:
   cover, the desktop "center bottom" anchor would crop the more
   interesting upper-sky content out of frame. Anchor TOP so the
   deeper-blue upper sky stays visible and the cropped half is the
   lower horizon — which the rising painting covers anyway.

   ALSO on mobile: scene-sky no longer extends to the viewport bottom.
   The painting takes over at 50svh and is supposed to cover everything
   below — letting scene-sky fill the entire viewport meant its sky-
   toned bg was what showed through in any bottom gap (iOS lvh quirk,
   painting positioning oddity, transient first-load state), creating
   the cream bar at the bottom of the screen. Capping scene-sky height
   to 65lvh keeps the sky image where it's needed (above the painting)
   and lets the body's earth-toned gradient backdrop show through
   anywhere lower if the painting falls short. */
@media (max-width: 767px) {
  .scene-sky {
    height: 65vh;   /* fallback for browsers without lvh */
    height: 65lvh;  /* modern */
  }
  .scene-sky > picture,
  .scene-sky > picture img {
    object-position: top center;
  }
}

/* TOP-anchored safety band for MOBILE only. On mobile, scene-sky is
   capped at 65lvh (see the @media block above for why), so the bottom
   strip of the viewport has no sky bitmap and would otherwise expose
   the body bg-color when the painting hasn't landed. scene-ground fills
   that strip with the deep-earth tone so there's no body-cream gap.
   On desktop, scene-sky already covers the full viewport with its
   bitmap, so scene-ground here would just overlay the sky as a brown
   strip — hidden via the `display: none` default below, revealed only
   in the mobile media query.

   Anchored from the TOP (not bottom:0) with overdraw past the
   largest-viewport bottom. The browser repositions bottom-anchored
   fixed elements while the dynamic toolbar animates — that mid-scroll
   repaint was the historical "solid block / seam flash" that kept
   forcing normalizeScroll back in (see script.js). Top at the sky cap
   (65lvh, minus 2px to absorb subpixel lvh rounding on fractional-DPR
   Android) + 240px overdraw (same buffer precedent as .scene-sky)
   means a ~60-110px toolbar collapse moves and exposes nothing. */
.scene-ground {
  position: fixed;
  top: calc(65vh - 2px);   /* fallback for browsers without lvh */
  top: calc(65lvh - 2px);
  left: 0;
  right: 0;
  height: calc(35vh + 242px);
  height: calc(35lvh + 242px);
  background-color: #2E2A1B;
  z-index: 2;
  pointer-events: none;
  display: none;
}
@media (max-width: 767px) {
  .scene-ground { display: block; }
}

.scene {
  /* Position calculated entirely in CSS using lvh + vw — no `100%`
     reference (which would force layout-aware resolution per scroll
     frame). lvh, NOT dvh: now that scrolling is native, the dynamic
     toolbar collapses mid-scroll, and dvh re-resolves during that
     animation — the painting would visibly shift by the toolbar delta.
     lvh is constant across toolbar states. The vw constant equals the
     painting's own height, derived from width × aspect
     (220vw × 6160/3656 = 370.677vw on mobile,
     120vw × 6160/3656 = 202.188vw on desktop).

     JS only writes --progress. Per scroll frame the browser
     evaluates a pure-viewport calc — no layout dependency, runs on
     the compositor thread.

     translate3d + backface-visibility + isolation force the .scene
     onto its own GPU compositing layer, so transform changes never
     re-rasterise the painting — they just move the cached layer.
     Without this the painting briefly disappears between frames on
     fast scroll, exposing the dark scene-ground behind as a "black
     flash". */
  top: 0;
  width: 100%;
  z-index: 3;
  aspect-ratio: 3656 / 6160;
  --hero-translate: 100vh;   /* fallback for browsers without lvh */
  --hero-translate: 100lvh;
  --landed-translate: calc(100vh - 202.188vw + 240px);
  --landed-translate: calc(100lvh - 202.188vw + 240px);
  transform: translate3d(0, calc(
    var(--hero-translate) * (1 - var(--progress, 0)) +
    var(--landed-translate) * var(--progress, 0)
  ), 0);
  will-change: transform;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  isolation: isolate;
}

.scene > picture,
.scene > picture img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center bottom;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* Scale the scene wider than the viewport so its rendered height
   exceeds the viewport, giving the rising mountain enough vertical
   travel to physically cover the hero title at a low scroll
   progress. The wider the scene element, the taller (via
   aspect-ratio: 2/3) and the more rise per scroll unit. 260vw on
   mobile + 130vw on desktop gives a silhouette-covers-title moment
   around progress 0.25 (just inside the day chapter) without an
   opacity fade. */
@media (max-width: 767px) {
  .scene {
    width: 220vw;
    left: -60vw;
    right: auto;
    /* Mobile: scene's top sits at 50svh at hero so the mountain
       ridge reads from the first frame (svh is static — safe). The
       landed-translate constant matches the mobile width × aspect:
         220vw × 6160/3656 = 370.677vw */
    --hero-translate: 50svh;
    --landed-translate: calc(100vh - 370.677vw + 240px);
    --landed-translate: calc(100lvh - 370.677vw + 240px);
  }
}

@media (min-width: 768px) {
  .scene {
    width: 120vw;
    left: -10vw;
    right: auto;
  }
}

/* No opacity hack on the hero title — the rising scene physically
   covers it via parallax, the same way the original single-painting
   did. To make this work with the new transparent-above-silhouette
   scene, the scene's display height is scaled up substantially (see
   .scene width rules below): a taller rendered scene rises through
   more vertical space per unit progress, so the silhouette overtakes
   the title position by ~25–35% scrolled (just inside the day chapter)
   instead of waiting until ~50% (attire). */

/* ============================================================
   DECORATIVE LAYERS
   Foliage (top-right) and Tree (bottom-left) — parallax
   ============================================================ */
.foliage-layer {
  position: fixed;
  /* lvh (large viewport — URL-bar-collapsed) is stable across URL bar
     toggles. Previously `top: -5%` resolved against the visible
     viewport height, which changes ~80px when the URL bar collapses,
     producing the visible "foliage drift up" the user reported. */
  top: -5lvh;
  right: -5%;
  width: clamp(400px, 50vw, 750px);
  z-index: 4;
  pointer-events: none;
  will-change: transform;
}

.foliage-layer img {
  width: 100%;
  height: auto;
  display: block;
}

.tree-layer {
  position: fixed;
  bottom: 0;
  left: -30%;
  width: clamp(620px, 70vw, 1040px);
  z-index: 26;
  pointer-events: none;
  will-change: transform;
  /* Initial transform handled by GSAP fromTo — no CSS transform here */
}


.tree-layer img {
  width: 100%;
  height: auto;
  display: block;
}



/* ============================================================
   NARRATOR BOX
   The signature dark content box — fixed center
   ============================================================ */
.narrator-box {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 25;

  /* Paper-card surface — cream with crisp 4px corners and a soft drop shadow.
     The card reads as a printed wedding invitation in the user's hand while
     the painting behind is the view through a window. No backdrop-filter:
     real paper is opaque, and the per-frame GPU blur cost on iOS was the
     #1 cause of scroll jank. */
  background: var(--cream);
  color: var(--ink);
  border: 0;
  border-radius: 4px;
  box-shadow:
    0 22px 60px -28px rgba(0, 0, 0, 0.55),
    0 4px 14px -6px rgba(0, 0, 0, 0.35),
    0 0 0 1px rgba(42, 31, 24, 0.05);

  /* Always 100% opacity. Hidden via clip-path until ScrollTrigger reveals. */
  opacity: 1;
  visibility: hidden;
  clip-path: inset(50% 50% 50% 50% round 2px);

  /* will-change: clip-path was held permanently for a one-time bloom-in;
     removed to free the GPU layer once the box is revealed. Browsers
     heuristically promote during the active transition without the hint. */
  overflow: hidden;

  /* Width/height morph between chapters via direct CSS transitions on
     inline-style assignments from script.js. 0.35s feels snappy. */
  transition:
    width 0.35s var(--ease-cinema),
    height 0.35s var(--ease-cinema);

  /* Bound the per-frame reflow during the width/height morph to this
     subtree. Without it, animating a layout property on a fixed element
     can make the browser re-check layout against the other fixed scene
     layers each frame; `contain: layout` scopes it to the box. (Only
     `layout`, not `paint` — `paint` would clip the card's drop shadow.) */
  contain: layout;
}

/* Scrollable mode — only for long-form chapters like RSVP, plus mobile Guide.
   Slightly more prominent scrollbar so touch users can SEE the box is scrollable
   (it was nearly invisible at 3px / rgba 0.25). */
.narrator-box--scrollable {
  overflow-y: auto;
  overflow-x: hidden;
  max-height: 85vh;
  max-height: 85dvh;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y;
  /* Chain vertical overscroll to the page: once the box's inner scroll
     bottoms out, continued swipes advance the document instead of dead-
     stopping under the finger (the page felt "stuck" whenever a gesture
     started on a bottomed-out box). Pull-to-refresh stays suppressed by
     body's overscroll-behavior-y: none. Safe only under NATIVE scrolling —
     do not combine with normalizeScroll's allowNestedScroll. */
  overscroll-behavior-x: contain;
  overscroll-behavior-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--ink-30) transparent;
}

.narrator-box--scrollable::-webkit-scrollbar {
  width: 5px;
}

.narrator-box--scrollable::-webkit-scrollbar-track {
  background: transparent;
}

.narrator-box--scrollable::-webkit-scrollbar-thumb {
  background: var(--ink-30);
  border-radius: 6px;
}

/* Soft bottom fade — cream on cream — signals more content below. */
.narrator-box--scrollable::after {
  content: '';
  position: sticky;
  bottom: 0;
  left: 0;
  right: 0;
  height: 36px;
  margin-top: -36px;
  background: linear-gradient(to bottom, rgba(250, 246, 238, 0), rgba(250, 246, 238, 0.95));
  pointer-events: none;
  z-index: 1;
  display: block;
}

/* ============================================================
   CHAPTERS
   Each content section inside the narrator box.
   All chapters are absolutely positioned and stacked.
   Only the active one has opacity:1, the rest are hidden.
   We use a dummy sizer to make the box grow to fit the
   LARGEST chapter, preventing layout jumps.
   ============================================================ */
/* Chapters fade in/out via opacity, gated by an `.is-active` class that
   script.js toggles. The browser handles interruption (rapid scroll
   through chapters) gracefully — no defensive cleanup needed.
   Sequential timing: outgoing fades out for 0.18s with NO delay; incoming
   waits 0.18s then fades in for 0.2s. visibility flips at the right edge
   of each transition so only one chapter is interactive at a time and
   accessibility tree stays clean. */
.chapter {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  padding: var(--space-5) var(--space-5) calc(var(--space-5) + var(--space-5)) var(--space-5);
  display: flex;
  flex-direction: column;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.18s ease-out, visibility 0s linear 0.18s;
}

.chapter.is-active {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.2s ease-in 0.18s, visibility 0s linear 0s;
}

/* --- Emotional Bridge --- */
.chapter-verse {
  font-family: var(--font-display);
  font-weight: 300;
  font-style: italic;
  font-size: var(--type-base);
  color: var(--color-text-muted);
  text-align: center;
  letter-spacing: 0.04em;
  line-height: 1.6;
  margin-bottom: var(--space-4);
}

/* --- Global Chapter Title — paper-card style --- */
.chapter-title {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(1.9rem, 4.2vw, 2.6rem);
  color: var(--ink);
  text-align: center;
  text-transform: none;
  letter-spacing: 0.05em;
  line-height: 1.1;
  padding: 0;
  margin: 0;
}

/* Short hairline rule beneath every chapter title — stationery convention. */
.chapter-title::after {
  content: '';
  display: block;
  width: 36px;
  height: 1px;
  background: var(--ink-30);
  margin: 18px auto 18px;
}

/* --- Chapter 1: The Day --- paper-card layout */
.chapter-date {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  color: var(--ink-58);
  text-align: center;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  margin: 0 0 6px;
}

.day-countdown {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  font-size: clamp(13px, 1.4vw, 15px);
  color: var(--ink-58);
  text-align: center;
  letter-spacing: 0.02em;
  line-height: 1.4;
  margin: 0 0 20px;
}

.timeline {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
}

/* Compact 3-column rows on mobile — label / venue+address / time on one line
   so the whole Day card fits within 82vh without internal scrolling. */
.timeline-event {
  display: grid;
  /* Label column fits the widest label ("Late Night") on ONE line — at 78px it
     wrapped, making that row taller and breaking the row-to-row alignment. */
  grid-template-columns: 90px 1fr auto;
  align-items: center;
  column-gap: 14px;
  padding: 14px 0;
  border-top: 1px solid var(--ink-12);
  text-align: left;
}

.timeline-event:last-child {
  border-bottom: 1px solid var(--ink-12);
}

.event-label {
  display: block;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  color: var(--ink-58);
  text-transform: uppercase;
  /* 0.16em matches the other 11px inline labels (party-role,
     circle-label, guide-count .l). The earlier 0.14em was the odd
     one out in the tracked-uppercase label tier. */
  letter-spacing: 0.16em;
  margin-bottom: 0;
  line-height: 1.3;
  white-space: nowrap;   /* keep "Late Night" on one line */
}

/* The venue + address sit inside a single clickable link to Google Maps. */
.venue-link {
  display: block;
  color: inherit;
  text-decoration: none;
  transition: opacity 0.2s;
}
.venue-link:hover { opacity: 0.65; }
.venue-link:hover .event-venue {
  text-decoration: underline;
  text-decoration-color: var(--ink-30);
  text-underline-offset: 4px;
}

.event-venue {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(1rem, 1.6vw, 1.2rem);
  color: var(--ink);
  letter-spacing: 0.02em;
  line-height: 1.2;
  margin: 0;
}

.event-address {
  display: block;
  margin-top: 2px;
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  font-size: clamp(12.5px, 1.1vw, 13.5px);
  color: var(--ink-58);
  letter-spacing: 0.02em;
  line-height: 1.35;
}

.event-time {
  display: block;
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  font-size: clamp(13.5px, 1.3vw, 16px);
  color: var(--ink-72);
  letter-spacing: 0.02em;
  white-space: nowrap;
}

/* --- Chapter 2: The Family --- */
.parents-block {
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin: 0;
}

.parent-line {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(1.05rem, 1.8vw, 1.3rem);
  color: var(--ink);
  line-height: 1.45;
  letter-spacing: 0.02em;
  margin: 0;
}

/* Short hairline separator between parents pair and the party grid. */
.family-sep {
  display: block;
  width: 48px;
  height: 1px;
  background: var(--ink-58);
  /* Was 44px each side (88px lineage-to-grid, the biggest gap on the site).
     30px tightens it toward the 40px section rhythm while the divider keeps
     room to read as a deliberate separator. */
  margin: 30px auto;
  border: 0;
}

.party-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 40px;
  row-gap: 30px;
  text-align: center;
  /* width:100% — the chapter is a centered flex column, which was shrink-
     wrapping the grid to ~272px (its content width) and leaving big empty
     margins on a wide desktop box. Fill the column up to max-width so the two
     columns spread and the block reads balanced. */
  width: 100%;
  max-width: 480px;
  margin: 0 auto;
  border-top: 0;
  padding-top: 0;
}

.party-member {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.party-role {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  color: var(--ink-58);
  text-transform: uppercase;
  letter-spacing: 0.16em;
}

.party-name {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(1.05rem, 1.6vw, 1.25rem);
  color: var(--ink);
  letter-spacing: 0.02em;
  line-height: 1.25;
}

/* --- Chapter 3: Attire — paper-card layout --- */
.dress-code {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  /* Statement tier: matches .rsvp-prompt and .moments-line. Each
     chapter's single short "statement" line uses this same size so
     the hierarchy reads identically across Attire/Gallery/RSVP. */
  font-size: clamp(1.35rem, 2.2vw, 1.6rem);
  color: var(--ink);
  text-align: center;
  letter-spacing: 0.02em;
  line-height: 1.25;
  margin: 0;
}

.palette-row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
  max-width: 480px;
  margin: 40px auto 0;   /* section rhythm: 40px standard (was 48, an outlier) */
  justify-content: center;
}

.palette-circle {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  /* Without min-width: 0, grid items default to min-content sizing, so the
     "TERRACOTTA" cell would grow to fit the longest unbreakable label and
     push the other swatches out of alignment. Forcing min-width: 0 keeps
     all four columns strictly equal regardless of label length. */
  min-width: 0;
}

.circle-swatch {
  width: 100%;
  max-width: 84px;
  aspect-ratio: 1;
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--ink-12);
}

/* Swatch colours pull straight from the :root tokens — single source
   of truth so the displayed Attire swatches always match wherever
   else the palette is used (Family chapter accents, Guide legend, etc). */
.circle-swatch.swatch-navy       { background-color: var(--swatch-navy); }
.circle-swatch.swatch-sage       { background-color: var(--swatch-sage); }
.circle-swatch.swatch-terracotta { background-color: var(--swatch-terracotta); }
.circle-swatch.swatch-clay       { background-color: var(--swatch-clay); }
/* Olive alias kept for any legacy markup. */
.circle-swatch.swatch-olive      { background-color: var(--swatch-olive); }

.circle-label {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  color: var(--ink-58);
  text-transform: uppercase;
  letter-spacing: 0.16em;
}

.weather-line {
  font-family: var(--font-body);
  /* 300 matches the other Quicksand body text on the site. The
     previous 400 was the only mid-weight Quicksand in use and read
     heavier than its footnote role wanted. */
  font-weight: 300;
  font-size: 12px;
  color: var(--ink-58);
  text-align: center;
  letter-spacing: 0.02em;
  line-height: 1.5;
  margin: 40px 0 0;   /* section rhythm: match the 40px standard */
  padding-top: 22px;
  border-top: 1px solid var(--ink-12);
}

/* ============================================================
   TINY SCROLL ARROW
   ============================================================ */
.scroll-hint-arrow {
  position: fixed;
  bottom: clamp(20px, 4vh, 36px);
  left: 50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  transform: translateX(-50%);
  animation: gentleDrift 2.5s infinite var(--ease-cinema);
  opacity: 0.7;
  z-index: 30;
  will-change: transform, opacity;
  transition: opacity 0.4s ease;
}

/* The arrow exists to teach first-time visitors to scroll. Once they have,
   it stops being useful and turns into visual noise inside every chapter. */
body.has-scrolled .scroll-hint-arrow {
  opacity: 0;
  pointer-events: none;
}

/* Vertical stem */
.scroll-hint-arrow::before {
  content: '';
  display: block;
  width: 1px;
  height: 18px;
  background: rgba(240, 235, 229, 0.55);
}

/* Chevron */
.scroll-hint-arrow::after {
  content: '';
  display: block;
  width: 6px;
  height: 6px;
  border-right: 1px solid rgba(240, 235, 229, 0.55);
  border-bottom: 1px solid rgba(240, 235, 229, 0.55);
  transform: translateY(-4px) rotate(45deg);
}

@keyframes gentleDrift {

  0%,
  100% {
    transform: translate(-50%, 0);
  }

  50% {
    transform: translate(-50%, 6px);
  }
}

/* ============================================================
   CHAPTER NAVIGATION
   Vertical dot column on the right side. Labels reveal on hover
   (desktop) or focus. Hidden on small phones to free up the edge.
   ============================================================ */
.chapter-nav {
  position: fixed;
  right: 1.4rem;
  top: 50%;
  transform: translateY(-50%);
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 0.5rem;
  pointer-events: auto;
}

.chapter-nav-dot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.7rem;
  background: none;
  border: 0;
  padding: 4px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.chapter-nav-dot::after {
  content: '';
  display: block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(240, 235, 229, 0.4);
  box-shadow: 0 0 0 1px rgba(8, 5, 3, 0.4);
  transition: background 0.2s ease, transform 0.2s ease;
}

.chapter-nav-dot:hover::after,
.chapter-nav-dot:focus-visible::after,
.chapter-nav-dot.is-active::after {
  background: rgba(220, 180, 140, 1);
  transform: scale(1.4);
}

.chapter-nav-label {
  font-family: var(--font-body);
  font-size: 0.65rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(240, 235, 229, 0.92);
  background: rgba(16, 7, 4, 0.85);
  border: 1px solid rgba(220, 180, 140, 0.25);
  padding: 0.25rem 0.6rem;
  border-radius: 999px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transform: translateX(6px);
  transition: opacity 0.2s ease, transform 0.2s ease;
}

@media (hover: hover) {
  .chapter-nav-dot:hover .chapter-nav-label {
    opacity: 1;
    transform: translateX(0);
  }
}

.chapter-nav-dot:focus-visible .chapter-nav-label {
  opacity: 1;
  transform: translateX(0);
}

@media (max-width: 767px) {
  /* Mobile: lay the nav out horizontally along the bottom-center of the
     viewport. The right-edge vertical column was too easy to mis-tap, too
     small to read, and lacked any indication of which dot was which
     chapter. A horizontal pill near the thumb-rest zone is more familiar
     (story dots, onboarding indicators) and the active chapter gets a
     visible label so the user always knows where they are. */
  .chapter-nav {
    right: auto;
    top: auto;
    left: 50%;
    bottom: calc(env(safe-area-inset-bottom, 0px) + 14px);
    transform: translateX(-50%);
    flex-direction: row;
    gap: 0;
    padding: 6px 4px 0 4px;
    background: rgba(16, 7, 4, 0.88);
    border-radius: 999px;
    border: 1px solid rgba(220, 180, 140, 0.18);
  }
  /* Each dot gets a generous 44×44 tap target (WCAG AA) while the visible
     ::after dot itself stays small (6×6). The bar is bottom-anchored so
     the slightly taller pill still sits in the thumb-rest zone. */
  .chapter-nav-dot {
    width: 44px;
    height: 44px;
    padding: 0;
    justify-content: center;
    flex-direction: column;
  }
  .chapter-nav-dot::after {
    width: 6px;
    height: 6px;
  }
  /* Active chapter label: shown ABOVE the active dot only. The label is
     re-positioned absolutely so its width doesn't compress neighbouring
     dots when text changes. */
  .chapter-nav-label {
    display: block;
    position: absolute;
    bottom: calc(100% + 4px);
    left: 50%;
    transform: translateX(-50%);
    opacity: 0;
    transition: opacity 0.2s ease;
    font-size: 0.7rem;
    letter-spacing: 0.18em;
    padding: 0.25rem 0.6rem;
    background: rgba(16, 7, 4, 0.78);
    border: 1px solid rgba(220, 180, 140, 0.22);
    pointer-events: none;
  }
  .chapter-nav-dot.is-active .chapter-nav-label {
    opacity: 1;
    transform: translateX(-50%);
  }
}

/* Hide nav while the gallery is fullscreen — its own close button is the
   only chrome we want visible there. */
.gallery-section.is-active ~ .chapter-nav,
body:has(.gallery-section.is-active) .chapter-nav {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}

/* ============================================================
   RSVP CHAPTER
   Editorial form — dark glass aesthetic
   ============================================================ */

/* ============================================================
   R.S.V.P. — paper-card styling
   ============================================================ */

.rsvp-deadline {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  color: var(--ink-58);
  text-align: center;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  margin: 0 0 40px;
}

.rsvp-prompt {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  font-size: clamp(1.35rem, 2.4vw, 1.8rem);
  color: var(--ink);
  text-align: center;
  letter-spacing: 0.02em;
  line-height: 1.25;
  margin: 0 auto 40px;
  max-width: 22ch;
}

.rsvp-gate-buttons {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 360px;
  margin: 0 auto;
}

.rsvp-gate-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 56px;
  padding: 16px 28px;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.25s ease, color 0.25s ease;
}

/* Accept = primary: filled ink, high-contrast — clearly recommended. */
.rsvp-gate-btn.rsvp-accept {
  background: var(--ink);
  border: 1px solid var(--ink);
  color: var(--cream);
}
.rsvp-gate-btn.rsvp-accept:hover {
  background: transparent;
  color: var(--ink);
}

/* Decline = secondary: ghost outline. */
.rsvp-gate-btn.rsvp-decline {
  background: transparent;
  border: 1px solid var(--ink);
  color: var(--ink);
}
.rsvp-gate-btn.rsvp-decline:hover {
  background: var(--ink);
  color: var(--cream);
}

/* --- Form Shared --- */
.rsvp-field {
  display: flex;
  flex-direction: column;
  gap: 8px;
  /* Field rhythm: 20px between every field AND fieldset (was 22 here, 16 on
     fieldsets — an uneven 22/16/16 cadence down the form). */
  margin-bottom: 20px;
}

.rsvp-label {
  display: block;
  width: 100%;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--ink-58);
  /* Top-aligned, left-aligned labels — the research-backed standard form
     pattern (one left scan-line down labels + inputs). */
  text-align: left;
  margin-bottom: 0;
}

.rsvp-required {
  color: var(--ink);
}

.rsvp-optional {
  font-weight: 400;
  font-style: italic;
  text-transform: none;
  letter-spacing: 0.04em;
  font-size: 11px;
  color: var(--ink-46);
}

.rsvp-input {
  width: 100%;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--ink-22);
  padding: 12px 0;
  min-height: 44px;
  font-family: var(--font-body);
  /* 16px floor avoids iOS Safari's auto-zoom-on-focus (any input <16px
     triggers a viewport zoom on tap, which feels broken on this site). */
  font-size: 16px;
  color: var(--ink);
  outline: none;
  transition: border-color 0.2s ease;
}

.rsvp-input::placeholder {
  color: var(--ink-46);
}

.rsvp-input:focus {
  border-bottom-color: var(--ink);
}

.rsvp-textarea {
  width: 100%;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--ink-22);
  padding: 12px 0;
  min-height: 80px;
  font-family: var(--font-body);
  font-size: 16px;
  color: var(--ink);
  outline: none;
  resize: vertical;
  transition: border-color 0.2s ease;
}

.rsvp-textarea::placeholder {
  color: var(--ink-46);
}

.rsvp-textarea:focus {
  border-bottom-color: var(--ink);
}

/* --- Back arrow button --- */
.rsvp-back-btn {
  position: absolute;
  top: 20px;
  left: 20px;
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  padding: 0;
  font-family: var(--font-body);
  font-size: 18px;
  color: var(--ink-58);
  cursor: pointer;
  z-index: 3;
  transition: color 0.2s ease;
}

.rsvp-back-btn:hover,
.rsvp-back-btn:focus-visible {
  color: var(--ink);
}

/* --- Notice (dietary buffet note) --- */
.rsvp-notice {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  padding: 16px 18px;
  background: rgba(42, 31, 24, 0.04);
  border-radius: 4px;
  margin: 0 0 32px;
}

.rsvp-notice-icon {
  flex: 0 0 22px;
  width: 22px;
  height: 22px;
  border: 1px solid var(--ink-46);
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 13px;
  color: var(--ink-58);
  line-height: 1;
}

.rsvp-notice p {
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--ink-72);
  line-height: 1.55;
  margin: 0;
}

/* --- Guest blocks — clean hairlines, no fill --- */
.rsvp-guest-card {
  border: 0;
  background: transparent;
  padding: 30px 0 4px;
  margin: 0;
  border-top: 1px solid var(--ink-12);
  transition: none;
}

.rsvp-guest-card:first-of-type {
  border-top: 0;
  padding-top: 2px;
}

.rsvp-guest-card:hover { background: transparent; }

.rsvp-guest-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 18px;
  padding-bottom: 0;
  border-bottom: 0;
}

.rsvp-guest-number {
  /* JS writes spelled-out "Guest One" (the old "01" numerals rendered as
     letterforms in this face). */
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  font-size: clamp(1.1rem, 1.5vw, 1.3rem);
  color: var(--ink);
  letter-spacing: 0.02em;
  text-transform: none;
}

.rsvp-remove-guest {
  background: none;
  border: 0;
  padding: 12px 4px;
  min-height: 44px;
  color: var(--ink-46);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  cursor: pointer;
  text-decoration: none;
  transition: color 0.2s ease;
}

.rsvp-remove-guest:hover {
  color: var(--ink);
}

/* --- Event chips — pill style --- */
.rsvp-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 12px;
}

.rsvp-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 40px;
  padding: 10px 16px;
  border: 1px solid var(--ink-22);
  border-radius: 999px;
  background: transparent;
  color: var(--ink-58);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

.rsvp-chip.active,
.rsvp-chip[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--cream);
}

.rsvp-chip:hover:not(.active):not([aria-pressed="true"]) {
  border-color: var(--ink);
  color: var(--ink);
}

.rsvp-chip:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* --- Checkboxes — ink box, ink dot when checked --- */
.rsvp-checks {
  display: grid;
  /* Left-aligned, content-sized columns: the two columns sit together at the
     left with a fixed gap, instead of the old 1fr 1fr that stretched to the
     full width and left a wide "river" down the middle. */
  grid-template-columns: repeat(2, max-content);
  justify-content: start;
  gap: 6px 48px;
  margin-top: 12px;
}

.rsvp-check {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 44px;
  padding: 4px 0;
  cursor: pointer;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 12.5px;
  text-transform: none;
  letter-spacing: 0.02em;
  color: var(--ink);
  user-select: none;
}

.rsvp-checkbox {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  border: 1px solid var(--ink-46);
  background: transparent;
  border-radius: 2px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: all 0.2s ease;
  cursor: pointer;
  margin: 0;
  padding: 0;
  position: relative;
}

.rsvp-checkbox:checked,
.rsvp-checkbox.checked {
  background: var(--ink);
  border-color: var(--ink);
}

.rsvp-checkbox:checked::after,
.rsvp-checkbox.checked::after {
  content: '';
  width: 8px;
  height: 8px;
  background: var(--cream);
  border-radius: 1px;
  display: block;
}

.rsvp-checkbox:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* --- Add Guest — quiet left-aligned action link --- */
.rsvp-add-guest {
  display: block;
  width: 100%;
  text-align: left;
  margin: 8px 0 28px;
  padding: 12px 0;
  min-height: 44px;
  background: transparent;
  border: 0;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--ink-58);
  cursor: pointer;
  transition: color 0.2s ease;
}

.rsvp-add-guest:hover {
  color: var(--ink);
}

/* --- Contact Section --- */
.rsvp-contact {
  padding-top: 32px;
  border-top: 1px solid var(--ink-12);
  margin-top: 8px;
  margin-bottom: 32px;
}

.rsvp-section-label {
  display: block;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--ink-58);
  text-align: left;
  margin: 0 0 20px;
}

.rsvp-contact-grid {
  /* Single column — standard single-column form flow. Each field on its own
     row keeps the label/input scan-line unambiguous. (.rsvp-field already
     carries the 20px bottom rhythm.) */
  display: flex;
  flex-direction: column;
}

/* --- Submit pill --- */
.rsvp-submit-btn {
  /* Full-width primary action — the modern single-column form standard, and it
     matches the form's full-width inputs. (The old inline-flex + margin:auto
     couldn't center and left it stuck at the left edge.) */
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 52px;
  padding: 16px 32px;
  background: var(--ink);
  border: 1px solid var(--ink);
  border-radius: 999px;
  color: var(--cream);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  cursor: pointer;
  transition: background 0.25s ease, color 0.25s ease;
  margin: 32px 0 0;
}

.rsvp-submit-btn:hover {
  background: transparent;
  color: var(--ink);
}

.rsvp-submit-btn:active {
  transform: scale(0.98);
}

.rsvp-validation-msg {
  text-align: center;
  margin-top: 18px;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink);
}

/* --- Field-level errors (appear only on a failed submit) --- */
.rsvp-input.has-error,
.rsvp-textarea.has-error {
  border-color: var(--accent-terracotta);
}

.rsvp-field-error {
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--accent-terracotta);
  margin: 6px 0 0;
  line-height: 1.4;
}

/* --- RSVP view crossfade (gate <-> forms <-> success) ---
   Same beat as the chapter fade: 0.18s out, 0.2s in. The old display
   toggle was a hard cut on the site's most important click. */
.rsvp-view {
  transition: opacity 0.2s ease-in;
}
.rsvp-view.is-leaving {
  opacity: 0;
  transition: opacity 0.18s ease-out;
}
.rsvp-view.is-entering {
  opacity: 0;
  transition: none;
}
@media (prefers-reduced-motion: reduce) {
  .rsvp-view, .rsvp-view.is-leaving { transition: none; }
}

/* --- Success State --- */
.rsvp-success {
  text-align: center;
  padding: 8px 0;
}

.rsvp-success-icon {
  display: none;   /* The ❦ ornament conflicts with the quiet stationery feel. */
}

.rsvp-success-title {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  font-size: clamp(1.35rem, 2.4vw, 1.8rem);
  color: var(--ink);
  letter-spacing: 0.02em;
  line-height: 1.25;
  margin: 0 auto 32px;
  max-width: 22ch;
}

.rsvp-success-msg {
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--ink);
  line-height: 1.6;
  margin: 0 auto 12px;
  max-width: 38ch;
}

.rsvp-success-msg #rsvp-confirm-email {
  font-weight: 500;
  color: var(--ink);
}

.rsvp-confirm-note {
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--ink-58);
  letter-spacing: 0.02em;
  line-height: 1.5;
  margin: 0 auto;
  max-width: 38ch;
}

.rsvp-success-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
  margin-top: 32px;
}

.rsvp-success-actions a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 14px 32px;
  border: 1px solid var(--ink);
  border-radius: 999px;
  background: transparent;
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  text-decoration: none;
  transition: background 0.25s ease, color 0.25s ease;
}

.rsvp-success-actions a:hover {
  background: var(--ink);
  color: var(--cream);
}

/* --- Fieldset/legend reset for RSVP groups --- */
.rsvp-fieldset {
  border: 0;
  padding: 0;
  margin: 0 0 20px 0;   /* match .rsvp-field for an even field cadence */
}

.rsvp-fieldset > legend {
  padding: 0;
}

/* --- 5-guest cap notice --- */
.rsvp-cap-message {
  display: none;
  text-align: center;
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--ink-58);
  letter-spacing: 0.02em;
  padding: 12px 16px;
  border: 1px dashed var(--ink-22);
  border-radius: 4px;
  margin: 16px 0;
}

.rsvp-cap-message.is-visible {
  display: block;
}

/* --- Decline form acknowledgment line (italic Palmore intro) --- */
.rsvp-decline-note {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  font-size: clamp(1.05rem, 1.6vw, 1.3rem);
  color: var(--ink);
  line-height: 1.5;
  letter-spacing: 0.02em;
  text-align: center;
  margin: 0 auto 40px;
  max-width: 34ch;
}

/* --- Paper-card primary pill button (used by Day + others) --- */
.day-actions {
  display: flex;
  justify-content: center;
  margin-top: 20px;
}

.day-add-to-cal {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 48px;
  padding: 14px 32px;
  background: var(--ink);
  color: var(--cream);
  border: 1px solid var(--ink);
  border-radius: 999px;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  text-decoration: none;
  transition: background 0.25s ease, color 0.25s ease;
}

.day-add-to-cal:hover {
  background: transparent;
  color: var(--ink);
}
.day-add-to-cal:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* Short viewports — compact The Day so the whole card (through the Add to
   Calendar button) fits inside the 82vh narrator cap WITHOUT internal
   scrolling. The internal-scroll fallback hides the button below a
   cream-on-cream fade, which reads as "the card just ends" on overlay-
   scrollbar systems (macOS) — the marquee CTA must never depend on it.
   Only #ch-day is compacted; other chapters keep their spacing. */
@media (max-height: 700px) {
  #ch-day.chapter {
    padding-top: calc(var(--space-5) * 0.7);
    padding-bottom: calc(var(--space-5) * 1.3);
  }
  #ch-day .chapter-title {
    font-size: clamp(1.6rem, 3.4vw, 2.1rem);
  }
  #ch-day .chapter-title::after {
    margin: 12px auto 12px;
  }
  #ch-day .day-countdown {
    margin-bottom: 12px;
  }
  #ch-day .timeline-event {
    padding: 9px 0;
  }
  #ch-day .day-actions {
    margin-top: 14px;
  }
  #ch-day .day-add-to-cal {
    min-height: 44px;
    padding: 11px 28px;
  }
}

/* --- Family chapter warmth line --- */
.family-intro {
  font-family: var(--font-display);
  font-weight: 300;
  font-style: italic;
  font-size: var(--type-base);
  color: var(--color-text);
  opacity: 0.9;
  line-height: 1.7;
  text-align: center;
  margin-bottom: var(--space-4);
}

/* --- Guide: action chips on cards (Directions, Book) --- */
.guide-card-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.55rem;
  margin-top: 0.7rem;
}

.guide-card-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4em;
  min-height: 44px;
  min-width: 44px;
  padding: 0.5rem 0.9rem;
  font-family: var(--font-body);
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--color-text);
  text-decoration: none;
  border: 1px solid rgba(220, 180, 140, 0.4);
  border-radius: 999px;
  background: transparent;
  transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease;
  -webkit-tap-highlight-color: transparent;
}

/* CSS-drawn NE arrow — pure border trick, never gets auto-converted into the
   iOS link emoji like the U+2197 character does. */
.guide-card-action::after {
  content: '';
  display: inline-block;
  width: 0.45em;
  height: 0.45em;
  border-top: 1px solid currentColor;
  border-right: 1px solid currentColor;
  transform: rotate(45deg) translateY(-0.05em);
}

.guide-card-action:hover,
.guide-card-action:focus-visible {
  background: rgba(220, 180, 140, 0.12);
  border-color: rgba(220, 180, 140, 0.7);
  color: #fff;
}

.guide-card-action--book {
  color: rgba(220, 180, 140, 0.95);
}

/* --- Mobile RSVP --- */
@media (max-width: 767px) {
  .rsvp-gate-buttons {
    grid-template-columns: 1fr;
  }

  .rsvp-checks {
    /* Single left-aligned column on phones. */
    grid-template-columns: 1fr;
    gap: 4px 0;
  }

  .rsvp-chips {
    flex-direction: column;
  }

  .rsvp-chip {
    text-align: center;
  }
}


/* ============================================================
   INFINITE GALLERY
   Draggable tile grid — covers full viewport
   ============================================================ */
.gallery-section {
  position: fixed;
  inset: 0;
  z-index: 50; /* Full takeover */
  background: #080503;
  visibility: hidden;
  clip-path: inset(100% 0 0 0);
  transition: clip-path 1.2s var(--ease-cinema), visibility 1.2s var(--ease-cinema);
}

.gallery-section.is-active {
  clip-path: inset(0% 0 0 0);
  visibility: visible;
}

/* --- Chapter 7: The Guide (teaser card) --- */
.guide-place {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  color: var(--ink-58);
  text-align: center;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  margin: 0 0 32px;   /* +8px counts margin = 40px section rhythm */
}

.guide-counts {
  display: flex;
  justify-content: center;
  gap: 48px;
  margin: 8px auto 0;
  padding: 24px 0;
  border-top: 1px solid var(--ink-12);
  border-bottom: 1px solid var(--ink-12);
  max-width: 460px;
}

.guide-count {
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: center;
}

.guide-count .n {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(1.8rem, 3vw, 2.5rem);
  color: var(--ink);
  letter-spacing: 0.02em;
  line-height: 1;
}

.guide-count .l {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  color: var(--ink-58);
  text-transform: uppercase;
  letter-spacing: 0.16em;
}

.guide-open-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: center;
  min-height: 48px;
  padding: 14px 32px;
  background: var(--ink);
  border: 1px solid var(--ink);
  border-radius: 999px;
  color: var(--cream);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  cursor: pointer;
  transition: background 0.25s ease, color 0.25s ease;
  margin: 36px auto 0;
}

.guide-open-btn:hover {
  background: transparent;
  color: var(--ink);
}

.guide-open-btn:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* Tighter mobile gap for the three counts row. */
@media (max-width: 480px) {
  .guide-counts {
    gap: 32px;
  }
}


/* --- Chapter 5: Gifts — registry teaser --- */
.gifts-intro {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  font-size: clamp(1.05rem, 1.6vw, 1.3rem);
  color: var(--ink);
  text-align: center;
  letter-spacing: 0.02em;
  line-height: 1.5;
  margin: 0 auto 40px;
  max-width: 34ch;
}

.registry-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 440px;
  margin: 0 auto;
}

.registry-link {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 16px;
  padding: 20px 24px;
  min-height: 64px;
  background: transparent;
  border: 1px solid var(--ink-22);
  border-radius: 4px;
  color: var(--ink);
  text-decoration: none;
  transition: border-color 0.25s ease, background 0.25s ease;
}
.registry-link:hover {
  border-color: var(--ink);
  background: rgba(42, 31, 24, 0.03);
}
.registry-text {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.registry-name {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(1.05rem, 1.5vw, 1.25rem);
  color: var(--ink);
  letter-spacing: 0.02em;
}
.registry-desc {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  /* 13px matches .day-countdown and the italic-sub tier. */
  font-size: clamp(13px, 1.1vw, 14px);
  color: var(--ink-58);
  letter-spacing: 0.02em;
}
.registry-arrow {
  font-family: var(--font-body);
  font-size: 18px;
  color: var(--ink-46);
  transition: color 0.25s ease, transform 0.25s ease;
}
.registry-link:hover .registry-arrow {
  color: var(--ink);
  transform: translateX(2px);
}

.gifts-footnote {
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--ink-58);
  text-align: center;
  letter-spacing: 0.02em;
  line-height: 1.5;
  margin: 36px auto 0;
  max-width: 36ch;
}

/* Our Moments single italic line */
.moments-line {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  /* Statement tier: matches .dress-code and .rsvp-prompt. */
  font-size: clamp(1.35rem, 2.2vw, 1.6rem);
  color: var(--ink);
  text-align: center;
  letter-spacing: 0.02em;
  line-height: 1.25;
  margin: 0 auto;
  max-width: 28ch;
}

/* Open Gallery — ghost pill on the cream card. */
.gallery-open-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: center;
  min-height: 48px;
  background: transparent;
  border: 1px solid var(--ink);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  font-size: 11px;
  padding: 14px 32px;
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.25s ease, color 0.25s ease;
  margin-top: 36px;
}

.gallery-open-btn:hover {
  background: var(--ink);
  color: var(--cream);
}

.gallery-open-btn:active {
  transform: scale(0.97);
}

/* Gallery overlay close button stays dark since it's over the gallery, not the cream card. */
/* Gallery close — circular icon button, solid cream pill over the dark gallery. */
.gallery-close-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  min-height: 48px;
  background: var(--cream);
  border: 0;
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 26px;
  line-height: 1;
  padding: 0;
  border-radius: 999px;
  cursor: pointer;
  box-shadow:
    0 8px 24px -8px rgba(0, 0, 0, 0.55),
    0 2px 6px -2px rgba(0, 0, 0, 0.35);
  transition: transform 0.2s ease, background 0.2s ease;
}

.gallery-close-btn:hover {
  background: #FFF;
  transform: scale(1.05);
}

.gallery-close-btn:active {
  transform: scale(0.95);
}

/* Universal focus ring — ink on cream surfaces. */
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

.gallery-close-btn {
  position: absolute;
  top: max(2rem, env(safe-area-inset-top, 0px));
  right: max(2rem, env(safe-area-inset-right, 0px));
  z-index: 60;
}

.gallery-canvas {
  position: absolute;
  inset: 0;
  cursor: grab;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

/* Individual tile — absolutely positioned, GSAP handles transform.
   `contain: layout paint style` isolates each tile from cascading
   layout/paint work as the canvas pans (without this, every paint
   walks the full ~400-tile tree).
   `content-visibility: auto` was added previously for further savings
   but caused black flashes on fast pan: it defers rendering of the
   contents until the tile is "relevant", and during rapid pan the tile
   enters the viewport before paint completes — for 1-2 frames the
   tile's own (transparent) background showed the dark canvas through.
   `contain` alone gives most of the perf benefit without that pitfall.
   Background set to match `.tile-frame` so the brief pre-paint moment
   reads as cream paper, not a dark hole. */
.gallery-tile {
  position: absolute;
  top: 0;
  left: 0;
  border-radius: 6px;
  overflow: hidden;
  transition: box-shadow 0.4s var(--ease-cinema);
  contain: layout paint style;
  background-color: #efe4cf;
  /* will-change: transform was here on every tile — with ~400 tiles in the
     9-copy layout, mobile GPUs (256-layer budget on iPhone) thrashed layers
     and produced visible jitter during pan. The wrapper has its own
     will-change; individual tiles don't need to be promoted unless they
     are actively animating, which only happens on hover (see below). */
}

@media (hover: hover) {
  /* Promote to a compositor layer only when the hover animation is
     imminent. Browsers GC the layer when the hover ends. */
  .gallery-photo-tile:hover {
    will-change: transform;
  }
}

/* --- Photo tiles (film-frame style) --- */
.gallery-photo-tile {
  cursor: pointer;
}

.gallery-photo-tile {
  box-shadow:
    0 4px 12px rgba(40, 25, 12, 0.35),
    0 1px 0 rgba(255, 240, 220, 0.03) inset;
  transition: transform 0.45s var(--ease-cinema), box-shadow 0.45s var(--ease-cinema);
}

/* `@media (hover: hover)` guards these against touch devices where `:hover`
   fires briefly on tap during pan — without the guard, every tap-and-drag
   triggered a box-shadow + scale transition on the tile, causing paint
   bombs at the worst possible time (during the gesture). */
@media (hover: hover) {
  .gallery-photo-tile:hover {
    /* Scale the WHOLE tile — cream-paper border, image, and shadow grow
       together rather than the image zooming inside a fixed frame.
       z-index lift keeps the grown tile from being clipped by neighbours. */
    transform: scale(1.04);
    z-index: 10;
    box-shadow:
      0 16px 40px rgba(40, 25, 12, 0.55),
      0 0 0 1px rgba(255, 240, 220, 0.08) inset;
  }
}

/* Cream paper tile — visible while the photo is decoding; shows as a 2px
   sepia frame around the photo once loaded. */
.tile-frame {
  width: 100%;
  height: 100%;
  background-color: #efe4cf;   /* aged paper cream */
  border-radius: 6px;
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.45s var(--ease-cinema);
}

/* Image inside the tile — the cream background acts as a sepia border via
   3px padding around the image. */
.gallery-photo-tile .tile-frame img {
  width: calc(100% - 6px);
  height: calc(100% - 6px);
  object-fit: cover;
  pointer-events: none;
  display: block;
  opacity: 0;
  /* Short fade only for genuinely-new decodes. A long fade during a pan reads
     as a cream "flash" as the tile resolves, so this is snappy. */
  transition: opacity 0.28s ease-out;
  border-radius: 3px;
}

.gallery-photo-tile.is-loaded .tile-frame img {
  opacity: 1;
}

/* Cache hits (already-decoded tiles entering view during a pan) show with NO
   fade at all — the fade itself was the visible flash. JS adds is-instant
   alongside is-loaded when the image is complete at attach time. */
.gallery-photo-tile.is-instant .tile-frame img {
  transition: none;
}

/* --- Center title tile — matches narrator box with radial glow --- */
.gallery-title-tile {
  background:
    radial-gradient(ellipse at 50% 40%, rgba(200, 160, 90, 0.06) 0%, transparent 70%),
    var(--color-box-bg);
  border: 1px solid rgba(240, 220, 180, 0.12);
  border-radius: var(--box-radius);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  box-shadow:
    0 16px 60px rgba(0, 0, 0, 0.4),
    inset 0 1px 0 rgba(240, 220, 180, 0.05);
}

.gallery-title-tile h2 {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: var(--type-2xl);
  color: var(--color-text);
  text-transform: uppercase;
  letter-spacing: 0.15em;
}

/* Decorative separator under gallery title */
.gallery-title-tile h2::after {
  content: '';
  display: block;
  width: 40px;
  height: 1px;
  background: rgba(240, 235, 229, 0.3);
  margin: var(--space-2) auto 0;
}

.gallery-title-tile p {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: var(--type-xs);
  color: var(--color-text-muted);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* ============================================================
   LIGHTBOX
   Full-screen photo viewer overlay
   ============================================================ */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: #050302;
  display: flex;
  align-items: center;
  justify-content: center;
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
  /* Opacity fade (not clip-path) so the full-rect backdrop covers from frame 1.
     The previous clip-path expansion left surrounding gallery tiles visible
     during the transition window. */
  transition: opacity 0.35s var(--ease-cinema), visibility 0s linear 0.35s;
}

.lightbox.is-open {
  visibility: visible;
  opacity: 1;
  pointer-events: auto;
  transition: opacity 0.35s var(--ease-cinema), visibility 0s linear 0s;
}

.lightbox-img {
  max-width: 90vw;
  max-height: 90vh;
  object-fit: contain;
  border-radius: 8px;
  box-shadow: 0 20px 80px rgba(0, 0, 0, 0.6);
}

.lightbox-close {
  position: absolute;
  top: max(clamp(1rem, 3vh, 2rem), env(safe-area-inset-top, 0px));
  right: max(clamp(1rem, 3vw, 2rem), env(safe-area-inset-right, 0px));
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  background: none;
  border: none;
  color: var(--color-text);
  font-size: 2rem;
  cursor: pointer;
  opacity: 0.6;
  transition: opacity 0.3s ease;
  line-height: 1;
  padding: var(--space-2);
}

.lightbox-close:hover {
  opacity: 1;
}


/* spacer-gallery height overrides are at end of file, after base .spacer */

/* Gallery drag hint */
.gallery-drag-hint {
  position: absolute;
  bottom: 8%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 10;
  opacity: 0;
  pointer-events: none;
  will-change: opacity;
}

.gallery-drag-hint span {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: var(--type-xs);
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.15em;
  background: var(--color-box-bg);
  border: 1px solid var(--color-box-border);
  padding: var(--space-2) var(--space-3);
  border-radius: 20px;
}

/* Gallery active cursor state */
.gallery-canvas.is-dragging {
  cursor: grabbing;
}

/* ============================================================
   THE GUIDE — Fullscreen Takeover
   Activated by the "Open Guide" CTA on the Guide teaser card.
   Cream surface · ink text · map on the right (desktop) / top
   (mobile) · panel with tabs + scrollable list on the other side.
   ============================================================ */

.guide-takeover {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: var(--cream);
  color: var(--ink);
  display: flex;
  flex-direction: column;
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  transition: opacity 0.32s var(--ease-cinema), transform 0.32s var(--ease-cinema);
}

.guide-takeover.is-open {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

/* Top bar */
.guide-takeover-bar {
  display: grid;
  grid-template-columns: 56px 1fr 56px;
  align-items: center;
  gap: 16px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--ink-12);
  background: var(--cream);
  z-index: 2;
  flex-shrink: 0;
}

.guide-takeover-close {
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  color: var(--ink-58);
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 20px;
  transition: color 0.2s ease;
}

.guide-takeover-close:hover { color: var(--ink); }
.guide-takeover-close:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: 2px;
}

.guide-takeover-meta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  text-align: center;
}

.guide-takeover-title {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(1.05rem, 1.5vw, 1.2rem);
  color: var(--ink);
  letter-spacing: 0.06em;
  line-height: 1.1;
}

.guide-takeover-place {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  font-size: 12.5px;
  color: var(--ink-58);
  letter-spacing: 0.02em;
}

/* Body — desktop: panel left, map right. Mobile: map top, panel bottom. */
.guide-takeover-body {
  display: grid;
  grid-template-columns: 400px 1fr;
  flex: 1;
  min-height: 0;
}

@media (max-width: 767px) {
  .guide-takeover-body {
    grid-template-columns: 1fr;
    grid-template-rows: 42% 1fr;
  }
}

/* Panel */
.guide-panel {
  background: var(--cream);
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--ink-12);
  min-height: 0;
}
@media (max-width: 767px) {
  .guide-panel {
    border-right: 0;
    border-top: 1px solid var(--ink-12);
    order: 2;
  }
}

.guide-panel-tabs {
  display: flex;
  gap: 28px;
  padding: 18px 28px 0;
  position: relative;
  flex-shrink: 0;
}
@media (max-width: 767px) {
  .guide-panel-tabs {
    padding: 14px 20px 0;
    gap: 22px;
  }
}
.guide-panel-tabs::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--ink-12);
}
.guide-panel-tab {
  background: transparent;
  border: 0;
  padding: 0 0 16px;
  min-height: 44px;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  color: var(--ink-46);
  text-transform: uppercase;
  letter-spacing: 0.18em;
  cursor: pointer;
  position: relative;
  transition: color 0.2s ease;
}
.guide-panel-tab:hover { color: var(--ink); }
.guide-panel-tab.is-on { color: var(--ink); }
.guide-panel-tab.is-on::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--ink);
  z-index: 1;
}
.guide-panel-tab:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: 2px;
}

/* Scrollable list */
.guide-panel-list {
  list-style: none;
  padding: 0;
  margin: 0;
  flex: 1;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--ink-22) transparent;
}
.guide-panel-list::-webkit-scrollbar { width: 6px; }
.guide-panel-list::-webkit-scrollbar-thumb { background: var(--ink-22); border-radius: 6px; }

.guide-panel-item {
  display: grid;
  grid-template-columns: 32px 1fr;
  gap: 14px;
  padding: 20px 28px;
  border-bottom: 1px solid var(--ink-12);
  cursor: pointer;
  transition: background 0.2s ease;
}
.guide-panel-item:hover { background: rgba(42, 31, 24, 0.035); }
.guide-panel-item.is-active { background: rgba(42, 31, 24, 0.06); }
@media (max-width: 767px) {
  .guide-panel-item {
    padding: 18px 20px;
    grid-template-columns: 28px 1fr;
  }
}

.guide-panel-num {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  color: var(--ink-46);
  font-size: 17px;
  line-height: 1.2;
  padding-top: 2px;
}
.guide-panel-item.is-active .guide-panel-num { color: var(--ink); }

.guide-panel-content {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.guide-panel-name {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(1.05rem, 1.4vw, 1.25rem);
  color: var(--ink);
  letter-spacing: 0.02em;
  line-height: 1.25;
}

.guide-panel-desc {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  font-size: 13px;
  color: var(--ink-58);
  letter-spacing: 0.02em;
  line-height: 1.45;
}

.guide-panel-addr {
  font-family: var(--font-body);
  font-size: 11px;
  color: var(--ink-46);
  letter-spacing: 0.06em;
  margin-top: 6px;
}

.guide-panel-directions {
  display: inline-flex;
  align-self: flex-start;
  min-height: 36px;
  padding: 8px 0;
  margin-top: 6px;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  color: var(--ink);
  text-transform: uppercase;
  letter-spacing: 0.16em;
  text-decoration: none;
  border-bottom: 1px solid var(--ink-30);
  transition: border-color 0.2s ease;
}
.guide-panel-directions:hover { border-color: var(--ink); }

/* Map area */
.guide-takeover-map {
  position: relative;
  background: #EFE9DC;
  min-height: 0;
}
@media (max-width: 767px) {
  .guide-takeover-map { order: 1; }
}

.guide-takeover-map .guide-map {
  position: absolute;
  inset: 0;
}

.guide-takeover-map .map-gesture-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(42, 31, 24, 0.12);
  color: var(--cream);
  font-family: var(--font-body);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s;
}

/* Centered map info card — replaces Leaflet popup so it never gets
   clipped by the takeover top bar. Sits in the middle of the visible
   map area regardless of pin position. */
.guide-info-card {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) translateY(8px);
  width: min(240px, calc(100% - 32px));
  /* Leaflet's tile/pane z-indexes go up to ~700 — this needs to clear them. */
  z-index: 1000;
  background: var(--cream);
  color: var(--ink);
  border-radius: 4px;
  box-shadow:
    0 20px 50px -16px rgba(0, 0, 0, 0.5),
    0 4px 12px -4px rgba(0, 0, 0, 0.25),
    0 0 0 1px rgba(42, 31, 24, 0.08);
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.22s var(--ease-cinema), transform 0.22s var(--ease-cinema);
}

.guide-info-card.is-visible {
  opacity: 1;
  transform: translate(-50%, -50%);
  pointer-events: auto;
}

.guide-info-close {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: rgba(250, 246, 238, 0.92);
  border: 0;
  border-radius: 999px;
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  z-index: 2;
  transition: background 0.2s ease;
}
.guide-info-close:hover { background: var(--cream); }

.guide-info-photo {
  width: 100%;
  aspect-ratio: 16 / 10;
  background: var(--ink-12);
  overflow: hidden;
}
.guide-info-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.guide-info-body {
  padding: 14px 16px 16px;
}

.guide-info-meta {
  display: block;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  color: var(--ink-58);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  margin: 0 0 6px;
}

.guide-info-name {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: 17px;
  color: var(--ink);
  letter-spacing: 0.02em;
  margin: 0 0 6px;
  line-height: 1.2;
}

.guide-info-desc {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  font-size: 13px;
  color: var(--ink-72);
  margin: 0 0 10px;
  line-height: 1.4;
}

.guide-info-directions {
  display: inline-flex;
  align-items: center;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  color: var(--ink);
  text-transform: uppercase;
  letter-spacing: 0.16em;
  text-decoration: none;
  padding: 6px 0;
  border-bottom: 1px solid var(--ink-30);
  transition: border-color 0.2s ease;
}
.guide-info-directions:hover { border-color: var(--ink); }

/* ── Mobile: compact bottom-anchored strip so the map pin stays visible.
      Photo moves to a small thumbnail on the left; copy + directions to
      the right. Card no longer covers the venue location. ── */
@media (max-width: 767px) {
  .guide-info-card {
    top: auto;
    left: 12px;
    right: 12px;
    bottom: 12px;
    width: auto;
    transform: translateY(8px);
    display: grid;
    grid-template-columns: 86px 1fr;
    align-items: stretch;
  }
  .guide-info-card.is-visible {
    transform: translateY(0);
  }
  .guide-info-photo {
    aspect-ratio: auto;
    width: 86px;
    height: 100%;
    min-height: 86px;
  }
  .guide-info-body {
    padding: 10px 12px 12px;
    /* leave room for the close button */
    padding-right: 38px;
  }
  .guide-info-meta {
    font-size: 10px;
    margin: 0 0 3px;
    letter-spacing: 0.12em;
  }
  .guide-info-name {
    font-size: 16px;
    margin: 0 0 2px;
  }
  .guide-info-desc {
    font-size: 12px;
    margin: 0 0 6px;
    /* clamp to one line so the strip stays compact regardless of copy length */
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .guide-info-directions {
    font-size: 10px;
    padding: 4px 0;
  }
  .guide-info-close {
    width: 28px;
    height: 28px;
    font-size: 16px;
    top: 4px;
    right: 4px;
  }
}

/* Legacy popup styling — kept for any cascade, but the centered info-card
   above replaces popups for the takeover. */

.guide-popup .leaflet-popup-content-wrapper {
  background: var(--cream);
  border-radius: 4px;
  box-shadow: 0 8px 32px -10px rgba(0,0,0,0.4);
  padding: 0;
}
.guide-popup .leaflet-popup-content {
  margin: 0;
  width: auto !important;
}
.guide-popup .leaflet-popup-tip {
  background: var(--cream);
}
.popup-card {
  width: 240px;
  color: var(--ink);
  font-family: var(--font-body);
}
.popup-body {
  padding: 14px 16px 16px;
}
.popup-meta {
  display: block;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  color: var(--ink-58);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  margin-bottom: 6px;
}
.popup-name {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: 18px;
  color: var(--ink);
  letter-spacing: 0.02em;
  margin: 0 0 6px;
}
.popup-desc {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  font-size: 13px;
  color: var(--ink-72);
  margin: 0 0 10px;
  line-height: 1.4;
}
.popup-directions {
  display: inline-flex;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 11px;
  color: var(--ink);
  text-transform: uppercase;
  letter-spacing: 0.16em;
  text-decoration: none;
  padding: 6px 0;
  border-bottom: 1px solid var(--ink-30);
}
.popup-directions:hover { border-color: var(--ink); }

/* (The legacy .map-bg-section and bottom-sheet drag-handle blocks were
   deleted — their trigger element never shipped, so none of it could
   execute. The fullscreen takeover is the guide on every viewport.) */

.guide-header h2 {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: var(--type-2xl);
  color: var(--color-text);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  line-height: 1;
}

.guide-subtitle {
  font-family: var(--font-body);
  font-size: var(--type-xs);
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  margin-top: var(--space-2);
  display: block;
  text-align: center;
}

/* --- Legend --- */
.guide-legend {
  display: flex;
  gap: var(--space-3);
  justify-content: center;
  margin-top: var(--space-3);
  margin-bottom: var(--space-4);
}

.legend-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
}

.legend-item span:last-child {
  font-family: var(--font-body);
  font-size: var(--type-xs);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--color-text-muted);
}

.legend-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 2px solid rgba(240, 235, 229, 0.6);
}

.legend-dot--stay {
  background: var(--swatch-navy);
}

.legend-dot--eat {
  background: var(--swatch-clay);
}

.legend-dot--leisure {
  background: var(--swatch-olive);
}

/* --- Tabs --- */
.guide-tabs {
  display: flex;
  justify-content: center;
  padding: var(--space-3) 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  background: var(--color-box-bg);
  position: sticky;
  top: 0;
  z-index: 10;
}

.guide-tab {
  flex: 1;
  min-height: 44px;
  padding: 0.75rem 0;
  font-family: var(--font-body);
  font-weight: 300;
  font-size: var(--type-xs);
  letter-spacing: 0.15em;
  text-transform: uppercase;
  text-align: center;
  color: var(--color-text-muted);
  background: none;
  border: none;
  cursor: pointer;
  position: relative;
  transition: color 0.3s var(--ease-cinema);
}

.guide-tab:hover {
  color: var(--color-text);
}

.guide-tab.active {
  color: var(--color-text);
}

.guide-tab:active {
  transform: scale(0.97);
}

.guide-tab.active::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 1px;
}

.guide-tab[data-tab="stay"].active::after {
  background: var(--swatch-navy);
}

.guide-tab[data-tab="eat"].active::after {
  background: var(--swatch-clay);
}

.guide-tab[data-tab="leisure"].active::after {
  background: var(--swatch-olive);
}

/* --- List (scrollable container for cards) --- */
.guide-list {
  flex: 1;
  overflow-y: auto;
  scrollbar-width: none;
  max-height: 360px;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  padding: 0.4rem 0.2rem 0.6rem;
}

.guide-list::-webkit-scrollbar {
  display: none;
}

/* --- Card: photo + body, hovers + active state --- */
.guide-card {
  display: grid;
  grid-template-columns: 88px 1fr;
  gap: 0.85rem;
  padding: 0.65rem;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.05);
  background: rgba(255, 255, 255, 0.015);
  cursor: pointer;
  transition: background 0.25s var(--ease-cinema),
              border-color 0.25s var(--ease-cinema),
              transform 0.25s var(--ease-cinema);
  -webkit-tap-highlight-color: transparent;
}

.guide-card:hover {
  background: rgba(255, 255, 255, 0.04);
  border-color: rgba(255, 255, 255, 0.1);
}

.guide-card.is-active {
  background: rgba(220, 180, 140, 0.08);
  border-color: rgba(220, 180, 140, 0.4);
}

.guide-card-photo {
  width: 88px;
  height: 88px;
  border-radius: 6px;
  overflow: hidden;
  background: linear-gradient(135deg, rgba(220, 180, 140, 0.08), rgba(118, 75, 62, 0.08));
  flex-shrink: 0;
}

.guide-card-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.4s var(--ease-cinema);
}

.guide-card:hover .guide-card-photo img,
.guide-card.is-active .guide-card-photo img {
  transform: scale(1.06);
}

.guide-card-body {
  min-width: 0; /* allow text truncation in grid child */
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.guide-card-meta {
  display: flex;
  align-items: baseline;
  gap: 0.55rem;
  margin-bottom: 0.15rem;
}

.guide-card-sub {
  font-family: var(--font-display);
  font-size: var(--type-sm);
  font-style: italic;
  color: var(--swatch-clay);
}

.guide-card-price {
  font-family: var(--font-body);
  font-size: var(--type-xs);
  color: rgba(240, 235, 229, 0.45);
  letter-spacing: 0.08em;
}

.guide-card-name {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: var(--type-lg);
  color: var(--color-text);
  letter-spacing: 0.02em;
  margin: 0;
  line-height: 1.15;
}

.guide-card-desc {
  font-family: var(--font-body);
  font-size: 0.78rem;
  color: var(--color-text-muted);
  margin: 0.35rem 0 0;
  line-height: 1.45;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* --- View All Button ---
   Was `position: sticky; bottom: 0` which made the button pin to the bottom
   of the scrollable list and visually overlap whichever item happened to
   scroll behind it (visible in the prior mobile screenshots). Static layout
   so it sits cleanly below the last list item. */
.guide-view-all {
  display: none;
  width: 100%;
  padding: 1rem;
  text-align: center;
  font-family: var(--font-body);
  font-weight: 300;
  font-size: var(--type-xs);
  text-transform: uppercase;
  letter-spacing: 0.15em;
  color: var(--color-text-muted);
  background: transparent;
  border: none;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  cursor: pointer;
  transition: background 0.3s, color 0.3s;
}

.guide-view-all:hover {
  background: rgba(255, 255, 255, 0.05);
  color: var(--color-text);
}

.guide-view-all:active {
  transform: scale(0.97);
}

/* --- Map Container --- */
/* Kept for nested map layout consistency */
.guide-map-wrapper {
  width: 100%;
  height: 100%;
  position: relative;
  overflow: hidden;
}

.guide-map {
  width: 100%;
  height: 100%;
  background: #EDE5D4;
}

/* Shown only if Leaflet/tiles fail to load — the place list below stays fully
   usable, so this just explains the empty map area instead of a blank box. */
.guide-map-fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: var(--space-4, 1.5rem);
  box-sizing: border-box;
  text-align: center;
  font-family: var(--font-body);
  font-weight: 300;
  font-size: var(--type-sm);
  line-height: 1.5;
  color: var(--color-text-muted);
}

/* No colour wash on the tiles. Every global tint we tried (heavy sepia, then a
   light one) read as a film laid over the whole map — most obvious over the
   large water areas on the EAT view. CARTO Voyager's native palette is already
   a soft warm beige that sits fine next to the cream chrome, and its water
   reads as natural light blue, so the tiles are left untouched: clean, natural,
   legible. (The perf-tier block below also forces none, so this just makes the
   default match it.) */
#guide-map .leaflet-tile-pane {
  filter: none;
}

/* Override Leaflet popup */
.leaflet-popup-content-wrapper {
  background: var(--color-box-bg) !important;
  border: 1px solid var(--color-box-border) !important;
  border-radius: var(--box-radius) !important;
  padding: 0 !important;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4) !important;
  overflow: hidden;
}

.leaflet-popup-content {
  margin: 0 !important;
  width: 240px !important;
}

.leaflet-popup-tip {
  background: var(--color-box-bg) !important;
  border: 1px solid var(--color-box-border) !important;
}

.leaflet-popup-close-button {
  color: var(--color-text-muted) !important;
  font-size: 16px !important;
  top: 8px !important;
  right: 8px !important;
}

/* Popup card content */
.popup-photo {
  height: 100px;
  background: rgba(255, 255, 255, 0.03);
  overflow: hidden;
}

.popup-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---- Guide photo: real image layered over a designed category tile ----
   Every venue photo area (info card + map popup) renders a fallback tile
   underneath. A real photo, when present, covers it; if the photo is missing
   or 404s (onerror removes the <img>), the tile shows through. So the guide
   never shows a blank box or a broken-image icon, only an intentional card. */
.guide-info-photo,
.popup-photo { position: relative; }

.guide-photo-real {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.guide-photo-fallback {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 9px;
  /* Warm paper ground, softly category-tinted below. */
  background: linear-gradient(158deg, #F3ECE0, #EBE1D2);
}
.guide-photo-fallback[data-cat="stay"] {
  background:
    radial-gradient(125% 92% at 50% 16%, rgba(123, 133, 80, 0.16), transparent 60%),
    linear-gradient(158deg, #F2EDDF, #E6E3CD);
}
.guide-photo-fallback[data-cat="eat"] {
  background:
    radial-gradient(125% 92% at 50% 16%, rgba(149, 96, 74, 0.17), transparent 60%),
    linear-gradient(158deg, #F4ECE2, #EBDCCD);
}
.guide-photo-fallback[data-cat="leisure"] {
  background:
    radial-gradient(125% 92% at 50% 16%, rgba(181, 82, 58, 0.13), transparent 60%),
    linear-gradient(158deg, #F4EBDF, #EEDDCC);
}
.guide-photo-fallback__rule {
  width: 24px;
  height: 1px;
  background: var(--ink-30);
}
.guide-photo-fallback__label {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1rem, 2.6vw, 1.3rem);
  line-height: 1.1;
  color: var(--ink-58);
  text-align: center;
  padding: 0 14px;
}
/* The map popup photo is shorter (100px) — scale the tile to suit. */
.popup-photo .guide-photo-fallback { gap: 6px; }
.popup-photo .guide-photo-fallback__rule { width: 18px; }
.popup-photo .guide-photo-fallback__label { font-size: 0.95rem; }

.popup-body {
  padding: 12px 16px;
}

.popup-meta {
  font-family: var(--font-body);
  font-size: var(--type-xs);
  color: var(--swatch-clay);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  display: block;
  margin-bottom: var(--space-1);
}

.popup-name {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: var(--type-lg);
  color: var(--color-text);
  letter-spacing: 0.02em;
  line-height: 1.2;
}

.popup-desc {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: var(--type-xs);
  color: var(--color-text-muted);
  margin-top: 0.3rem;
  line-height: 1.4;
}

/* Map tint overlay */
.map-tint {
  position: absolute;
  inset: 0;
  z-index: 400;
  background: rgba(212, 201, 184, 0.08);
  pointer-events: none;
  mix-blend-mode: multiply;
}

/* Coordinates badge */
.map-coords {
  position: absolute;
  top: 1.5rem;
  right: 1.5rem;
  z-index: 500;
  font-family: var(--font-body);
  font-size: var(--type-xs);
  color: var(--color-text-muted);
  background: var(--color-box-bg);
  border: 1px solid var(--color-box-border);
  padding: var(--space-2) var(--space-3);
  letter-spacing: 0.08em;
  pointer-events: none;
  border-radius: 4px;
}

/* Gesture overlay (mobile) */
.map-gesture-overlay {
  position: absolute;
  inset: 0;
  z-index: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.4);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s;
}

.map-gesture-overlay.visible {
  opacity: 1;
}

.map-gesture-overlay span {
  background: var(--color-box-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: var(--type-xs);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-box-border);
}

/* SVG teardrop pins — replace the dim 10px brown dots. */
/* MapLibre marker element (native GL map). The library sets the positioning
   transform on this element; the pin SVG lives inside and is what we scale. */
.guide-marker {
  background: none;
  border: none;
  cursor: pointer;
}

/* Pin interaction — hover and selected (active) lift the pin from its tip so
   the chosen venue reads as distinct and the pin is never the hidden element. */
.guide-marker svg {
  display: block;
  transition: transform 0.2s var(--ease-out-expo, ease-out);
  transform-origin: 50% 100%;
}
.guide-marker:hover svg { transform: scale(1.12); }
/* List-row hover highlights the matching pin (map↔list sync, desktop). */
.guide-marker.is-hover { z-index: 900; }
.guide-marker.is-hover svg { transform: scale(1.12); }
.guide-marker.is-active { z-index: 1000 !important; }
.guide-marker.is-active svg { transform: scale(1.22); }

/* Leaflet zoom controls — restyle to match the editorial palette */
.leaflet-control-zoom {
  border: 1px solid var(--color-box-border) !important;
  border-radius: 6px !important;
  overflow: hidden;
  box-shadow: 0 4px 16px rgba(8, 5, 3, 0.35) !important;
  margin: 1rem !important;
}

.leaflet-control-zoom a {
  background: var(--color-box-bg) !important;
  color: var(--color-text) !important;
  font-family: var(--font-body) !important;
  font-weight: 300;
  font-size: 1.1rem;
  width: 44px !important;
  height: 44px !important;
  line-height: 44px !important;
  border-bottom: 1px solid rgba(240, 235, 229, 0.08) !important;
}

.leaflet-control-zoom a:last-child {
  border-bottom: none !important;
}

.leaflet-control-zoom a:hover {
  background: rgba(28, 18, 12, 0.95) !important;
  color: rgba(220, 180, 140, 1) !important;
}

/* Attribution: match site typography */
.leaflet-control-attribution {
  background: rgba(16, 7, 4, 0.7) !important;
  color: rgba(240, 235, 229, 0.7) !important;
  font-family: var(--font-body) !important;
  font-size: 0.55rem !important;
  letter-spacing: 0.05em !important;
  padding: 2px 6px !important;
}

.leaflet-control-attribution a {
  color: rgba(220, 180, 140, 0.95) !important;
}

/* ============================================================
   SCROLL TRACK
   Invisible spacers that drive ScrollTrigger
   ============================================================ */
.scroll-track {
  position: relative;
  z-index: 0;
  /* Previously had `pointer-events: none` so touches passed through to
     the fixed-position UI (narrator-box, etc) above. But on Android
     Chrome that pointer-events:none chain (scroll-track → scene → sky,
     all none) left no element to initiate document scroll on — the
     gesture silently failed. The spacers inside hold no interactive
     content; letting them be touchable doesn't intercept anything,
     since narrator-box at z:25 sits above scroll-track's z:0 for hit
     testing. */
}

.spacer {
  /* lvh = "large viewport height" — the size the viewport WOULD be with the
     mobile URL bar hidden. Unlike dvh, lvh is constant: the URL bar
     collapsing or expanding during scroll does NOT resize the spacer.
     This is critical: dvh-sized spacers shrink ~80px each when the URL
     bar appears, so 7 spacers shift ~560px of scroll distance mid-scroll,
     which makes ScrollTrigger recompute every parallax trigger and the
     painting snap to a new position. Using lvh (with vh fallback for
     older browsers — vh also doesn't change on iOS) keeps the total
     scrollable height fixed. */
  height: 150vh;
  height: 150lvh;
  position: relative;
}

/* ============================================================
   REDUCED MOTION
   ============================================================ */
@media (prefers-reduced-motion: reduce) {

  .scene-sky,
  .scene,
  .scene img,
  .foliage-layer,
  .tree-layer,
  .title-layer,
  .narrator-box,
  .chapter {
    will-change: auto !important;
    transition: none !important;
    animation: none !important;
  }

  .narrator-box {
    position: relative;
    transform: none;
    opacity: 1;
    visibility: visible;
    margin: 2rem auto;
  }

  .chapter {
    position: relative;
    opacity: 1;
    visibility: visible;
    margin-bottom: 2rem;
  }

  .scroll-track {
    display: none;
  }

  .foliage-layer,
  .tree-layer {
    position: absolute;
  }
}

/* ============================================================
   RESPONSIVE
   ============================================================ */

/* --- Tablet (portrait iPads, small laptops) --- */
@media (max-width: 1024px) {
  .foliage-layer {
    width: clamp(280px, 40vw, 500px);
  }

  .tree-layer {
    width: clamp(350px, 50vw, 600px);
  }

  .guide-panel {
    width: 360px;
  }
}

/* --- Mobile + Tablet portrait --- */
@media (max-width: 767px) {

  /* ── Hero title: bigger on mobile. SVG is vector — scales crisp.
        92vw on 375 phone = 345px (was 292). 96vw on 320 = 307.
        Push the title higher on the viewport instead of dead-center so
        the painting can rise into the lower 50% of viewport unimpeded
        and the title sits comfortably in the sky region above. ── */
  .hero-layer {
    align-items: flex-start;
    padding-top: 22dvh;
  }

  .hero-layer img {
    width: clamp(280px, 92vw, 420px);
  }

  /* ── Narrator box: stays centered ── */
  .narrator-box {
    top: 50%;
    bottom: auto;
    transform: translate(-50%, -50%);
  }

  /* (The mobile bottom-sheet guide pattern — ~120 lines of peek/half/full
     sheet states — was deleted: its activating class was never applied
     because the trigger element never shipped. The fullscreen takeover is
     the mobile guide.) */

  /* ── Decorative layers: framing elements at the corners. Tree sits in
        front of narrator box (z:26) for foreground depth; foliage frames
        from the top. No opacity play — scene elements are physical, not
        overlays. ── */
  .foliage-layer {
    width: clamp(480px, 135vw, 760px);
    top: -10%;
    right: -22%;
  }

  .tree-layer {
    width: clamp(320px, 78vw, 460px);
    bottom: -2%;
    left: auto;
    right: 72vw;
    z-index: 26;
  }

  .roles-row {
    grid-template-columns: repeat(2, 1fr);
    gap: 0.8rem;
  }

  .attire-layout {
    grid-template-columns: 1fr;
    gap: 1.5rem;
  }

  .family-columns {
    gap: 1rem;
  }

  /* ── Guide: stacked layout ── */
  .guide-section {
    flex-direction: column;
  }

  .guide-panel {
    width: 100%;
    height: auto;
    position: relative;
    order: 2;
    border-right: none;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
  }

  .guide-map-wrapper {
    order: 1;
    height: 45dvh;
    flex-shrink: 0;
  }

  .guide-header {
    flex-direction: column;
    gap: 1rem;
  }

  .guide-legend {
    flex-direction: row;
  }
}

/* --- Small phones (≤480px) --- */
@media (max-width: 480px) {

  .hero-layer img {
    width: clamp(260px, 96vw, 360px);
  }

  /* Scene elements scaled for narrow viewports. No opacity play. */
  .foliage-layer {
    width: clamp(360px, 120vw, 520px);
  }

  .tree-layer {
    width: clamp(280px, 72vw, 380px);
  }

  .family-columns {
    grid-template-columns: 1fr;
  }

  .roles-row {
    grid-template-columns: 1fr 1fr;
  }

  /* Parent line size matches .party-name (the wedding-party names
     below) so the two blocks read as the same hierarchy tier. The
     line wraps to two rows at this size on 320–375 viewports;
     text-wrap: balance keeps the wrap at a word boundary instead
     of mid-name ("Lisa\nBergen"). */
  .parent-line {
    font-size: clamp(1.05rem, 1.6vw, 1.25rem);
    line-height: 1.45;
    text-wrap: balance;
  }

  /* Tighter padding inside the box on small phones — saves horizontal space
     for the chapter content. */
  .chapter {
    padding: var(--space-4) var(--space-3) calc(var(--space-4) + var(--space-3)) var(--space-3);
  }

  /* Mobile palette: 4-col grid stays, gap tightens, swatches sized for narrow viewports. */
  .palette-row {
    gap: 14px;
    max-width: 300px;
  }

  .circle-swatch {
    max-width: 60px;
  }

  /* Tighten label letter-spacing on mobile so the longest label
     ("TERRACOTTA") fits within its column without bleeding into neighbours. */
  .circle-label {
    font-size: 10px;
    letter-spacing: 0.08em;
  }
}

/* --- Very small phones (≤360px), graceful fallback --- */
@media (max-width: 360px) {

  .foliage-layer,
  .tree-layer {
    display: none;
  }

  .hero-layer img {
    /* On a 320 device — push the title further. SVG scales crisp. */
    width: min(98vw, 320px);
  }

  .party-grid {
    gap: var(--space-3);
  }

  /* Box doesn't push to the viewport edges on the smallest phones. */
  .chapter {
    padding: var(--space-3) var(--space-3) calc(var(--space-3) + var(--space-3)) var(--space-3);
  }

  .day-actions {
    margin-top: var(--space-2);
  }
}

/* ============================================================
   ROTATE HINT
   Phones held in landscape get a "rotate to portrait" prompt
   rather than a broken layout. iPads in landscape (≥501 tall)
   are excluded — they read fine in widescreen.
   ============================================================ */
.rotate-hint {
  display: none;
}

.rotate-hint.is-dismissed {
  display: none !important;
}

@media (max-width: 900px) and (max-height: 500px) and (orientation: landscape) {
  .rotate-hint {
    display: flex;
    position: fixed;
    inset: 0;
    z-index: 9999;
    background: rgba(8, 5, 3, 0.92);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    padding: var(--space-4);
    pointer-events: auto;
  }

  .rotate-hint p {
    font-family: var(--font-display);
    font-weight: 300;
    font-style: italic;
    font-size: clamp(1rem, 3.5vw, 1.4rem);
    color: var(--color-text);
    text-align: center;
    line-height: 1.5;
    letter-spacing: 0.03em;
    max-width: 28em;
  }

  .rotate-hint-dismiss {
    background: transparent;
    border: 1px solid rgba(240, 235, 229, 0.35);
    color: var(--color-text);
    font-family: var(--font-body);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.15em;
    padding: 0.5rem 1rem;
    cursor: pointer;
    transition: all 0.2s ease;
  }

  .rotate-hint-dismiss:hover {
    border-color: rgba(240, 235, 229, 0.65);
    background: rgba(240, 235, 229, 0.05);
  }

  /* Pause the GSAP scene while the rotate prompt is up so the chapters
     don't fire scroll triggers behind the overlay. */
  body:not(.rotate-dismissed) {
    overflow: hidden;
  }
}
/* ============================================================
   PERFORMANCE TIERS (static — decided once at load)
   html[data-perf-tier] is set by the inline <head> seed / adaptive.js
   from capability signals at load time and is NOT changed at runtime.
   "full" = no overrides — the design is untouched on any device with the
   headroom. "reduced"/"minimal" shed the GPU-expensive effects below.
   NOTE: only the rules in this section read the tier. The scroll engine
   (clip-path reveals, scene/foliage/tree parallax) does NOT — it runs the
   same at every tier, so do not assume a tier change alters it.
   ============================================================ */

/* Guide map — the single biggest runtime GPU cost. The 5-stage tile
   filter re-filters every visible tile on every pan/zoom frame, and the
   multiply overlay forces a backdrop read-back on top. Drop both; the
   map stays fully legible, just without the warm-paper tint. */
html[data-perf-tier="reduced"] #guide-map .leaflet-tile-pane,
html[data-perf-tier="minimal"] #guide-map .leaflet-tile-pane {
  filter: none;
}
html[data-perf-tier="reduced"] .map-tint,
html[data-perf-tier="minimal"] .map-tint {
  mix-blend-mode: normal;                 /* multiply read-back → plain composite */
  background: rgba(190, 165, 120, 0.10);  /* flat warm hint, one cheap layer */
}

/* Landscape rotate-hint backdrop blur → flat scrim (backdrop-filter is
   one of the most expensive effects under GPU contention). */
html[data-perf-tier="reduced"] .rotate-hint,
html[data-perf-tier="minimal"] .rotate-hint {
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
  background: rgba(8, 5, 3, 0.96);
}

/* Minimal tier: release the decorative corner layers' GPU promotion to
   cut tile-memory pressure (the scene/sky keep theirs — they remain the
   live parallax core and animate at every tier). */
html[data-perf-tier="minimal"] .foliage-layer,
html[data-perf-tier="minimal"] .tree-layer {
  will-change: auto;
}

/* ============================================================
   PEONY IRIS SHUTTER
   Reusable cover->load->reveal transition for the Gallery and Guide
   takeovers. Sits above everything (takeovers are z:50). Starts "open"
   (clipped to a 0% circle = invisible). cover() grows the circle to 150%
   (full screen) while assets load; reveal() shrinks it back to 0% over the
   now-loaded view. Single clip-path animation = compositor-friendly, smooth.
   Driven by shutter.js / window.__shutter. The opening preloader (inline in
   index.html) uses the same iris language and the same SVG peony mark — if
   you edit the petal paths or the blush, mirror the change there.
   ============================================================ */
#peony-shutter {
  position: fixed;
  inset: 0;
  z-index: 9990;
  display: grid;
  place-items: center;
  background: var(--cream);
  pointer-events: none;
  clip-path: circle(0% at 50% 50%);
  -webkit-clip-path: circle(0% at 50% 50%);
  transition: clip-path 0.62s var(--ease-cinema),
              -webkit-clip-path 0.62s var(--ease-cinema);
  will-change: clip-path;
}
#peony-shutter[data-state="covering"] {
  clip-path: circle(150% at 50% 50%);
  -webkit-clip-path: circle(150% at 50% 50%);
  transition-duration: 0.52s;   /* cover slightly faster than reveal */
  pointer-events: auto;
}
#peony-shutter .peony-mark {
  width: clamp(108px, 26vw, 172px);
  height: auto;
  color: var(--ink);
  opacity: 0;
  transition: opacity 0.42s ease;
}
/* Soft blush fill behind the ink linework. Petals are semi-transparent, so
   where they overlap (toward the center) the rose deepens naturally — giving
   the flat line-peony a little of a real peony's cupped depth. Tweak the rose
   or alpha via --peony-blush. */
#peony-shutter .peony-mark path {
  fill: var(--peony-blush, rgba(214, 158, 154, 0.20));
}
#peony-shutter[data-state="covering"] .peony-mark {
  opacity: 1;
  transition-delay: 0.1s;
}
/* While covered (i.e. loading), the peony breathes gently so the wait reads
   as an intentional flourish, not a freeze. */
@media (prefers-reduced-motion: no-preference) {
  #peony-shutter[data-state="covering"] .peony-mark {
    animation: peony-breathe 2.6s var(--ease-cinema) 0.25s infinite;
  }
}
@keyframes peony-breathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.07); }
}
@media (prefers-reduced-motion: reduce) {
  #peony-shutter { transition: none; }
  #peony-shutter .peony-mark { transition: none; }
}
