﻿/* ═══════════════════════════════════════════════════════════
   FONTS — self-hosted (fonts/ folder)

   Cormorant Infant  variable 300–700 + matching italic  — body serif
   Cinzel            variable 400–900                    — display caps / labels
   Corinthia         static 400                          — fallback for the signature script
   Pinyon Script     static 400                          — section titles and the couple's names
   STIX Two Text     variable 400–700                    — UI / notices

   swap: text paints in the fallback immediately, then re-renders once the
   webfont lands, so nothing on the page is invisible while fonts download.

   The format() hints are plain 'woff2'/'truetype', NOT the 'woff2-variations'
   spelling. That legacy hint is Chrome-only: Safari and iOS treat it as an
   unknown format and skip the src entry, so the face never loads and the
   text silently falls back to Georgia. The hint describes the container, not
   the variability — the weight axes work fine declared this way.
═══════════════════════════════════════════════════════════ */
@font-face {
  font-family: 'Cormorant Infant';
  src: url('fonts/CormorantInfant-VariableFont_wght.woff2') format('woff2'),
       url('fonts/CormorantInfant-VariableFont_wght.ttf')   format('truetype');
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Cormorant Infant';
  src: url('fonts/CormorantInfant-Italic-VariableFont_wght.woff2') format('woff2'),
       url('fonts/CormorantInfant-Italic-VariableFont_wght.ttf')   format('truetype');
  font-weight: 300 700;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: 'Cinzel';
  src: url('fonts/Cinzel-VariableFont_wght.woff2') format('woff2'),
       url('fonts/Cinzel-VariableFont_wght.ttf')   format('truetype');
  font-weight: 400 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'STIX Two Text';
  src: url('fonts/STIXTwoText.woff2') format('woff2'),
       url('fonts/STIXTwoText.ttf')   format('truetype');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Corinthia';
  src: url('fonts/Corinthia-Regular.woff2') format('woff2'),
       url('fonts/Corinthia-Regular.ttf')   format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Pinyon Script';
  src: url('fonts/PinyonScript-Regular.woff2') format('woff2'),
       url('fonts/PinyonScript-Regular.ttf')   format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ═══════════════════════════════════════════════════════════
   RESET & VARIABLES
═══════════════════════════════════════════════════════════ */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

:root {
  --cream:       #f5ede0;
  --cream-light: #faf5ec;
  --tan:         #d8c4a4;
  --tan-dark:    #b8a080;
  --brown:       #3a2818;
  --brown-mid:   #5c3e28;
  --brown-light: #8a6448;
  --gold:        #b49060;
  /* sampled from the painted frame in Background & Banners/Border.png —
     the green band down each side of the page */
  --sage:        #708058;
  --sage-veil:   rgba(112, 128, 88, 0.78);
  --text:        #2a1a0f;
  --text-soft:   #7a5c40;

  /* --font-script  section headings, mid-size script
     --font-signature  the couple's names — higher contrast, larger per em
     --font-serif   body copy, inputs, running text
     --font-sans    letterspaced uppercase labels and buttons (a caps serif,
                    kept under this name so every existing rule still resolves)
     --font-ui      form notices and errors — full sentences, needs lowercase */
  --font-script:    'Pinyon Script', 'Corinthia', cursive;
  /* Pinyon, not Corinthia, on the couple's names. Note the size consequence:
     at the same font-size Pinyon paints 1.32x the width and 1.17x the height
     of Corinthia, so every rule using this variable had its size cut to about
     0.76 of what it was. That factor matches the two by WIDTH rather than
     height, because the binding constraint is the gate card — a fixed 480px
     box the wordmark has to stay on one line inside. */
  --font-signature: 'Pinyon Script', 'Corinthia', cursive;
  --font-serif:     'Cormorant Infant', Georgia, serif;
  --font-sans:      'Cinzel', Georgia, serif;
  --font-ui:        'STIX Two Text', Georgia, serif;

  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);

  /* Landing screen budget. The clip is capped at 100svh minus these three,
     so the clip and the scroll bar under it always fit the first screen with
     nothing cut off — change them here and the video resizes to match.
     --invite-line-h is 0 on a wide screen because the greeting is overlaid on
     the clip and costs it no height; only the narrow-screen fallback, where
     the frame is too short to letter into, claims a band of its own. */
  --cue-bar-h:     56px;
  --cue-bar-gap:   0px;
  --invite-line-h: 0px;
}

html {
  scroll-behavior: smooth;
}

body {
  background: var(--cream);
  color: var(--text);
  font-family: var(--font-serif);
  overflow-x: hidden;
  min-height: 100vh;
}

/* ═══════════════════════════════════════════════════════════
   NAME GATE
═══════════════════════════════════════════════════════════ */
#gate {
  position: fixed;
  inset: 0;
  z-index: 300;
  background-image: url('Background%20%26%20Banners/Main%20Background.png');
  background-size: cover;
  background-position: center;
  display: flex;
  justify-content: center;
  /* Three fields plus the button make this card taller than a short screen
     (landscape phones, small laptops). Without scrolling the Enter button
     falls below the fold with no way to reach it. `margin: auto` on the card
     centres it when there is room and refuses to clip when there is not —
     which `align-items: center` alone does. */
  overflow-y: auto;
  padding: 28px 16px;
  transition: opacity 0.9s ease, visibility 0.9s ease;
}

#gate.gate-out {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.gate-inner {
  margin: auto;
  background: rgba(250, 244, 235, 0.93);
  backdrop-filter: blur(12px);
  border: 1px solid var(--tan);
  padding: 60px 72px;
  width: min(480px, 92vw);
  text-align: center;
  box-shadow: 0 12px 56px rgba(58, 40, 24, 0.14);
}

.gate-names {
  font-family: var(--font-signature);
  /* Sized to keep the wordmark on ONE line inside the gate card, which is a
     fixed min(480px, 92vw) box — 334px of content width at desktop. Pinyon
     paints this string at 5.4px per px of font-size, so anything above about
     3.7rem breaks it across two lines mid-phrase. (It was already breaking
     before the font change, at Corinthia 5.28rem.) */
  font-size: 3.6rem;
  color: var(--brown);
  line-height: 1.2;
}

.gate-date {
  font-family: var(--font-sans);
  font-size: 0.912rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-soft);
  margin-top: 8px;
}

.gate-rule {
  width: 52px;
  height: 1px;
  background: var(--tan);
  margin: 28px auto;
}

.gate-copy {
  font-size: 1.2rem;
  line-height: 1.8;
  color: var(--brown-light);
  margin-bottom: 32px;
}

#gate-form {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

#gate-form input {
  width: 100%;
  padding: 14px 18px;
  background: rgba(255, 255, 255, 0.75);
  border: 1px solid var(--tan);
  font-family: var(--font-serif);
  font-size: 1.2rem;
  color: var(--text);
  outline: none;
  transition: border-color 0.25s;
}

#gate-form input::placeholder {
  color: var(--text-soft);
  opacity: 0.7;
}

#gate-form input:focus {
  border-color: var(--gold);
}

#gate-form input.shake {
  border-color: #b03030;
  animation: shake 0.45s ease;
}

#gate-btn {
  margin-top: 6px;
  padding: 15px;
  background: var(--brown);
  color: var(--cream-light);
  border: none;
  font-family: var(--font-sans);
  font-size: 0.936rem;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 0.25s, transform 0.15s;
}

#gate-btn:hover  { background: var(--brown-mid); }
#gate-btn:active { transform: scale(0.98); }

#gate-error {
  font-family: var(--font-ui);
  font-size: 0.984rem;
  color: #b03030;
}

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  18%, 54%  { transform: translateX(-7px); }
  36%, 72%  { transform: translateX(7px); }
}

/* ═══════════════════════════════════════════════════════════
   FIXED BACKGROUND & BORDER
═══════════════════════════════════════════════════════════ */
#bg-layer {
  position: fixed;
  inset: 0;
  z-index: 0;
  background-image: url('Background%20%26%20Banners/Main%20Background.png');
  background-size: cover;
  background-position: top center;
  background-repeat: no-repeat;
}

#border-layer {
  position: fixed;
  inset: 0;
  z-index: 10;
  background-image: url('Background%20%26%20Banners/Border.png');
  background-size: 100% 100%;
  background-position: top center;
  background-repeat: no-repeat;
  pointer-events: none;
  opacity: 0;
}

/* ═══════════════════════════════════════════════════════════
   SECTION NAV
   Full-bleed jump bar pinned to the top edge. It does cross the painted
   frame in #border-layer — the green band down each side stops at the bar
   and picks up again below it — which is the trade for a bar that runs edge
   to edge. Hence the bottom hairline and no side or top border: a box outline
   would draw a second, competing frame across the top of the real one.

   Sits at z-index 60: above the border (10) and the page content (5), below
   the dress-guide lightbox (200) and the RSVP modal (210), so a modal never
   has a nav bar floating over its backdrop.
═══════════════════════════════════════════════════════════ */
#section-nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 60;
  transform: translateY(-100%);
  background: rgba(250, 244, 235, 0.94);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--tan);
  box-shadow: 0 4px 22px rgba(58, 40, 24, 0.11);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.45s ease, transform 0.45s ease, visibility 0.45s ease;

  /* Two children: the scrolling link strip, and the RSVP block held at the
     right edge. align-items: stretch is what makes the sage box run the full
     height of the bar rather than floating inside it. */
  display: flex;
  align-items: stretch;
}

/* Added by initSectionNav() once the landing screen has been scrolled past.
   `hidden` is on the element in the markup as well, so a guest with no
   JavaScript never gets a bar whose highlight would not work. */
#section-nav.nav-shown {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

/* A modal is up (both openGuide and openRsvp put .modal-active on the body):
   get out of its way entirely rather than float over its backdrop. Outranks
   .nav-shown above on specificity, so no JS is needed to undo it. */
body.modal-active #section-nav {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

#section-nav-list {
  display: flex;
  align-items: stretch;
  list-style: none;
  /* Takes the width the RSVP block does not. min-width: 0 is what allows it
     to be squeezed below its content width — without it a flex item refuses
     to shrink past that, the strip never overflows, and on a phone the labels
     would push the sage box off the right edge instead of scrolling. */
  flex: 1;
  min-width: 0;
  /* The bar is full width but the labels are not, so they sit in the middle
     of it. `safe` is doing real work here: with a plain `center`, once the
     labels overflow on a phone the first one is pushed off the left edge into
     space no scroll can reach. `safe` drops back to flex-start exactly when
     that would happen. Browsers without it ignore the whole declaration and
     land on flex-start anyway, which is the same fallback. */
  justify-content: safe center;
  /* Seven labels do not fit across a phone. Rather than wrap to two rows or
     shrink the type past reading size, the strip scrolls sideways under the
     finger. */
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}

#section-nav-list::-webkit-scrollbar { display: none; }

/* Scroll affordance. A strip that overflows with a hard edge just looks like
   a bar that happens to end at "Travel"; softening the cut is what says there
   is more to swipe to. Only ever set on the side that actually has more —
   syncStripFades() in script.js owns these — so on a desktop, where all seven
   fit, neither is applied and no label is dimmed. */
#section-nav-list.fade-end {
  mask-image: linear-gradient(to right, #000 calc(100% - 34px), transparent 100%);
}

#section-nav-list.fade-start {
  mask-image: linear-gradient(to right, transparent 0, #000 34px);
}

#section-nav-list.fade-start.fade-end {
  mask-image: linear-gradient(to right,
    transparent 0, #000 34px,
    #000 calc(100% - 34px), transparent 100%);
}

#section-nav-list a {
  display: block;
  /* The links set the bar's height, and the height is what gives the sage
     RSVP block its presence — it fills whatever these leave. */
  padding: 15px 17px;
  font-family: var(--font-sans);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--brown-light);
  text-decoration: none;
  /* transparent by default so the link does not shift when it goes active */
  border-bottom: 2px solid transparent;
  transition: color 0.25s ease, border-color 0.25s ease, background 0.25s ease;
}

#section-nav-list li + li a {
  box-shadow: inset 1px 0 0 rgba(216, 196, 164, 0.55);
}

#section-nav-list a:hover,
#section-nav-list a:focus-visible {
  color: var(--brown);
  background: rgba(216, 196, 164, 0.22);
}

#section-nav-list a:focus-visible {
  outline: 1px solid var(--gold);
  outline-offset: -3px;
}

/* Set by the scroll spy in initSectionNav(). */
#section-nav-list a.nav-active {
  color: var(--brown);
  border-bottom-color: var(--gold);
}

/* ── RSVP block ──────────────────────────────────────────────
   The sage is sampled from the painted frame, so the one saturated block on
   the page is still a colour the artwork already uses. It is the only filled
   element in the bar, which is the whole point: six quiet labels and one
   thing to press.

   flex-shrink: 0 holds its width while the link strip is squeezed, so it
   stays whole and reachable at every width — on a phone the labels scroll
   underneath it rather than pushing it off screen. */
#nav-rsvp {
  flex: 0 0 auto;
  padding: 0 clamp(24px, 3.4vw, 48px);
  /* Four letters is not much to pad out, so a floor on the width is what
     actually makes this read as a block rather than a wide label. */
  min-width: clamp(130px, 13vw, 190px);
  background: var(--sage);
  color: var(--cream-light);
  border: none;
  font-family: var(--font-sans);
  /* Deliberately well above the 0.72rem the links run at — this is the one
     thing in the bar we want read first, and the size is what says so. */
  font-size: 1.02rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.25s ease;
}

#nav-rsvp:hover {
  /* darker rather than lighter: the cream lettering has to keep its contrast */
  background: #5f6d4a;
}

#nav-rsvp:focus-visible {
  outline: 2px solid var(--cream-light);
  outline-offset: -5px;
}

/* ── Hamburger (phones only) ─────────────────────────────────
   Above 768px the six labels fit across the bar and the toggle is not
   rendered at all. Below it they did not: the strip scrolled sideways and
   kept most of itself off the edge, so a guest had to discover it by
   swiping a bar that looked like it simply ended at "Travel". The labels
   now open downward as a list instead. */
#nav-toggle {
  display: none;
}

@media (max-width: 768px) {
  /* The bar's height used to come from the padding on the links inside it.
     Those are in an absolutely-positioned panel now, so they no longer prop
     it open and it collapsed to the height of the burger's own 1.5px bar.
     State it here instead; #section-nav is align-items: stretch, so the
     burger and the RSVP block both fill it. */
  #section-nav {
    min-height: 54px;
  }

  /* With the link strip out of the flow, these two are the only flex items
     left and both sat against the left edge. The strip used to be the thing
     taking up the slack. */
  #nav-rsvp {
    margin-left: auto;
  }

  #nav-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 52px;
    padding: 0;
    background: none;
    border: none;
    color: var(--brown-light);
    cursor: pointer;
  }

  #nav-toggle:focus-visible {
    outline: 1px solid var(--gold);
    outline-offset: -4px;
  }

  /* Three bars from one element: the span is the middle, the two
     pseudo-elements are offset above and below it. */
  #nav-toggle .burger-bars {
    position: relative;
    display: block;
    width: 21px;
    height: 1.5px;
    background: currentColor;
    transition: background 0.2s ease;
  }

  #nav-toggle .burger-bars::before,
  #nav-toggle .burger-bars::after {
    content: '';
    position: absolute;
    left: 0;
    width: 100%;
    height: 100%;
    background: currentColor;
    transition: top 0.28s ease, transform 0.28s ease;
  }

  #nav-toggle .burger-bars::before { top: -6.5px; }
  #nav-toggle .burger-bars::after  { top:  6.5px; }

  /* Open: the middle bar goes, the outer two cross. */
  #section-nav.nav-open #nav-toggle .burger-bars {
    background: transparent;
  }

  #section-nav.nav-open #nav-toggle .burger-bars::before {
    top: 0;
    transform: rotate(45deg);
  }

  #section-nav.nav-open #nav-toggle .burger-bars::after {
    top: 0;
    transform: rotate(-45deg);
  }

  /* The strip becomes a panel hanging under the bar. display rather than a
     height cap so nothing needs measuring, and the fade masks the strip
     used are pointless on a vertical list — they are cleared here so a
     stale .fade-end from a resize cannot dim the last row. */
  #section-nav-list {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    overflow-x: visible;
    background: rgba(250, 244, 235, 0.97);
    backdrop-filter: blur(10px);
    border-bottom: 1px solid var(--tan);
    box-shadow: 0 10px 24px rgba(58, 40, 24, 0.14);
    -webkit-mask-image: none;
            mask-image: none;
  }

  #section-nav.nav-open #section-nav-list {
    display: flex;
  }

  /* Full-width rows, hairline between them, and a wider tap target than the
     15px the horizontal bar used. */
  #section-nav-list a {
    padding: 15px 22px;
    font-size: 0.78rem;
    border-bottom: none;
    border-left: 2px solid transparent;
  }

  #section-nav-list li + li a {
    box-shadow: inset 0 1px 0 rgba(216, 196, 164, 0.55);
  }

  /* The active marker was a bottom border, which on stacked rows reads as a
     divider rather than a highlight. Down the leading edge instead. */
  #section-nav-list a.nav-active {
    border-left-color: var(--gold);
    background: rgba(216, 196, 164, 0.18);
  }
}

/* Landing on a section must not tuck its heading under the bar. The bar is
   48px tall here, so this is its height plus room to breathe. */
#hero,
#map,
.content-section {
  scroll-margin-top: 76px;
}

@media (max-width: 900px) {
  #section-nav-list a {
    padding: 12px 14px;
    font-size: 0.665rem;
    letter-spacing: 0.14em;
  }

  /* Smaller floor on a phone: every pixel this takes is one the scrolling
     link strip beside it loses. */
  #nav-rsvp {
    padding: 0 clamp(16px, 4.6vw, 26px);
    min-width: 100px;
    font-size: 0.88rem;
    letter-spacing: 0.16em;
  }

  /* 42px bar on a phone */
  #hero,
  #map,
  .content-section {
    scroll-margin-top: 64px;
  }
}

@media (prefers-reduced-motion: reduce) {
  #section-nav { transition: opacity 0.2s ease, visibility 0.2s ease; }
  #section-nav,
  #section-nav.nav-shown { transform: none; }
}

/* ═══════════════════════════════════════════════════════════
   MUSIC TOGGLE
   Sits above the border layer (z-index 10) but below the modal and the
   gate, so it never covers either. Unhidden by script.js only once the
   track is actually playing — an audio control that does nothing is
   worse than no control.
═══════════════════════════════════════════════════════════ */
#music-toggle {
  position: fixed;
  right: 26px;
  bottom: 24px;
  z-index: 100;
  width: 42px;
  height: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--tan);
  border-radius: 50%;
  /* Deliberately no backdrop-filter. This button overlaps the landing
     video, and a live blur of whatever is behind it has to be recomputed
     on every video frame. A slightly more opaque fill looks the same and
     costs nothing. */
  background: rgba(250, 244, 235, 0.94);
  color: var(--brown);
  cursor: pointer;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.6s ease, transform 0.6s ease,
              background 0.25s ease, border-color 0.25s ease;
  box-shadow: 0 3px 14px rgba(58, 40, 24, 0.12);
}

#music-toggle.mt-shown {
  opacity: 0.82;
  transform: translateY(0);
}

#music-toggle.mt-shown:hover {
  opacity: 1;
  background: var(--cream-light);
  border-color: var(--gold);
}

#music-toggle svg {
  width: 21px;
  height: 21px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

#music-toggle .mt-speaker {
  fill: currentColor;
  stroke-width: 1;
}

/* Playing: waves shown, slash hidden. Muted: the reverse. */
#music-toggle .mt-slash { opacity: 0; transition: opacity 0.2s ease; }
#music-toggle .mt-waves { opacity: 1; transition: opacity 0.2s ease; }

#music-toggle.mt-muted .mt-slash { opacity: 1; }
#music-toggle.mt-muted .mt-waves { opacity: 0; }

/* An event modal owns the whole screen — get the toggle out of its way
   rather than relying on stacking order to hide it. */
body.modal-active #music-toggle {
  opacity: 0;
  pointer-events: none;
}

@media (max-width: 768px) {
  #music-toggle { right: 16px; bottom: 16px; }
}

/* ═══════════════════════════════════════════════════════════
   PAGE WRAPPER
═══════════════════════════════════════════════════════════ */
.page-hidden {
  opacity: 0;
  visibility: hidden;
}

.page-visible {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.25s ease;
}

main {
  position: relative;
  z-index: 5;
  padding: 0 24px;
}

/* ═══════════════════════════════════════════════════════════
   INVITE GREETING
   Lettered onto the clip's own top margin, above the "Goa · Jan 5-6, 2027"
   line painted into the frame. The top eighth of the frame is clean cream
   paper — the only part of this collage text reads on — so the line needs no
   scrim behind it.

   height: 0 is what makes it an overlay rather than a bar: the block takes no
   room in the flow and its two lines simply overflow down onto the clip that
   follows it, so the landing screen budget above is untouched. It stays a
   sibling of the clip rather than a child so the narrow-screen fallback can
   put it back into the flow with one rule.

   Sized off min(vw, svh) because the frame is width-bound on a tall screen
   and height-bound on a short one — whichever is smaller is the one that
   decides how much cream there is to letter into.
═══════════════════════════════════════════════════════════ */
#invite-greeting {
  position: relative;
  /* over the clip, under the fixed border frame and the nav */
  z-index: 6;
  height: 0;
  /* Down into the frame's top margin, tracking the frame as it scales. Has to
     be `top` on a relatively positioned box, not a margin or padding on
     either this or the lines inside it: a top margin here collapses through
     the zero-height box and shoves the clip itself down the page, and padding
     gives the box real height again. `top` moves the letters and nothing
     else. */
  top: min(2.1vw, 2.35svh);
  text-align: center;
  /* decorative: never intercept a click meant for the clip beneath */
  pointer-events: none;
  line-height: 1.3;
  font-size: clamp(1rem, min(2.05vw, 2.35svh), 1.62rem);
}

.invite-dear {
  display: block;
  font-family: var(--font-serif);
  font-size: 1em;
  color: var(--brown);
}

/* Smaller than the name above it — the guest's own name is the thing to read
   first, this is the sentence it hangs off. */
.invite-line {
  display: block;
  font-family: var(--font-serif);
  font-style: italic;
  font-size: 0.68em;
  color: var(--text-soft);
  margin-top: 0.15em;
}

/* Nothing to address (no name in sessionStorage): say nothing rather than
   "Dear ". Costs no layout either way, since the block has no height. */
#invite-greeting.no-name .invite-dear,
#invite-greeting.no-name .invite-line {
  display: none;
}

/* No clip to letter onto — it failed to load — so the line takes a band of
   its own back rather than floating over an empty frame. */
main:has(#video-hero.video-failed) #invite-greeting {
  height: auto;
  top: 0;
  padding: 20px 16px 14px;
  font-size: 1.5rem;
}

/* ═══════════════════════════════════════════════════════════
   VIDEO LANDING
═══════════════════════════════════════════════════════════ */
#video-hero {
  position: relative;
  /* break out of main's 24px padding to sit edge to edge */
  width: 100vw;
  margin-left: calc(50% - 50vw);
  /* Match the clip's own 16:9 shape rather than forcing a full-viewport
     box. Filling the viewport meant object-fit: cover had to crop the
     frame, which cut into the title lettering. Capped at the viewport
     so it can never need scrolling to see. */
  aspect-ratio: 16 / 9;
  height: auto;
  /* leave room for the scroll bar underneath — see --cue-bar-h */
  max-height: calc(100svh - var(--cue-bar-h) - var(--cue-bar-gap) - var(--invite-line-h));
  overflow: hidden;
  line-height: 0;
  /* cream underneath, so nothing ever flashes black */
  background: var(--cream);
}

#hero-video {
  width: 100%;
  height: 100%;
  /* contain, not cover — show the whole frame, never crop it */
  object-fit: contain;
  display: block;
  /* held back until frames are genuinely rendering */
  opacity: 0;
  transition: opacity 1s ease;
}

#video-hero.video-ready #hero-video {
  opacity: 1;
}

/* Retired. This used to feather the bottom of the clip into the paper,
   but the gradient ramped to opaque cream over the last 12% of the frame
   and read as a white glow along the bottom edge. The clip already sits
   on a cream field, so no blending is needed. */
#video-scrim {
  display: none;
}

/* A sibling of #video-hero, not a child of it — see the note in index.html. */
#scroll-bar {
  /* full-bleed, like the clip above it */
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-top: var(--cue-bar-gap);
  height: var(--cue-bar-h);
  display: flex;
  align-items: center;
  justify-content: center;
  /* sage band under the cream clip, picking up the green of the painted
     frame. Kept semi-translucent so the paper reads through it and the band
     sits in the page rather than on top of it — the cue still carries the
     landing screen, but as part of the frame's palette rather than a hard
     brown slab. */
  background: var(--sage-veil);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}

#scroll-cue {
  /* label then arrow, side by side — one line of content means the bar can be
     roughly half the height it needed when they were stacked */
  display: flex;
  flex-direction: row;
  align-items: center;
  /* Everything on the bar is sized in em off this, so the arrow tracks the
     label as it scales rather than looking oversized on a phone. Same clamp
     the label used to carry: the tracked-out caps must not overflow a narrow
     screen, but should still read large on a desktop. */
  font-size: clamp(0.78rem, 1.85vw, 1.2rem);
  /* the label's trailing letter-spacing already contributes ~0.2em, so this
     is smaller than the optical gap it produces */
  gap: 0.36em;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  opacity: 0;
  transition: opacity 0.8s ease 0.6s;
}

#video-hero.video-ready ~ #scroll-bar #scroll-cue {
  opacity: 1;
}

/* No clip, no bar — nothing above it to scroll past. */
#video-hero.video-failed ~ #scroll-bar {
  display: none;
}

#scroll-cue:hover .cue-label,
#scroll-cue:hover .cue-arrow { border-color: #fff; color: #fff; }

#scroll-cue .cue-arrow {
  display: block;
  width: 0.72em;
  height: 0.72em;
  /* the chevron's visual mass sits below the element's centre, so nudge it up
     to optically level with the cap-height of the label beside it */
  margin-bottom: 0.42em;
  flex: none;
  border-right: 2px solid var(--cream);
  border-bottom: 2px solid var(--cream);
  transform: rotate(45deg);
  animation: cue-bob 2.1s ease-in-out infinite;
  transition: border-color 0.25s;
}

#scroll-cue .cue-label {
  font-family: var(--font-sans);
  /* the scaling clamp lives on #scroll-cue so the arrow can share it */
  font-size: 1em;
  font-weight: 600;
  letter-spacing: 0.2em;
  /* the tracking adds a trailing gap after the last letter; pull it back so
     the line stays optically centred on the bar */
  text-indent: 0.2em;
  text-transform: uppercase;
  color: var(--cream);
  white-space: nowrap;
  line-height: 1;
  transition: color 0.25s;
}

@keyframes cue-bob {
  0%, 100% { transform: rotate(45deg) translate(0, 0); }
  50%      { transform: rotate(45deg) translate(4px, 4px); }
}

/* If the video can't be decoded, collapse the section rather than leaving
   a full screen of empty paper above the title. */
#video-hero.video-failed {
  height: auto;
  min-height: 0;
}

@media (prefers-reduced-motion: reduce) {
  #scroll-cue .cue-arrow { animation: none; }
}

/* ═══════════════════════════════════════════════════════════
   HERO
═══════════════════════════════════════════════════════════ */
#hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-top: 56px;
  padding-bottom: 8px;
  text-align: center;
}

#couple-photo {
  width: clamp(336px, 55vw, 624px);
  height: auto;
  filter: drop-shadow(0 10px 30px rgba(58, 40, 24, 0.18));
}

#hero-title {
  font-family: var(--font-script);
  /* Kept on a single line at every width: nowrap holds the phrase together
     and the vw term shrinks the type to match, so it never wraps and never
     overflows the 88vw the rest of the hero sits inside. */
  font-size: min(4.68rem, 7.6vw);
  line-height: 1.25;
  font-weight: 400;
  color: var(--brown);
  max-width: 96vw;
  white-space: nowrap;
  margin-top: -4px;
}

#hero-blurb {
  font-family: var(--font-serif);
  font-size: 1.56rem;
  line-height: 1.9;
  font-style: italic;
  color: var(--brown-light);
  /* 640, not 660: at 660 the measure ends one word short and "Village." is
     left hanging on a line of its own. Pulling the measure in by 20px moves
     "the Taj Holiday" down with it, so the paragraph closes on a full line. */
  max-width: min(640px, 88vw);
  /* Belt and braces for the same problem at other widths — a large phone in
     landscape lands on a measure that also orphans the last word, and a
     fallback font would shift every break. Browsers that support it avoid
     short last lines on their own; the rest get the tuned measure above.
     `pretty` rather than `balance` on purpose: balance gives up past a
     handful of lines (this runs to nine on a phone) and visibly re-rags the
     whole paragraph, which would make Firefox and Chrome disagree. */
  text-wrap: pretty;
  margin: 18px auto 26px;
}

/* Only one of these left — the RSVP section at the foot of the page. The
   copy that used to sit under the hero is now #nav-rsvp in the top bar. */
.rsvp-btn-img {
  /* Named so the crop below can be derived from it. */
  --rsvp-btn-w: clamp(200px, 27vw, 330px);
  width: var(--rsvp-btn-w);
  height: auto;
  cursor: pointer;
  transition: transform 0.2s ease, filter 0.2s ease;

  /* RSVP Button.png is 500x293 with the artwork between y=108 and y=185, so
     37% of the file is empty above the button and 37% below — 71px of each at
     full size. That empty space is why the button looked marooned between the
     line above it and the footer below. Tied to the button's own width rather
     than flat pixels so it holds at every size in the clamp; 0.15 of 0.216
     reclaims most of the band without cropping the painted edge. */
  margin-top:    calc(var(--rsvp-btn-w) * -0.15);
  margin-bottom: calc(var(--rsvp-btn-w) * -0.15);
}

.rsvp-btn-img:hover {
  transform: translateY(-3px);
  filter: brightness(1.08);
}

.rsvp-btn-img:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 6px;
}

/* The section's own copy, which replaced the form that used to sit here. */
.rsvp-cta {
  display: flex;
  justify-content: center;
  padding-top: 0;
}

/* The RSVP section is a heading, one line, and a button — it does not need
   the same rhythm as sections carrying real copy, and at full spacing it read
   as three things adrift rather than one block. */
#rsvp .section-title { margin-bottom: 22px; }
#rsvp .section-text  { margin-bottom: 22px; }

/* ═══════════════════════════════════════════════════════════
   MAP SECTION
═══════════════════════════════════════════════════════════ */
#map {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 10px 0;

  /* One geometry for the whole map. The event rows and the trails drawn
     between them both lay themselves out on it, so a trail cannot drift out
     of the island column when the row proportions change. */
  --map-w:      min(1400px, 95vw);
  --map-gap:    clamp(14px, 2.4vw, 40px);
  --map-island: 1fr;
  --map-detail: 1fr;
}

/* The flourishes used to run down the middle of the page, threading between
   islands that alternated left and right. Every island sits on the left now,
   so each trail rides in the island column with them and the eye follows the
   chain straight down instead of being thrown back to the centre. */
.map-trail {
  display: grid;
  grid-template-columns: minmax(0, var(--map-island)) minmax(0, var(--map-detail));
  gap: var(--map-gap);
  width: var(--map-w);
  /* The vertical gap between two cards IS this element — there is no
     padding anywhere in the map, so the flourish's box is the whole of it.
     Which means the gap can only be tightened by cropping the empty space
     in that box, and there is far less of it than the artwork suggests:
     Map trail-2.png draws ink from 12.5%% to 82.8%% of its own height, so
     only an eighth of it is clear above the ink. Past that the flourish
     starts overprinting the card above.

     Hence the pull-in is a fraction of --map-w rather than a pixel value:
     the flourish is a fixed share of --map-w too, so one number holds the
     same clearance at every width. A pixel value tuned here overlapped the
     ink just above the 768px breakpoint, where the trail is at its
     smallest. Raising 0.008 tightens the gap; much past 0.012 and trail-2
     crosses into the card. */
  margin: calc(var(--map-w) * -0.008) 0;
  line-height: 0;
}

/* Was 55%. The flourish scales with this and the gap scales with the
   flourish, so this is the real control over how far apart two cards sit:
   the gap runs about 0.081 x --map-w, or a shade under a quarter of the
   width the trail is drawn at. */
.map-trail img {
  grid-column: 1;
  width: 34%;
  height: auto;
  display: block;
  margin: 0 auto;
}

/* The trail PNGs are mostly empty — Map trail-1.png is 1200x703 with the ink
   between y=285 and y=403, so 40% of the file above the flourish is nothing.
   Between two islands that empty space reads as ordinary breathing room, but
   the first trail follows the hero, where it left a gap far wider than any
   other on the page.

   Pulled up less than it once was: this was tuned when the hero ended in the
   RSVP button, whose artwork carried an empty band of its own for the trail
   to ride up over. The button now lives in the top bar, so the trail comes up
   under the blurb's last line instead — text, with no slack in it — and the
   old value ran the flourish too close to the words.

   A percentage margin here resolves against .map-trail's width, which is
   --map-w, and the trail image is a fixed share of that — so this scales with
   the map instead of drifting at other widths the way a pixel value would. */
#trail-1 img {
  margin-top: -2.5%;
}

.event-stop {
  width: 100%;
  display: flex;
  justify-content: center;
}

/* ── Event row: the card's two pages, front left, back right ──
   This row used to be an island cut-out beside a wide painted banner, on an
   0.82 / 1.5 split. It now holds the two pages of the printed card, which
   are the same portrait size, so the columns are even. */
.event-row {
  display: grid;
  grid-template-columns: minmax(0, var(--map-island)) minmax(0, var(--map-detail));
  align-items: center;
  gap: var(--map-gap);
  width: var(--map-w);
}

.event-island {
  line-height: 0;
}

.island-card {
  width: 100%;
  height: auto;
  display: block;
}


/* Shrink-wraps the rendered banner so the date line and the "Outfit inspiration"
   hotspot can both be placed in percentages of the artwork rather than of
   the viewport. container-type lets the date size itself off the banner's
   own width (cqw), so it tracks the painted text at any width. */
.detail-banner {
  position: relative;
  line-height: 0;
  container-type: inline-size;
}

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


/* ── "Outfit inspiration" hotspot ───────────────────────────────
   The words are part of the banner artwork. This sits invisibly on top of
   them and lights them up on hover, so the painted text is the link.
   initEventDetails() sets left/top/width/height and --wear-glow per event. */
.wear-hotspot {
  position: absolute;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
  border-radius: 4px;
  transition: background 0.25s ease, box-shadow 0.25s ease;
}

/* Extends the tap target past the glow without moving it — the words are
   only ~14px tall on a phone. */
.wear-hotspot::after {
  content: '';
  position: absolute;
  inset: -10px -14px;
}

.wear-hotspot:hover,
.wear-hotspot:focus-visible {
  background: var(--wear-glow, rgba(255, 255, 255, 0.14));
  box-shadow: 0 0 0 7px var(--wear-glow, rgba(255, 255, 255, 0.14));
  outline: none;
}

/* ═══════════════════════════════════════════════════════════
   ANIMATION CLASSES
═══════════════════════════════════════════════════════════ */

/* All animated elements start invisible */
.anim-elem {
  opacity: 0;
}

/* ── Keyframes ──────────────────────────────────────────── */

@keyframes fade-up {
  0%   { opacity: 0; transform: translateY(28px); }
  100% { opacity: 1; transform: translateY(0); }
}

@keyframes border-fade {
  0%   { opacity: 0; }
  100% { opacity: 1; }
}

/* ═══════════════════════════════════════════════════════════
   SECTION DIVIDER
═══════════════════════════════════════════════════════════ */
.divider-wrap {
  display: flex;
  justify-content: center;
  width: min(320px, 50vw);
  margin: 36px auto;
}

.divider-img {
  width: 100%;
  height: auto;
  opacity: 0.75;
  /* Line Element.png is 700x410 with the rule itself only 48px of that — 44%
     of the file is empty above it and 44% below. Rendered at 320px wide that
     was 82px of nothing on each side of a 22px line, and it was the single
     biggest contributor to the gap between sections.

     A percentage margin resolves against .divider-wrap's width, which is this
     image's width, so -20% scales with the divider instead of drifting at
     other widths. It reclaims about four fifths of the empty band and leaves
     the rest as breathing room. */
  margin: -20% 0;
}

/* ═══════════════════════════════════════════════════════════
   CONTENT SECTIONS (Story, Family, Travel, FAQs, RSVP)
═══════════════════════════════════════════════════════════ */
.content-section {
  /* Was 56. Halving the run of empty page between one section's last line
     and the next section's title meant taking it from all three places it
     came from: this padding, the divider's margins, and the divider PNG's own
     empty top and bottom. See .divider-img below. */
  padding: 40px 0;
  max-width: 820px;
  margin: 0 auto;
}

.section-title {
  font-family: var(--font-script);
  font-size: clamp(3.24rem, 6.8vw, 4.68rem);
  line-height: 1.25;
  font-weight: 400;
  color: var(--brown);
  text-align: center;
  margin-bottom: 36px;
}

/* "FAQs" and "RSVP" are acronyms — as script capitals they were close to
   unreadable, so these two headings take the display caps serif instead.
   Cinzel's cap height is roughly twice Pinyon's per em, hence the much
   smaller size; the id selectors also outrank the responsive
   .section-title override further down, so the clamp stays in charge. */
#faqs .section-title,
#rsvp .section-title {
  font-family: var(--font-sans);
  font-size: clamp(2.1rem, 4.4vw, 2.88rem);
  font-weight: 500;
  letter-spacing: 0.12em;
}

.section-body {
  /* inner content */
}

.section-text {
  /* Matches #hero-blurb and the story prose, so every section intro on
     the page reads at one size. */
  font-size: 1.56rem;
  line-height: 1.9;
  color: var(--brown-light);
  text-align: center;
  font-style: italic;
  margin-bottom: 40px;
  max-width: 660px;
  margin-left: auto;
  margin-right: auto;
}

/* ── Our Story prose ─────────────────────────────────────────
   The italic, centred .section-text works for a line or two but not
   for ten paragraphs — centred rag makes every line start in a
   different place, so the eye loses its way back. This block is set
   ranged left, roman rather than italic, at a slightly smaller size
   and a measure of roughly 70 characters. */
.story-prose {
  max-width: 780px;
  margin: 0 auto 44px;
  font-family: var(--font-serif);
  font-size: 1.56rem;
  line-height: 1.9;
  color: var(--brown-light);
  text-align: left;
}

.story-prose p {
  margin: 0 0 1.1em;
}

.story-prose p:last-child {
  margin-bottom: 0;
}

/* The closing line echoes the standfirst above the story. It sits inside
   .story-more now, so this has to reach in — .story-prose p:last-child
   would match paragraph four, the last one before the collapse. */
.story-more p:last-child {
  font-style: italic;
  color: var(--brown-mid);
}

/* ── Story collapse ──────────────────────────────────────────
   The last paragraph before the cut dissolves downwards so the text
   visibly runs out rather than stopping flat.

   This is a mask on the text itself, not a cream gradient laid over
   it: the section sits on the paper texture and, once expanded, over
   the photo grid, so a solid-colour wash would show as a pale band
   against both. Masking fades the glyphs to genuinely transparent
   whatever is behind them.

   The ramp stays fully opaque through the first half of the block so
   the paragraph is still readable, then falls away over the last line
   or two — that final dissolve is the cue, and the button underneath
   says the rest out loud for anyone who misses it. */
.story-prose.is-collapsed .story-more {
  display: none;
}

/* .story-more is a div, so among the direct children this is the last
   paragraph still on screen — the one the cut lands on. */
.story-prose.is-collapsed > p:last-of-type {
  /* Measured from the bottom in em rather than in percentages, so the
     fade is always about the last one and a half lines. As a
     percentage it scaled with the paragraph instead: this block wraps
     to three lines on a desktop and five on a phone, where a 44% ramp
     was dissolving two full lines of readable text.

     Stopping a shade short of the bottom edge is deliberate — the
     line-height leaves descender space below the final baseline, and a
     ramp that only hits zero at the box edge never quite finishes
     dissolving the glyphs. */
  -webkit-mask-image: linear-gradient(to bottom,
                      #000 calc(100% - 2.9em),
                      rgba(0, 0, 0, 0.24) calc(100% - 1.1em),
                      transparent calc(100% - 0.15em));
          mask-image: linear-gradient(to bottom,
                      #000 calc(100% - 2.9em),
                      rgba(0, 0, 0, 0.24) calc(100% - 1.1em),
                      transparent calc(100% - 0.15em));
}

/* Pull the button up while collapsed so it reads as the continuation
   of the fade rather than a separate thing further down the page. */
.story-prose.is-collapsed {
  margin-bottom: 10px;
}

.story-more-wrap {
  display: flex;
  justify-content: center;
  margin-bottom: 44px;
}

/* Revealed with a keyframe rather than a transition, for the same
   reason as the gallery: this section is built inside the gate's
   visibility:hidden subtree, where a transition never commits. */
.story-prose:not(.is-collapsed) .story-more {
  animation: story-more-in 0.4s ease both;
}

@keyframes story-more-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .story-prose:not(.is-collapsed) .story-more { animation: none; }
}

/* On a phone the story opens on the first two paragraphs only, so Keep
   reading lands directly under the anthropology line instead of four
   paragraphs further down — four of these run much longer on a narrow
   column. .story-more-wrap already follows .story-prose in the markup, so
   hiding everything past the cut is all it takes to bring the button up. */
@media (max-width: 768px) {
  .story-prose.is-collapsed > p:nth-of-type(n+3) {
    display: none;
  }

  /* The dissolve has to come off here. It is keyed to :last-of-type, which
     is still paragraph four — now hidden — and paragraph two is a single
     short line that the 2.9em ramp would fade away almost entirely. A clean
     stop with the button right beneath it reads better at this width. */
  .story-prose.is-collapsed > p:last-of-type {
    -webkit-mask-image: none;
            mask-image: none;
  }

  .story-prose.is-collapsed > p:nth-of-type(2) {
    margin-bottom: 0;
  }
}

/* ── Photo Gallery ───────────────────────────────────────────
   Twenty-seven photos, so the tiles run three across and take a
   portrait 4:5 crop — most of these came off a phone held upright,
   and a square or landscape frame lopped the heads off. The old
   two-across, fixed-260px-tall layout was built for four images
   and would have run to fourteen rows here. */
.photo-gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
}

.gallery-item {
  overflow: hidden;
  border: 3px solid rgba(255,255,255,0.9);
  box-shadow: 0 4px 22px rgba(58, 40, 24, 0.12);
  line-height: 0;
  transition: transform 0.35s ease;
}

.gallery-item:hover {
  transform: rotate(1deg) scale(1.02);
  position: relative;
  z-index: 2;
}

/* ── Gallery preview / expand ─────────────────────────────────
   Collapsed, the grid shows one preview block of tiles and the
   rest are display:none. Hiding by nth-child rather than capping
   the wrapper's height means no JS has to measure anything, and
   it stays correct when the column count changes at the mobile
   breakpoint — the cutoff just moves with it. */
.gallery-wrap {
  position: relative;
}

.gallery-wrap.is-collapsed .gallery-item:nth-child(n + 10) {
  display: none;
}

/* Cream wash over the last preview row, so the grid reads as cut
   off mid-story rather than simply ending. Pointer-events off or
   it would eat the hover on the tiles underneath.

   Shown and hidden with display, not an opacity transition: the
   whole section is built inside the gate's visibility:hidden
   subtree, and an opacity tween queued in there never commits —
   it stayed stuck at 1 and left the haze sitting over the grid
   after expanding. Nothing to get stuck this way, and the fade
   should disappear the instant the rows below it appear anyway. */
.gallery-fade {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 130px;
  pointer-events: none;
  background: linear-gradient(to bottom,
              rgba(245, 237, 224, 0) 0%,
              rgba(245, 237, 224, 0.82) 65%,
              var(--cream) 100%);
}

.gallery-wrap.is-collapsed .gallery-fade {
  display: block;
}

.gallery-more {
  display: flex;
  justify-content: center;
  padding-top: 26px;
}

.gallery-toggle,
.story-toggle {
  font-family: var(--font-sans);
  font-size: 0.82rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--brown-mid);
  background: rgba(255, 255, 255, 0.5);
  border: 1px solid var(--gold);
  border-radius: 2px;
  padding: 13px 30px;
  cursor: pointer;
  transition: background 0.25s ease, color 0.25s ease, transform 0.2s ease;
}

.gallery-toggle:hover,
.story-toggle:hover {
  background: var(--gold);
  color: #fff;
  transform: translateY(-2px);
}

.gallery-toggle:focus-visible,
.story-toggle:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 4px;
}

/* Tiles revealed by the expand fade up in a short stagger. The class
   is removed once it has played so collapsing and reopening replays
   it; :nth-child caps the delay so the last rows are not left waiting
   several seconds. */
.gallery-wrap.just-expanded .gallery-item:nth-child(n + 10) {
  animation: gallery-in 0.45s ease both;
}

.gallery-wrap.just-expanded .gallery-item:nth-child(n + 10):nth-child(-n + 12) { animation-delay: 0.04s; }
.gallery-wrap.just-expanded .gallery-item:nth-child(n + 13):nth-child(-n + 15) { animation-delay: 0.08s; }
.gallery-wrap.just-expanded .gallery-item:nth-child(n + 16) { animation-delay: 0.12s; }

@keyframes gallery-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .gallery-wrap.just-expanded .gallery-item:nth-child(n + 10) { animation: none; }
  .gallery-toggle:hover { transform: none; }
}

.gallery-item img {
  width: 100%;
  aspect-ratio: 4 / 5;
  height: auto;
  object-fit: cover;
  display: block;
}

/* ── Hover captions ───────────────────────────────────────────
   Text comes from captions.js and is inserted by script.js. The
   band is clipped by the tile's own overflow:hidden, so it can
   simply sit below the bottom edge until the tile is hovered. */
.gallery-item {
  position: relative;
}

.gallery-caption {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 26px 14px 12px;
  font-family: var(--font-serif);
  font-size: 0.95rem;
  line-height: 1.35;
  color: #fff;
  text-align: center;
  background: linear-gradient(to top,
              rgba(28, 18, 10, 0.88) 0%,
              rgba(28, 18, 10, 0.62) 55%,
              rgba(28, 18, 10, 0) 100%);
  pointer-events: none;
}

/* Revealed with display + a keyframe rather than a transition on a
   resting transform. These spans are built while the whole page is
   still inside the gate's visibility:hidden subtree, and a transition
   set up in there never commits — captions came out stuck half open
   on some tiles and shut on others. An animation runs from its own
   keyframes every time the hover matches, so there is no carried-over
   computed value to get stuck. */
.gallery-item:hover .gallery-caption,
.gallery-item:focus-within .gallery-caption {
  display: block;
  animation: gallery-caption-in 0.28s ease both;
}

@keyframes gallery-caption-in {
  from { transform: translateY(100%); }
  to   { transform: none; }
}

/* Touch devices get no hover, so the caption is put on a tap instead
   (script.js toggles .show-caption). It used to sit on the photo
   permanently, but the longer captions ran to four or five lines on a
   phone-width tile and buried more than half the picture. A small
   corner dot marks the tiles worth tapping. */
@media (hover: none) {
  .gallery-caption {
    font-size: 0.82rem;
    padding: 20px 10px 9px;
  }

  .gallery-item.show-caption .gallery-caption {
    display: block;
    animation: gallery-caption-in 0.28s ease both;
  }

  .gallery-item.has-caption::after {
    content: '';
    position: absolute;
    right: 8px;
    bottom: 8px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.85);
    box-shadow: 0 0 4px rgba(28, 18, 10, 0.55);
  }

  .gallery-item.show-caption.has-caption::after {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .gallery-item:hover .gallery-caption,
  .gallery-item:focus-within .gallery-caption { animation: none; }
}

/* ── Family ───
   One block per side of the family, the two side by side. The sides do not
   hold the same number of photographs, so the columns are top-aligned rather
   than stretched — otherwise the shorter card grows to match the taller one
   and trails a lot of empty box under its last photograph. */
.family-groups {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  align-items: start;
  gap: 28px;
  margin-top: 12px;
}

.family-group {
  background: rgba(255, 255, 255, 0.55);
  border: 1px solid var(--tan);
  padding: 40px 28px 36px;
  text-align: center;
}

.family-group-head {
  margin-bottom: 8px;
}

.family-script {
  font-family: var(--font-script);
  font-size: 2.76rem;
  line-height: 1.3;
  color: var(--brown);
}

.family-sub {
  font-family: var(--font-sans);
  font-size: 0.864rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-soft);
  margin-top: 10px;
}

/* One photograph per row inside its column — each column is only half the
   section wide, so two tiles across came out around 170px each. Every tile
   takes the same portrait 4:5 crop; the two shots that arrived landscape were
   cropped to vertical in the file, framed on the couple, rather than left to
   the tile's centre crop, which cut one of the two people out. */
.family-photos {
  display: grid;
  grid-template-columns: 1fr;
  gap: 28px;
  margin-top: 28px;
}

.family-photo {
  margin: 0;
}

.family-photo-frame {
  overflow: hidden;
  border: 3px solid rgba(255, 255, 255, 0.9);
  box-shadow: 0 4px 22px rgba(58, 40, 24, 0.12);
  line-height: 0;
  transition: transform 0.35s ease;
}

.family-photo-frame:hover {
  transform: rotate(1deg) scale(1.02);
  position: relative;
  z-index: 2;
}

.family-photo-frame img {
  width: 100%;
  aspect-ratio: 4 / 5;
  height: auto;
  object-fit: cover;
  display: block;
}

/* A tile whose photograph is still to come. It holds a note at the same 4:5
   size so the column keeps its rhythm, and drops the frame's white mount and
   hover tilt — there is no picture under them to lift. */
.family-photo-pending {
  aspect-ratio: 4 / 5;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 20px;
  background: rgba(255, 255, 255, 0.4);
  border: 1px dashed var(--tan);
  font-family: var(--font-ui);
  font-size: 0.9rem;
  font-style: italic;
  line-height: 1.4;
  text-align: center;
  color: var(--text-soft);
}

.is-pending .family-photo-frame {
  border: 0;
  box-shadow: none;
}

.is-pending .family-photo-frame:hover {
  transform: none;
}

.family-caption {
  margin-top: 12px;
}

.family-names {
  display: block;
  font-family: var(--font-serif);
  font-size: 1.1rem;
  color: var(--brown-mid);
  line-height: 1.35;
}

.family-rel {
  display: block;
  font-family: var(--font-sans);
  font-size: 0.66rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-soft);
  margin-top: 6px;
}

/* ── Travel & Stay ─── */
/* One panel per guest category (see .stay-option in index.html); only ever
   one of them is on the page at a time, so this is a single box, not a grid. */
.travel-panel {
  background: rgba(255, 255, 255, 0.55);
  border: 1px solid var(--tan);
  padding: 28px 30px;
}

/* The practical-things box sits under whichever stay panel is showing. */
.travel-panel + .travel-panel {
  margin-top: 20px;
}

.travel-panel .panel-heading {
  font-family: var(--font-serif);
  font-weight: 600;
  color: var(--brown);
  margin-bottom: 14px;
}

.travel-panel a {
  color: var(--brown);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Sized to match .faq-q below — the two sections sit one after the other, so
   the travel copy reading smaller than the questions under it looked off. */
.travel-panel p,
.travel-panel li {
  font-size: 1.2rem;
  line-height: 1.75;
  color: var(--brown-light);
}

.travel-panel ul {
  list-style: disc;
  padding-left: 22px;
}

/* Top level carries an emoji per bullet, so the disc marker comes off and the
   emoji hangs in the indent instead — one marker per line, not two. Sub-levels
   and the lead paragraphs are untouched. */
.travel-panel .travel-list {
  list-style: none;
  padding-left: 0;
}

.travel-list > li,
.travel-panel .lead-line {
  position: relative;
  padding-left: 40px;
}

.bullet-icon {
  position: absolute;
  left: 0;
  top: 0.1em;
  font-size: 1.2rem;
  line-height: 1.6;
}

/* Sub-bullets under the airports line: hollow markers so the two levels are
   told apart at a glance, and tighter than the gap between top-level items. */
.travel-panel ul ul {
  list-style: circle;
  margin-top: 6px;
}

.travel-panel li + li {
  margin-top: 8px;
}

.travel-panel ul ul li + li {
  margin-top: 4px;
}

.travel-panel p + ul,
.travel-panel ul + p {
  margin-top: 16px;
}

/* ── FAQs ─── */
.faq-list {
  border: 1px solid var(--tan);
  overflow: hidden;
}

.faq-item {
  border-bottom: 1px solid var(--tan);
  background: rgba(255, 255, 255, 0.45);
}

.faq-item:last-child {
  border-bottom: none;
}

.faq-q {
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 22px 26px;
  font-family: var(--font-serif);
  font-size: 1.2rem;
  color: var(--brown);
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 20px;
  transition: background 0.2s;
}

.faq-q:hover {
  background: rgba(255, 255, 255, 0.4);
}

.faq-q::after {
  content: '+';
  font-size: 1.8rem;
  color: var(--gold);
  flex-shrink: 0;
  transition: transform 0.3s ease;
  line-height: 1;
}

.faq-item.open .faq-q::after {
  transform: rotate(45deg);
}

.faq-a {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.38s ease, padding 0.38s ease;
  padding: 0 26px;
}

.faq-item.open .faq-a {
  /* Generous cap — the longest answer (things to do in Goa) needs the room.
     max-height animates, so this is an upper bound, not the rendered height. */
  max-height: 1200px;
  padding: 0 26px 22px;
}

.faq-a p {
  font-size: 1rem;
  line-height: 1.8;
  color: var(--brown-light);
}

.faq-a p + p,
.faq-a ul + p {
  margin-top: 12px;
}

.faq-a ul {
  list-style: disc;
  padding-left: 20px;
  margin-top: 8px;
}

.faq-a li {
  font-size: 1rem;
  line-height: 1.8;
  color: var(--brown-light);
}

.faq-a a {
  color: var(--brown);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ── RSVP Form ─── */
.section-text strong {
  color: var(--brown);
  font-style: normal;
}

#rsvp-form {
  max-width: 680px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 22px;
}

.field-row {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 18px;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.field label,
.field-label {
  font-family: var(--font-sans);
  font-size: 1.18rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--brown);
}

.req {
  color: var(--gold);
}

/* :not() excludes the tick boxes. This selector is ID-qualified, so without
   the exclusion it outranks any class rule sizing them and they render as
   full-width text boxes that shove their labels onto three lines. */
#rsvp-form input:not([type="radio"]):not([type="checkbox"]),
#rsvp-form textarea {
  width: 100%;
  padding: 16px 19px;
  background: rgba(255, 255, 255, 0.7);
  border: 1px solid var(--tan);
  font-family: var(--font-serif);
  font-size: 1.42rem;
  color: var(--text);
  outline: none;
  resize: vertical;
  transition: border-color 0.25s;
}

#rsvp-form input:not([type="radio"])::placeholder,
#rsvp-form textarea::placeholder {
  color: var(--text-soft);
  opacity: 0.65;
}

#rsvp-form input:not([type="radio"]):focus,
#rsvp-form textarea:focus {
  border-color: var(--gold);
}

.submit-row {
  display: flex;
  justify-content: center;
  padding-top: 4px;
}

.rsvp-submit {
  padding: 22px 60px;
  background: var(--brown);
  color: var(--cream-light);
  border: none;
  font-family: var(--font-sans);
  font-size: 1.2rem;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 0.25s, transform 0.15s;
}

.rsvp-submit:hover  { background: var(--brown-mid); }
.rsvp-submit:active { transform: scale(0.98); }

.form-error {
  font-family: var(--font-ui);
  font-size: 1.18rem;
  color: #b03030;
  text-align: center;
}

.rsvp-thanks {
  text-align: center;
  padding: 32px;
  background: rgba(255, 255, 255, 0.6);
  border: 1px solid var(--tan);
}

.thanks-script {
  font-family: var(--font-signature);
  font-size: 3rem;
  color: var(--brown);
  margin-bottom: 12px;
}

.rsvp-thanks p {
  font-size: 1.42rem;
  color: var(--brown-light);
  line-height: 1.7;
}

/* ── RSVP: party blocks ─── */
/* #rsvp-people is a flex child of the form and carries its own rhythm, so
   the form's 20px gap would stack on top of the card spacing below. Zero
   the wrapper and let the cards own their margins. */
#rsvp-people {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.party-intro {
  font-family: var(--font-ui);
  font-size: 1.24rem;
  color: var(--brown-light);
  text-align: center;
  margin: 0 0 2px;
}

.person-card {
  padding: 27px 28px;
  background: rgba(255, 255, 255, 0.42);
  border: 1px solid var(--tan);
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.person-name {
  font-family: var(--font-sans);
  font-size: 1.55rem;
  letter-spacing: 0.06em;
  color: var(--brown);
  margin: 0;
}

/* Attending is a single either/or question, so these are radios — but they
   read as tick boxes, which is what the question looks like. */
.attend-choice {
  display: flex;
  flex-wrap: wrap;
  gap: 16px 36px;
}

.attend-item {
  display: flex;
  align-items: center;
  gap: 14px;
  white-space: nowrap;
  cursor: pointer;
  font-family: var(--font-serif);
  font-size: 1.5rem;
  color: var(--brown-light);
}

.attend-item input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  width: 26px;
  height: 26px;
  flex-shrink: 0;
  margin: 0;
  background: rgba(255, 255, 255, 0.8);
  border: 1px solid var(--tan-dark);
  cursor: pointer;
  position: relative;
  transition: background 0.2s, border-color 0.2s;
}

.attend-item input[type="radio"]:checked {
  background: var(--brown);
  border-color: var(--brown);
}

/* Drawn rather than a glyph so it sits identically in every browser. */
.attend-item input[type="radio"]:checked::after {
  content: '';
  position: absolute;
  left: 8px;
  top: 4px;
  width: 8px;
  height: 14px;
  border: solid var(--cream-light);
  border-width: 0 2.4px 2.4px 0;
  transform: rotate(42deg);
}

.attend-item input[type="radio"]:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.attend-item:has(input:checked) {
  color: var(--brown);
}

/* Contact and dietary answers, revealed once someone is marked attending. */
.person-details {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding-top: 4px;
  border-top: 1px solid rgba(216, 196, 164, 0.55);
}

/* ── RSVP: previously submitted answers ─── */
.rsvp-saved {
  max-width: 680px;
  margin: 0 auto 26px;
  padding: 30px 28px;
  background: rgba(255, 255, 255, 0.5);
  border: 1px solid var(--tan);
}

.saved-title {
  font-family: var(--font-sans);
  font-size: 1.18rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--brown);
  text-align: center;
  margin: 0;
}

.saved-when {
  font-family: var(--font-ui);
  font-size: 1.18rem;
  color: var(--text-soft);
  text-align: center;
  margin: 6px 0 22px;
}

.saved-person + .saved-person {
  margin-top: 18px;
  padding-top: 18px;
  border-top: 1px solid rgba(216, 196, 164, 0.55);
}

.saved-person-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
}

.saved-person-name {
  font-family: var(--font-serif);
  font-size: 1.62rem;
  color: var(--brown);
}

.saved-badge {
  flex-shrink: 0;
  padding: 5px 13px;
  font-family: var(--font-sans);
  font-size: 1.1rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  border: 1px solid;
}

.saved-badge.yes { color: var(--brown);      border-color: var(--gold); }
.saved-badge.no  { color: var(--text-soft);  border-color: var(--tan);  }

/* Label / value pairs in two columns, so the labels line up down the panel. */
.saved-details {
  display: grid;
  grid-template-columns: 10.6rem 1fr;
  gap: 5px 14px;
  margin: 11px 0 0;
}

.saved-details dt {
  font-family: var(--font-sans);
  font-size: 1.1rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-soft);
  padding-top: 0.2em;
}

.saved-details dd {
  font-family: var(--font-serif);
  font-size: 1.26rem;
  color: var(--brown-light);
  margin: 0;
  word-break: break-word;
}

.saved-note {
  margin-top: 20px;
  padding-top: 18px;
  border-top: 1px solid rgba(216, 196, 164, 0.55);
}

.saved-hint {
  font-family: var(--font-ui);
  font-size: 1.18rem;
  color: var(--text-soft);
  text-align: center;
  margin: 22px 0 0;
}

.saved-hint em {
  color: var(--brown-light);
}

/* ═══════════════════════════════════════════════════════════
   FOOTER
═══════════════════════════════════════════════════════════ */
footer {
  text-align: center;
  padding: 60px 24px 100px;
}

.footer-script {
  font-family: var(--font-signature);
  font-size: 3.28rem;
  color: var(--brown);
  line-height: 1.3;
}

.footer-detail {
  font-family: var(--font-sans);
  font-size: 0.864rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-soft);
  margin-top: 8px;
}

/* ═══════════════════════════════════════════════════════════
   DRESS GUIDE LIGHTBOX
   Each event now reads in full on the page, so the only layer left is the
   dress guide behind the "Outfit inspiration" words painted into every banner.
   One tall image, centred; never a new tab.
═══════════════════════════════════════════════════════════ */
#guide-lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(14, 8, 4, 0.96);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 58px 20px 30px;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  transition: opacity 0.4s ease, visibility 0.4s ease;
}

/* Shared with the RSVP modal. */
.modal-closed {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.modal-open {
  opacity: 1;
  visibility: visible;
  pointer-events: all;
}

/* Was a round ×. "Return" says what actually happens — the guide is a
   full-screen layer over the map and the guest is going back to the event
   they opened it from, not dismissing a dialogue. Left in the corner the
   × was in: it is the only control on the layer, and a guest who has just
   tapped into a full-screen image looks to that corner for the way out.

   Set in the label face the nav and the scroll cue use, so it reads as part
   of the site rather than as browser furniture. */
#guide-close {
  position: fixed;
  top: 14px;
  right: 18px;
  display: inline-flex;
  align-items: center;
  gap: 0.62em;
  /* asymmetric: the tracking below leaves a gap after the N, so the right
     side needs less padding than it looks like it does */
  padding: 11px 17px 11px 16px;
  background: rgba(14, 8, 4, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 999px;
  color: rgba(255, 255, 255, 0.78);
  font-family: var(--font-sans);
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  z-index: 10;
  transition: color 0.2s, background 0.2s, border-color 0.2s;
}

/* Drawn rather than set as a glyph, the same way the scroll cue's chevron
   is: an arrow character would come from whichever fallback face happens to
   carry it, and Cinzel does not. border-left + border-bottom turned 45°
   points left. Sized in em so it tracks the label. */
#guide-close .guide-back-arrow {
  flex: none;
  width: 0.8em;
  height: 0.8em;
  border-left: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
}

#guide-close:hover,
#guide-close:focus-visible {
  color: #fff;
  background: rgba(14, 8, 4, 0.8);
  border-color: rgba(255, 255, 255, 0.42);
  outline: none;
}

#guide-img {
  display: block;
  width: 100%;
  max-width: 620px;
  height: auto;
  margin: auto;
  border-radius: 3px;
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.55);
  animation: modal-up 0.45s var(--ease-spring) forwards;
}

/* Where the whole guide fits on screen, keep it whole rather than making
   the guest scroll a 620px column. */
@media (min-height: 780px) {
  #guide-img {
    width: auto;
    max-width: 100%;
    max-height: 84vh;
    object-fit: contain;
  }
}

@keyframes modal-up {
  0%   { opacity: 0; transform: translateY(36px) scale(0.97); }
  100% { opacity: 1; transform: translateY(0)    scale(1); }
}

/* ═══════════════════════════════════════════════════════════
   UTILITY
═══════════════════════════════════════════════════════════ */
.hidden {
  display: none !important;
}

/* ═══════════════════════════════════════════════════════════
   RESPONSIVE
═══════════════════════════════════════════════════════════ */
@media (max-width: 768px) {
  .gate-inner { padding: 40px 28px; }
  /* Under this width the clip is only a few hundred pixels tall and its top
     margin scales down with it — type small enough to sit inside it would be
     too small to read. The greeting comes out of the frame and takes its own
     line above it, and --invite-line-h hands the clip's budget the height
     back so the scroll cue still lands on the first screen. */
  :root { --invite-line-h: 90px; }
  #invite-greeting {
    height: auto;
    top: 0;
    min-height: var(--invite-line-h);
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 20px 16px 14px;
    font-size: 1.38rem;
  }
  .invite-line { font-size: 0.72em; }
  .gate-names { font-size: 3rem; }   /* Pinyon — see the size note on --font-signature */

  #couple-photo { width: 78%; }

  /* No room for two columns here: island above, its banner below — and the
     trails come back to the middle with them. */
  #map {
    --map-w:   96vw;
    --map-gap: 6px;
  }

  .event-row,
  .map-trail { grid-template-columns: minmax(0, 1fr); }

  /* Stacked, both pages want the same full-width column. */
  .event-island {
    width: 100%;
    margin: 0;
  }

  /* Stacked, the gap between a banner and the next island is only ~85px, so
     the trail is scaled to fit inside it and barely pulled in — any more and
     the flourish crosses the banner's bottom artwork. */
  .map-trail     { margin: -10px 0; }
  .map-trail img { width: 54%; }

  .family-groups { grid-template-columns: 1fr; gap: 24px; }
  .family-photos { gap: 24px; }
  .family-group   { padding: 32px 18px 28px; }
  .family-names   { font-size: 1rem; }
  .story-prose { font-size: 1.32rem; line-height: 1.82; margin-bottom: 34px; }

  .photo-gallery { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  /* Two columns here, so the preview cutoff moves to keep whole rows. */
  .gallery-wrap.is-collapsed .gallery-item:nth-child(n + 10) { display: grid; }
  .gallery-wrap.is-collapsed .gallery-item:nth-child(n + 9)  { display: none; }
  .gallery-wrap.just-expanded .gallery-item:nth-child(n + 9) { animation: gallery-in 0.45s ease both; }
  .gallery-fade { height: 90px; }
  .gallery-toggle,
  .story-toggle { font-size: 0.76rem; padding: 12px 22px; }

  .two-col { grid-template-columns: 1fr; }

  .person-card { padding: 22px 20px; }
  .rsvp-saved  { padding: 26px 20px; }

  /* Name and badge side by side get cramped once the name wraps. */
  .saved-person-head {
    flex-direction: column;
    align-items: flex-start;
    gap: 7px;
  }

  /* A 8.5rem label column leaves too little for the value on a phone. */
  .saved-details { grid-template-columns: 1fr; gap: 2px; }
  .saved-details dd { margin-bottom: 8px; }

  .wear-hotspot::after { inset: -16px -22px; }
}

@media (max-width: 480px) {
  main { padding: 0 12px; }
  .gate-inner { padding: 32px 18px; }
  /* smaller type needs less room, and the landing screen is tightest here */
  :root { --invite-line-h: 84px; }
  #invite-greeting { font-size: 1.2rem; }
  .content-section { padding: 30px 0; }
  .section-title   { font-size: 3rem; }
  .family-script   { font-size: 2.28rem; }
  .gate-names      { font-size: 2.64rem; }
  .thanks-script   { font-size: 2.36rem; }
  .footer-script   { font-size: 2.54rem; }
}


/* ═══════════════════════════════════════════════════════════
   RSVP MODAL
   The form is long, so unlike the event modal this one is a scrollable
   cream panel on a dark backdrop rather than a centred image.
═══════════════════════════════════════════════════════════ */
#rsvp-modal {
  position: fixed;
  inset: 0;
  z-index: 210;
  background: rgba(14, 8, 4, 0.9);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: clamp(16px, 4vh, 48px) clamp(12px, 3vw, 32px);
  overflow-y: auto;
  transition: opacity 0.35s ease, visibility 0.35s ease;
}

#rsvp-modal-panel {
  position: relative;
  width: 100%;
  max-width: 780px;
  margin: auto;   /* centres a short panel, top-aligns a tall one */
  padding: 44px clamp(20px, 4vw, 44px) 40px;
  background: var(--cream);
  border: 1px solid var(--tan);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
  max-height: calc(100vh - 2 * clamp(16px, 4vh, 48px));
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Matches the entrance of the event modal's banner. */
.modal-open #rsvp-modal-panel {
  animation: rsvp-panel-in 0.45s var(--ease-spring) both;
}

@keyframes rsvp-panel-in {
  from { opacity: 0; transform: translateY(18px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}

#rsvp-modal-close {
  position: absolute;
  top: 8px;
  right: 14px;
  background: none;
  border: none;
  color: var(--brown-light);
  font-size: 2.64rem;
  line-height: 1;
  cursor: pointer;
  z-index: 2;
  transition: color 0.2s;
}

#rsvp-modal-close:hover {
  color: var(--brown);
}

#rsvp-modal-close:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

/* "RSVP" as script capitals is near-unreadable — same reasoning as the
   section headings, so the same display caps serif. */
#rsvp-modal-title {
  font-family: var(--font-sans);
  font-size: clamp(2.15rem, 4.6vw, 2.9rem);
  font-weight: 500;
  letter-spacing: 0.12em;
  color: var(--brown);
  text-align: center;
  margin: 0;
}

#rsvp-modal-sub {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: 1.48rem;
  line-height: 1.7;
  color: var(--brown-light);
  text-align: center;
  margin: 12px 0 28px;
}

/* Sits with the dietary field, so it repeats once per attending guest — kept
   to a quiet hint rather than a callout, which at three-per-party would shout. */
.diet-hint {
  margin: -1px 0 4px;
  font-family: var(--font-ui);
  font-style: italic;
  font-size: 1.14rem;
  line-height: 1.55;
  color: var(--text-soft);
}

.diet-hint strong {
  color: var(--brown-light);
  font-style: normal;
  font-weight: 700;
}

#rsvp-modal-sub strong {
  color: var(--brown);
  font-style: normal;
}

@media (max-width: 768px) {
  #rsvp-modal { padding: 0; }

  /* Full-bleed on a phone: a floating card wastes width the form needs. */
  #rsvp-modal-panel {
    max-width: none;
    min-height: 100%;
    max-height: none;
    border: none;
    padding: 40px 18px 32px;
  }
}
