/* ==========================================================================
   Sarah & Spencer — Save the Date
   Quiet luxury: parchment, olive, stone, a little antique gold.
   Mobile first. One motion language: slow, eased, physical.
   ========================================================================== */

:root {
  /* --- paper & ink --- */
  --paper:        #f7f2e9;
  --paper-warm:   #f2ebdd;
  --paper-deep:   #ece3d2;
  --card:         #fbf7f0;

  /* --- measured hero palette (see spec) --- */
  --ink:          #45423e;   /* headings, names, input values — 9.8:1 */
  --ink-body:     #443f38;   /* body copy — 9.6:1 */
  --ink-soft:     #625b50;   /* secondary copy, field labels — 6.0:1 */
  --date:         #8f8981;   /* hero date line */
  --stone:        #9c9382;   /* hero location line */
  --leaf:         #9e9c84;   /* botanical fills */
  --rule:         #cdbca6;   /* hairline rules */
  --gold:         #c49f6c;   /* ampersand + accents */
  --gold-deep:    #a8834f;
  --olive:        #7f8a6b;

  /* Darker siblings of --stone and --gold-deep, for text set on paper.
     The lighter pair stays where it belongs: the hero lettering (whose
     colours are specified exactly) and non-text botanical artwork.

     These were originally set to the lightest value that still cleared
     4.5:1 on paper, which is the wrong target for a Garamond at 300–400:
     a thin, low-x-height serif needs real headroom above the minimum or
     it greys out. Every one of these now clears 5.4:1, measured against
     --paper (#f7f2e9), the warmest point on the page. */
  --stone-text:   #6a6152;   /* 5.5:1 — hints, captions, quiet meta */
  --gold-text:    #7a5c33;   /* 5.5:1 — eyebrows, accents, the send button */
  --err:          #8c4f39;   /* 5.6:1 — validation messages */

  /* --- type -----------------------------------------------------------
     Two faces, one family tree. Cormorant Garamond is a *display* cut:
     its x-height is roughly 0.36em against EB Garamond's 0.48em, so at
     the body sizes this page was using it rendered with the effective
     size of ~12px text, in a 300 weight, in a light ink. That — not the
     colour and not the layout — is why the page was hard to read.

     Cormorant now does only what it is good at: the large display lines.
     EB Garamond, a true text Garamond drawn for continuous reading, takes
     every piece of copy you actually have to read: paragraphs, form
     labels, inputs, buttons, hints, errors and captions. The two are
     close cousins, so the page looks like itself — it just resolves. */
  --font-display: 'Cormorant Garamond', 'Iowan Old Style', Georgia, 'Times New Roman', serif;
  --font-text:    'EB Garamond', 'Iowan Old Style', Georgia, 'Times New Roman', serif;

  /* Minimum legible sizes. Nothing on this page may go below --t-micro,
     and --t-micro is only ever allowed for tracked uppercase labels of
     three words or fewer. Body copy starts at --t-body. */
  --t-micro: .78rem;   /* 12.5px — tracked caps only */
  --t-small: .95rem;   /* 15.2px — hints, captions, notes */
  --t-body:  1.09rem;  /* 17.4px — paragraphs */

  /* --- motion --- */
  --ease-out:    cubic-bezier(.16, 1, .3, 1);
  --ease-inout:  cubic-bezier(.62, .02, .26, 1);
  --ease-soft:   cubic-bezier(.25, .8, .28, 1);
  /* the reveal easing: a long, slow settle with no overshoot */
  --ease-reveal: cubic-bezier(.22, 1, .36, 1);
  /* the hero-nav's own curve: a light overshoot past the resting point.
     Drives the hamburger's fold into an X and the dropdown's arrival, so
     the two read as one gesture rather than two unrelated transitions.
     --ease-bounce-strong is the same shape pushed further, reserved for
     the hamburger's middle bar (it has the least distance to travel and
     reads as inert next to the outer two without the extra snap). */
  --ease-bounce:        cubic-bezier(.5, .85, .25, 1.1);
  --ease-bounce-strong: cubic-bezier(.5, .85, .25, 1.8);

  /* --- hero nav ---
     One thin border standing in for both controls' former glow — see
     §1.5 — so the hamburger and RSVP read as one family. */
  --nav-border: rgba(255, 255, 255, .45);

  /* --- RSVP pill ---
     The warm taupe/khaki fill for the RSVP button (§1.5), darkened just
     enough below its reference swatch that white text on it still clears
     4.5:1. */
  --rsvp-light: #9c8863;
  --rsvp:       #7d6a4e;
  --rsvp-deep:  #5f4e38;

  /* How far down the hero the painting starts dissolving into paper. One
     value in every configuration now — there is no scroll hand-off to hold
     it back for, so the painting carries the same soft foot always. */
  --hero-fade-start: 76%;

  /* 0 → 1: how much of the ivory at the foot of the painting is present.
     Always 1. It used to be handed to the scroll under html.js so the
     ivory arrived as you moved; that was part of what made the scroll down
     from the hero feel wrong, and it is gone. Kept as a variable because
     the dissolve and its warm bloom both read it. */
  --hero-ivory: 1;

  /* How the 4:3 artwork is framed inside a viewport that is never 4:3.
     One token, because the still, the video and the three parallax planes
     must crop identically or the planes slide out of register with the
     picture they were cut from — see §8 for what the numbers are doing. */
  --hero-framing: 50% 46%;

  /* Where the name/date/actions group centres in the viewport. Deliberately
     below middle: it leaves the clouds and the mountain line — the only
     things actually moving in the loop — completely unobstructed, and sets
     the copy over the lake, which is the calmest, most even part of the
     painting and so the most readable. */
  --hero-anchor: 59%;

  /* Total scroll runway of the hero. The default is exactly one screen —
     no sticky hand-off — because that is the only version guaranteed to
     behave everywhere. The extra runway is opted into below, gated on the
     same feature that makes sticky survive our overflow rules. */
  --hero-runway: 100svh;

  /* Inline glyphs, drawn once and tinted by whatever masks them. */
  --ico-alert: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9.5'/%3E%3Cpath d='M12 7.2v5.6M12 16.6h.01'/%3E%3C/svg%3E");
  --ico-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 12.5l5 5 10-11'/%3E%3C/svg%3E");

  --gutter: clamp(1.25rem, 6vw, 2.5rem);
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);

  /* --- spacing scale ---------------------------------------------------
     Not a repo-wide constraint — most spacing on this page is a fluid
     clamp() tuned to its own section, and that's earned its keep. This is
     for the handful of static, same-role gaps where nothing warrants an
     independent value: the frosted-glass card family below (§11.5/11.6)
     had picked three slightly different paddings — 1.6rem, 1.7rem, 1.5rem
     — for what is visually the same card. New shared spots should reach
     for one of these before inventing a fourth. */
  --space-2xs: .5rem;    /*  8px */
  --space-xs:  .7rem;    /* 11.2px */
  --space-sm:  1rem;     /* 16px */
  --space-md:  1.5rem;   /* 24px */
  --space-lg:  2rem;     /* 32px */
  --space-xl:  3rem;     /* 48px */
}

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

/* `overflow-x: hidden` turns an element into a scroll container, which
   silently kills `position: sticky` for everything inside it — that is what
   the hero hand-off rides on. `clip` does the same job without creating one.
   The `hidden` line stays first as the fallback for engines that predate
   `clip`; those engines also skip the sticky runway entirely (see below), so
   they get a plain one-screen hero rather than a broken one. */
html {
  -webkit-text-size-adjust: 100%;
  overflow-x: hidden;
  overflow-x: clip;
}

body {
  font-family: var(--font-text);
  font-size: var(--t-body);
  line-height: 1.6;
  color: var(--ink-body);
  background: var(--paper);
  overflow-x: hidden;
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* Scroll is held only while the envelope is on screen. */
html.is-sealed, html.is-sealed body { overflow: hidden; height: 100%; }

img, svg, video { display: block; max-width: 100%; }
.svg-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

/* A 1.5px hairline is not a focus ring, it is a rumour. 2.5px in the
   darkest accent on the palette is visible on paper, on the painting and
   on the charcoal envelope alike, which is the whole job. */
:focus-visible {
  outline: 2.5px solid var(--gold-text);
  outline-offset: 3px;
  border-radius: 2px;
}

/* Keyboard users landed on the envelope and then had to tab the whole
   hero to reach the only thing on the page that asks for input. */
.skip-link {
  position: absolute;
  top: 0;
  left: 50%;
  z-index: 200;
  padding: .85rem 1.6rem;
  border: 1px solid var(--gold-text);
  border-radius: 0 0 .55rem .55rem;
  background: var(--card);
  font-family: var(--font-text);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: .2em;
  text-indent: .2em;
  text-transform: uppercase;
  color: var(--gold-text);
  text-decoration: none;
  transform: translate(-50%, -110%);
  transition: transform .22s var(--ease-out);
}
.skip-link:focus { transform: translate(-50%, 0); }


/* ==========================================================================
   1 · ENVELOPE
   ========================================================================== */

.env-scene {
  position: fixed;
  inset: 0;
  z-index: 90;
  overflow: hidden;
  /* The footage's own studio backdrop, not black — so there is nothing
     for the eye to catch between this and the video's first frame. */
  background: var(--paper-deep);
  opacity: 1;
  transition: opacity .85s var(--ease-soft);
}
/* Without JS there is nothing to open, so never show the envelope. */
html:not(.js) .env-scene { display: none; }
.env-scene.is-gone { opacity: 0; pointer-events: none; }

.env-stage { position: absolute; inset: 0; }

.env {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  padding: 0;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform .3s var(--ease-soft);
}
/* The button fills the screen, so a default outline would draw off-canvas
   and never be seen — pull it back onto the visible edge instead. */
.env:focus-visible { outline-offset: -6px; }
.env.is-open { cursor: default; }
/* The footage is the affordance; this only confirms a tap registered. */
.env.is-pressed { transform: scale(.985); }

.env-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  background: var(--paper-deep);
}

/* --- the whole envelope, whichever clip ends up on screen ---------------
   Two clips, two native shapes: the landscape studio shot is ~1.82:1 (see
   assets/video/README.md), the portrait take save-the-date.js prefers on
   phones is 9:16. `cover` crops brutally whenever the screen's own shape
   runs the opposite way of whichever clip is loaded — on a phone showing
   the landscape clip barely a quarter of the width survives; on the rare
   screen showing the portrait clip sideways (a landscape phone under the
   767px mobile threshold, or a fallback after the portrait file itself
   fails — see setEnvelopeSource/onError in save-the-date.js) most of the
   height goes the same way. `.is-mobile-cut` on `.env-scene` is JS's own
   record of which clip is actually loaded (not just screen width, so the
   fallback path gets this right too — see setEnvelopeSource), and each
   rule below only fires for the clip it actually applies to, on the axis
   that actually crops it. Both switch to `contain`, letterboxed on the
   scene's own paper tone (`.env-video`'s background, already set above)
   — the same tone the clip's own tail fades to, so the bars read as
   composed rather than broken. */
@media (max-aspect-ratio: 4/3) {
  .env-scene:not(.is-mobile-cut) .env-video { object-fit: contain; }
}
@media (orientation: landscape) {
  .env-scene.is-mobile-cut .env-video { object-fit: contain; }
}


/* ==========================================================================
   1.5 · HERO NAV
   One component, one markup partial, shared by both pages: a hamburger
   (left) that opens a small glass menu, and an RSVP button (right) that
   needs no script at all. Fixed to the top of the viewport and visible
   from the very first frame, straight over each page's own artwork, and
   stays on through the rest of the scroll too — there is nothing to gate
   here, so it is simply always there. index.html wires the toggle up with
   assets/js/hero-nav.js; travel/index.html has its own copy of the same
   handful of lines in the "Hero nav" block of travel.js, so nothing about
   either page depends on the other's script.

   Neither control is a plate over the artwork, but they no longer lean on
   a soft glow to prove it either — a large radial halo read as cheap once
   the rest of the page settled on hard, deliberate edges. Both now carry
   their own contained fill (frosted glass on the hamburger, a solid taupe
   gradient on RSVP) plus one thin --nav-border hairline, so the separation
   from whatever passage of sky, peak or paper sits behind them comes from
   the control itself, not a bloom bleeding past its own bounds.

   z-index sits below the save-the-date's envelope (90): while the
   envelope is up this is fully covered, so it is only ever seen once the
   guest has already opened it. travel/index.html has no envelope, but
   keeps the same value for the one component shared between them. */

.hero-nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 70;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  padding: calc(var(--safe-t) + .85rem)
           calc(var(--gutter) - .35rem + var(--safe-r)) 0
           calc(var(--gutter) - .35rem + var(--safe-l));
  pointer-events: none;
}

.hero-nav-toggle,
.hero-nav-rsvp {
  position: relative;
  pointer-events: auto;
}

.hero-nav-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--nav-border);
  background: rgba(251, 247, 240, .58);
  box-shadow: 0 1px 3px rgba(46, 38, 26, .16);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
          backdrop-filter: blur(14px) saturate(160%);
  cursor: pointer;
  color: var(--ink);
  transition: color .3s var(--ease-soft), background .3s var(--ease-soft), border-color .3s var(--ease-soft);
}
.hero-nav-toggle:hover,
.hero-nav-toggle:focus-visible,
.hero-nav-toggle.is-open {
  color: var(--gold-text);
  background: rgba(251, 247, 240, .84);
  border-color: rgba(255, 255, 255, .7);
}

.hero-nav-toggle-bars {
  position: relative;
  display: block;
  width: 20px;
  height: 13px;
}
.hero-nav-toggle-bars span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 1.6px;
  border-radius: 1px;
  background: currentColor;
}
.hero-nav-toggle-bars span:nth-child(1) {
  top: 0;
  transition: top .3s var(--ease-bounce), transform .3s var(--ease-bounce);
}
.hero-nav-toggle-bars span:nth-child(2) {
  top: 5.7px;
  transition: opacity .2s var(--ease-bounce-strong), transform .3s var(--ease-bounce-strong);
}
.hero-nav-toggle-bars span:nth-child(3) {
  top: 11.4px;
  transition: top .3s var(--ease-bounce), transform .3s var(--ease-bounce);
}
/* Bars fold into an X once open, the standard hamburger-to-close tell.
   The full 315deg/135deg turns (rather than the shorter -45deg/135deg
   path) are deliberate: paired with the overshoot easings above, the
   extra travel is what makes the fold read as a snap instead of a slide. */
.hero-nav-toggle.is-open .hero-nav-toggle-bars span:nth-child(1) { top: 5.7px; transform: rotate(315deg); }
.hero-nav-toggle.is-open .hero-nav-toggle-bars span:nth-child(2) { opacity: 0; transform: rotate(45deg); }
.hero-nav-toggle.is-open .hero-nav-toggle-bars span:nth-child(3) { top: 5.7px; transform: rotate(135deg); }

/* RSVP: a solid taupe pill (see --rsvp tokens) rather than the glass fill
   the hamburger and dropdown share — it is the page's one call to action,
   so it is allowed its own color instead of blending into the family.
   The label fades and the trailing chip expands into a full-width "go"
   affordance on hover/focus, the same slide-reveal as the site's other
   flow-btn pills (§9), just packaged for this pill's own shape. */
.hero-nav-rsvp {
  display: inline-flex;
  align-items: center;
  overflow: hidden;
  min-width: 8.25rem;
  height: 44px;
  padding: 0 1.35rem;
  border-radius: 999px;
  font-family: var(--font-text);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: .17em;
  text-indent: .17em;
  text-transform: uppercase;
  color: var(--card);
  background: linear-gradient(135deg, var(--rsvp-light) 0%, var(--rsvp) 55%, var(--rsvp-deep) 100%);
  border: 1px solid var(--nav-border);
  box-shadow: 0 1px 3px rgba(46, 38, 26, .22), inset 0 1px 0 rgba(255, 255, 255, .22);
  text-decoration: none;
  transition: border-color .3s var(--ease-soft), transform .2s var(--ease-bounce);
}
.hero-nav-rsvp:hover,
.hero-nav-rsvp:focus-visible { border-color: rgba(255, 255, 255, .7); }
.hero-nav-rsvp:active { transform: scale(.96); }

.hero-nav-rsvp-label {
  margin-right: 1.7rem;
  transition: opacity .35s var(--ease-soft);
}
.hero-nav-rsvp:hover .hero-nav-rsvp-label,
.hero-nav-rsvp:focus-visible .hero-nav-rsvp-label { opacity: 0; }

.hero-nav-rsvp-icon {
  position: absolute;
  top: 4px;
  right: 4px;
  bottom: 4px;
  display: grid;
  width: 26px;
  place-items: center;
  border-radius: 999px;
  background: rgba(255, 255, 255, .2);
  color: var(--card);
  transition: width .4s var(--ease-bounce);
}
.hero-nav-rsvp-icon svg { width: 15px; height: 15px; }
.hero-nav-rsvp:hover .hero-nav-rsvp-icon,
.hero-nav-rsvp:focus-visible .hero-nav-rsvp-icon { width: calc(100% - 8px); }

/* The dropdown menu is allowed to read as a solid card once open — unlike
   the two controls above, it is a brief, guest-triggered interaction, not
   something permanently laid over the painting. Same glass-pill treatment
   the old §1.5 nav used, so the two still feel like one family. The
   overshoot easing is shared with the hamburger's own fold (see
   --ease-bounce above), so opening the menu and folding the icon that
   opened it read as the same gesture rather than two unrelated ones. */
.hero-nav-menu {
  position: absolute;
  top: calc(var(--safe-t) + 3.55rem);
  left: calc(var(--gutter) - .35rem + var(--safe-l));
  width: max-content;
  max-width: calc(100vw - 2 * (var(--gutter) - .35rem) - var(--safe-l) - var(--safe-r));
  padding: 1rem 1.35rem;
  border-radius: 1rem;
  background: rgba(251, 247, 240, .92);
  border: 1px solid rgba(120, 104, 80, .14);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .55),
    0 22px 44px -26px rgba(93, 74, 44, .42),
    0 4px 16px -10px rgba(93, 74, 44, .18);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
          backdrop-filter: blur(18px) saturate(160%);
  pointer-events: auto;
  opacity: 0;
  transform: translateY(-10px) scale(.94);
  transform-origin: top left;
  transition: opacity .3s var(--ease-bounce), transform .34s var(--ease-bounce);
}
.hero-nav-menu.is-open {
  opacity: 1;
  transform: translateY(0) scale(1);
}

.hero-nav-menu-links { list-style: none; }
.hero-nav-menu-links li + li { margin-top: .3rem; }
.hero-nav-menu-links a {
  display: block;
  padding: .5rem 0;
  font-family: var(--font-text);
  font-size: var(--t-small);
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--stone-text);
  text-decoration: none;
  white-space: nowrap;
  transition: color .3s var(--ease-soft);
}
.hero-nav-menu-links a:hover,
.hero-nav-menu-links a:focus-visible { color: var(--gold-text); }
.hero-nav-menu-links a[aria-current="page"] { color: var(--gold-text); }

@media (prefers-reduced-motion: reduce) {
  .hero-nav-menu,
  .hero-nav-toggle-bars span,
  .hero-nav-rsvp-label,
  .hero-nav-rsvp-icon { transition: none !important; }
}


/* ==========================================================================
   2 · HERO
   ========================================================================== */

.page {
  opacity: 1;
  transition: opacity 1.1s var(--ease-soft), transform 1.4s var(--ease-out);
}
html.is-sealed .page { opacity: 0; transform: scale(1.035); }

/* .hero is only a scroll runway. Nothing is painted on it directly —
   .hero-viewport is the pane that actually holds the picture, and it
   sticks to the top of the screen for the extra 18svh so the copy can
   lift away and the paper can climb over a painting that is standing
   still. Sticky (not fixed, not transformed) keeps iOS Safari honest
   when the URL bar collapses. */
.hero {
  position: relative;
  height: var(--hero-runway);
  min-height: 30rem;
  isolation: isolate;
}
@supports not (height: 100svh) { :root { --hero-runway: 100vh; } }

/* There is no sticky hand-off any more. The hero was 118svh of runway with
   a pinned pane inside it: the painting held still while the copy lifted
   away, an ivory wash climbed over it and three depth planes slid against
   each other. It read as the page refusing to move. The hero is one screen
   now and it scrolls like one, so the transition into the paper is just
   the dissolve the painting already carries at its foot. */

/* Exactly the visible screen, and deliberately no min-height: a sticky pane
   that is as tall as the block it sticks inside has nowhere to stick, which
   is what a shared 30rem floor did to short landscape windows. .hero keeps
   the floor; the pane just tracks the viewport. */
.hero-viewport {
  position: relative;
  height: 100svh;
  overflow: hidden;
  isolation: isolate;
}
@supports not (height: 100svh) { .hero-viewport { height: 100vh; } }

.hero-media {
  position: absolute;
  inset: 0;
  overflow: hidden;
  /* Warm, never black: if decode is slow this is what shows through. */
  background: var(--paper-deep);
  /* The painting dissolves into the paper instead of meeting it on an edge.
     Stops are fractions of the remaining height, so the ramp always finishes
     exactly at the bottom no matter where --hero-fade-start is set. A plain
     two-stop gradient bands and shows a start line partway up the artwork. */
  --fade-span: calc(100% - var(--hero-fade-start));
  --fade: linear-gradient(to bottom,
      #000 0%,
      #000              var(--hero-fade-start),
      rgba(0,0,0,.985)  calc(var(--hero-fade-start) + .09 * var(--fade-span)),
      rgba(0,0,0,.945)  calc(var(--hero-fade-start) + .18 * var(--fade-span)),
      rgba(0,0,0,.865)  calc(var(--hero-fade-start) + .28 * var(--fade-span)),
      rgba(0,0,0,.755)  calc(var(--hero-fade-start) + .38 * var(--fade-span)),
      rgba(0,0,0,.620)  calc(var(--hero-fade-start) + .48 * var(--fade-span)),
      rgba(0,0,0,.470)  calc(var(--hero-fade-start) + .58 * var(--fade-span)),
      rgba(0,0,0,.320)  calc(var(--hero-fade-start) + .68 * var(--fade-span)),
      rgba(0,0,0,.175)  calc(var(--hero-fade-start) + .79 * var(--fade-span)),
      rgba(0,0,0,.060)  calc(var(--hero-fade-start) + .89 * var(--fade-span)),
      rgba(0,0,0,0)     100%);
  -webkit-mask-image: var(--fade);
          mask-image: var(--fade);
}

/* The artwork is 4:3 and the viewport almost never is, so object-position
   is doing real compositional work at every size — see the breakpoints
   under §2.1. No transform, no scale, no Ken Burns: the loop already
   contains its own camera and a second one would fight it. */
.hero-still,
.hero-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--hero-framing);
}
.hero-still,
.hero-video { background: var(--paper-deep); }
.hero-video { opacity: 0; transition: opacity 1.2s var(--ease-soft); }
.hero-video.is-playing { opacity: 1; }

/* Warm scrim: lifts the lettering off the water without a panel.
   Deliberately not a rectangle and not grey — it is a soft ivory bloom
   centred on the copy, so it reads as light in the painting rather than
   as a layer over it. It tracks --hero-anchor. */
/* Sized and weighted for the content group as it stands. When the two
   action buttons were removed the group got shorter, so — being centred on
   --hero-anchor — it slid down into the mountain haze, and the date and
   place lines are pale artwork whose colours are fixed by the reference and
   cannot be darkened. The scrim is the only lever, so it carries more: a
   wider, taller ellipse at higher opacity. It is still a bloom of light in
   the painting rather than a panel over it. */
.hero-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(ellipse 76% 34% at 50% var(--hero-anchor),
      rgba(252,250,246,.76) 0%,
      rgba(252,250,246,.62) 38%,
      rgba(252,250,246,.36) 62%,
      rgba(252,250,246,.14) 82%,
      rgba(252,250,246,0)   100%);
}

/* A whisper of edge vignette. Warm, not dark — it stops the painting
   from feeling like it was cut out with scissors at the screen edge. */
.hero-viewport::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(ellipse 92% 80% at 50% 46%,
      rgba(120,104,80,0)    62%,
      rgba(120,104,80,.045) 86%,
      rgba(120,104,80,.085) 100%);
}

/* Fallback dissolve for engines without mask-image, plus a soft warm bloom
   at the join in every engine. Like the mask above it, it is held at zero
   until the guest scrolls wherever something else can bring it in —
   --hero-ivory is what decides that, and it is 1 by default. */
.hero-dissolve {
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 34%;
  z-index: 2;
  pointer-events: none;
  /* full strength where this layer *is* the dissolve; halved where the
     mask is doing the real work and this is only the warm bloom */
  --dissolve-peak: 1;
  background: linear-gradient(to bottom,
      rgba(247,242,233,0) 0%,
      rgba(247,242,233,.03) 22%,
      rgba(247,242,233,.10) 38%,
      rgba(247,242,233,.24) 52%,
      rgba(247,242,233,.45) 64%,
      rgba(247,242,233,.68) 75%,
      rgba(247,242,233,.86) 84%,
      rgba(247,242,233,.96) 92%,
      rgba(247,242,233,1)  100%);
}
@supports (mask-image: linear-gradient(#000, #000)) or (-webkit-mask-image: linear-gradient(#000, #000)) {
  .hero-dissolve { --dissolve-peak: .5; }
}
.hero-dissolve { opacity: calc(var(--dissolve-peak) * var(--hero-ivory)); }

/* .hero-wash and the three .hero-layer planes are gone with the parallax —
   the dissolve at the foot of the painting does the whole hand-off now. The
   plane artwork and its generator were deleted with them. */

/* --- hero content group --------------------------------------------------
   Names, divider, date/place and the two actions travel as one block,
   centred on --hero-anchor. Grouping them is what keeps the spacing
   between the lettering and the buttons constant at every viewport
   height instead of stretching with it. */
.hero-content {
  position: absolute;
  left: 50%;
  top: var(--hero-anchor);
  z-index: 3;
  width: min(69.27%, 1003px);
  max-width: calc(100% - 2 * var(--gutter));
  display: flex;
  flex-direction: column;
  align-items: center;
  transform: translate(-50%, -50%);
}

/* --- hero typography (see spec §9) --------------------------------------
   Reference composition 1448 × 1086. Title group width min(69.27%, 1003px),
   divider 32.10% of that, metadata 23.93%. These internal proportions are
   traced from the reference and must not drift; only the group's position
   on the screen is ours to place. */
.hero-type {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  pointer-events: none;
}

/* The three hero pieces are artwork traced from the painted reference, so
   they are sized by width alone and always keep their own aspect ratio.
   Widths are the measured reference proportions: the title spans the whole
   1003px composition, the divider is 322/1003 of it, the metadata 240/1003.
   Nothing here may set fill, stroke, filter or a text property — that would
   fight the artwork. There is deliberately no shadow or glow: the reference
   has none, and .hero-scrim already lifts the sky behind the lettering. */
.hero-title {
  display: block;
  width: 100%;
  line-height: 0;
}
.hero-title-art {
  display: block;
  width: 100%;
  height: auto;
}

.hero-divider {
  display: block;
  width: 32.1%;            /* 322 / 1003 */
  height: auto;
  margin-top: clamp(14px, 2.14vw, 31px);
}

.hero-meta {
  width: 100%;
  margin-top: clamp(10px, 1.45vw, 21px);
  line-height: 0;
}
.hero-meta-art {
  display: block;
  width: 23.93%;           /* 240 / 1003 */
  height: auto;
  margin-inline: auto;
}

/* --- hero actions ---
   No local glow behind these: .hero-scrim already blooms across the whole
   content group, and a second ellipse here sat in its own stacking context
   and washed out the names above it. One readability layer, not two. */
/* The hero action row and its booking note were removed with the buttons —
   the lodging ask now lives on the last screen of the form. The flow button
   below is still the site's one button component; the form uses it. */

/* --- §2.4 THE FLOW BUTTON -------------------------------------------------
   One button component for the whole site. On rest it is a quiet pill of
   printed stock. On engagement an ink circle floods out from the centre,
   the label steps aside for an arrow arriving from the left, the outgoing
   arrow leaves stage right, and the pill relaxes from a 100px radius into
   a 12px card. Four things move on four different curves, which is what
   makes it read as a gesture rather than a state change.

   Ported by hand from the React/Tailwind original — this site is static
   HTML with no build step, so the classes are real CSS instead. Behaviour
   is identical; only the palette changed, from #111 to the page's own
   charcoal and antique gold.

   Variants set three custom properties and inherit everything else:
     --fb-ink     the colour that floods in
     --fb-on-ink  the label colour once it has
     --fb-line    the resting hairline
   -------------------------------------------------------------------------- */
.flow-btn {
  --fb-ink:    var(--ink);
  --fb-on-ink: #fbf7f0;
  --fb-line:   rgba(168,131,79,.42);
  --fb-base:   rgba(252,249,243,.93);

  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  isolation: isolate;
  /* 3.25rem ≈ 52px: comfortably over the 44px floor even before padding,
     which the old 2.9rem pill was only just clearing. */
  min-height: 3.25rem;
  padding: .95em 2.15em;
  border: 1.5px solid var(--fb-line);
  border-radius: 100px;
  background: var(--fb-base);
  font-family: var(--font-text);
  /* was clamp(.68rem, …) — 10.9px of tracked uppercase serif. */
  font-size: clamp(.8rem, 2.9vw, .86rem);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: .17em;
  text-indent: .17em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  color: var(--ink);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition:
    color         .6s cubic-bezier(.23, 1, .32, 1),
    border-color  .6s cubic-bezier(.23, 1, .32, 1),
    border-radius .6s cubic-bezier(.23, 1, .32, 1),
    box-shadow    .4s var(--ease-soft),
    transform     .18s var(--ease-soft);
}

/* The label and both arrows travel together as one centred group. Early on
   this positioned the arrows against the outer button's own edges (a %
   of its full width), which reads fine on an auto-width pill but strands
   the arrow far from the label on a full-width variant like .btn-send,
   where the label sits centred but the edges are hundreds of pixels away.
   Anchoring both arrows to this tight wrapper instead — sized to the
   label alone, since the arrows are position:absolute and don't add to
   its flow width — keeps the arrow beside the label at any button width. */
.flow-btn__content {
  position: relative;
  z-index: 2;
  display: inline-flex;
  align-items: center;
}

/* The label rides above the ink and steps right as the arrow arrives. */
.flow-btn__label {
  position: relative;
  display: block;
  transform: translateX(-.55em);
  transition: transform .8s ease-out;
}

/* Both arrows are the same mark; only their resting position differs.
   --in waits just off the label's left edge, --out sits just past its
   right edge, and on engagement they swap sides so the pair reads as a
   single arrow passing through. Visibility is opacity-driven rather than
   distance-driven — the content group can be centred anywhere inside a
   button of any width, so there is no fixed "off the edge" to hide behind. */
.flow-btn__arrow {
  position: absolute;
  width: 1em;
  height: 1em;
  stroke: currentColor;
  fill: none;
  transition:
    left    .8s cubic-bezier(.34, 1.56, .64, 1),
    right   .8s cubic-bezier(.34, 1.56, .64, 1),
    opacity .5s cubic-bezier(.23, 1, .32, 1),
    stroke  .6s cubic-bezier(.23, 1, .32, 1);
}
.flow-btn__arrow--in  { left: -2.4em; opacity: 0; }
.flow-btn__arrow--out { right: -1.5em; opacity: 1; }

/* The flood. This used to be a small circle scaled up ~32× on the
   compositor rather than resized, so nothing relayouts mid-transition.
   That works everywhere except one place: on a wide flow-btn (.btn-send
   spans 500px+), Chromium fails to clip a child scaled that far past its
   own box to the button's rounded corners — the corners render dead
   square for the life of the hover, border-radius: 12px or not. Reproduced
   in isolation with nothing but overflow:hidden + border-radius on the
   parent and transform: scale(32) on an absolutely-positioned child, so
   it is not fixable by touching stacking context or layer hints.
   clip-path doesn't share that bug: the circle here is drawn at full
   size from the start and simply revealed, so there is no oversized
   layer for the corner clip to fail on. 75% is comfortably past the
   ~70.7% a clip-path circle needs to reach every corner of any box,
   per the spec's diagonal-based percentage resolution — verified
   full-bleed at 75% on the widest button on the site. */
.flow-btn__ink {
  position: absolute;
  z-index: 1;
  inset: 0;
  background: var(--fb-ink);
  clip-path: circle(0% at 50% 50%);
  transition: clip-path .8s cubic-bezier(.19, 1, .22, 1);
}

/* Any leading icon a variant supplies sits with the label. */
.flow-btn__icon {
  position: relative;
  z-index: 2;
  width: 1.05em;
  height: 1.05em;
  flex: none;
  margin-right: .6em;
  color: var(--gold-deep);
  transform: translateX(-.55em);
  transition: transform .8s ease-out, color .6s var(--ease-soft);
}

/* --- the engaged state ---
   Hover is only one of three ways in. Touch devices never hover, and
   keyboard users never hover, so :active and :focus-visible get the
   identical treatment rather than a degraded one. */
.flow-btn:focus-visible,
.flow-btn:active {
  border-radius: 12px;
  border-color: transparent;
  color: var(--fb-on-ink);
}
.flow-btn:focus-visible .flow-btn__ink,
.flow-btn:active .flow-btn__ink {
  clip-path: circle(75% at 50% 50%);
}
.flow-btn:focus-visible .flow-btn__label,
.flow-btn:active .flow-btn__label,
.flow-btn:focus-visible .flow-btn__icon,
.flow-btn:active .flow-btn__icon {
  transform: translateX(.55em);
}
.flow-btn:focus-visible .flow-btn__icon,
.flow-btn:active .flow-btn__icon { color: var(--fb-on-ink); }
.flow-btn:focus-visible .flow-btn__arrow--in,
.flow-btn:active .flow-btn__arrow--in { left: -1.5em; opacity: 1; }
.flow-btn:focus-visible .flow-btn__arrow--out,
.flow-btn:active .flow-btn__arrow--out { right: -2.4em; opacity: 0; }

@media (hover: hover) and (pointer: fine) {
  .flow-btn:hover {
    border-radius: 12px;
    border-color: transparent;
    color: var(--fb-on-ink);
  }
  .flow-btn:hover .flow-btn__ink {
    clip-path: circle(75% at 50% 50%);
  }
  .flow-btn:hover .flow-btn__label,
  .flow-btn:hover .flow-btn__icon { transform: translateX(.55em); }
  .flow-btn:hover .flow-btn__icon { color: var(--fb-on-ink); }
  .flow-btn:hover .flow-btn__arrow--in  { left: -1.5em; opacity: 1; }
  .flow-btn:hover .flow-btn__arrow--out { right: -2.4em; opacity: 0; }
}

.flow-btn:active { transform: scale(.96); }

/* Focus needs to survive the flood: once the ink is charcoal, a charcoal
   ring is invisible. The ring goes outside the button and stays gold. */
.flow-btn:focus-visible { outline: 2.5px solid var(--gold-text); outline-offset: 3px; }

/* --- variants ------------------------------------------------------------ */

/* Hero primary — warm ivory stock on the painting, charcoal flood. */
.flow-btn--stock {
  box-shadow: 0 1px 2px rgba(93,74,44,.06), 0 6px 18px rgba(93,74,44,.07);
}

/* Hero secondary. It sits over the trees and the lodge, the busiest
   quarter of the painting, so it stays nearly opaque — a properly glassy
   button was unreadable against the dark conifers. Hierarchy comes from
   the shadow, not from making this one hard to read. */
.flow-btn--quiet {
  --fb-base: rgba(252,249,243,.84);
  --fb-line: rgba(168,131,79,.5);
}

/* The form's send button — gold flood on paper. */
.flow-btn--gold {
  --fb-ink:    var(--gold-text);
  --fb-on-ink: var(--paper);
  --fb-line:   var(--gold-text);
  --fb-base:   transparent;
  color: var(--gold-text);
}

/* --- the stepper's Continue/Send button: a faster engage ---
   Every other flow-btn on the site is something a guest pauses on before
   acting — Book, Add to calendar. This one is tapped in rapid succession,
   once per question, and a real tap (or a quick click) releases well
   inside the base component's 600–800ms hover-tuned choreography. The
   flood-in starts, gets interrupted mid-transition by :active ending, and
   snaps back — which reads as a flash or a stutter, not a deliberate
   motion. Scoped to .btn-send only: the flood-fill itself stays, it just
   moves fast enough for a tap-and-release to actually see it land. */
.flow-btn.btn-send {
  transition:
    color         .2s var(--ease-out),
    border-color  .2s var(--ease-out),
    border-radius .2s var(--ease-out),
    box-shadow    .3s var(--ease-soft),
    transform     .18s var(--ease-soft);
}
.btn-send .flow-btn__label,
.btn-send .flow-btn__icon { transition: transform .2s var(--ease-out); }
.btn-send .flow-btn__arrow {
  transition: left .2s var(--ease-out), right .2s var(--ease-out), stroke .16s var(--ease-out);
}
.btn-send .flow-btn__ink {
  transition: clip-path .2s var(--ease-out);
}


/* --- scroll cue: a tracked line and a growing botanical stem ---
   The stem draws itself over ~4.4s, which is slower than any chevron
   would dare to be, and that slowness is the point. */
/* Now the only interactive thing in the hero, so it gets a real target
   (the whole label-plus-arrow column) rather than the tap-sized sliver a
   decorative cue could get away with. */
.hero-cue {
  position: absolute;
  left: 50%;
  bottom: calc(1.4rem + var(--safe-b));
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .7rem;
  width: max-content;
  padding: .7rem 1.4rem;
  transform: translateX(-50%);
  color: var(--ink-body);
  opacity: 1;
  text-decoration: none;
  transition: opacity .6s ease;
}

/* Its own bloom. The cue lands on the marina — the busiest passage in the
   painting — and now that the ivory is held back until the scroll starts,
   that passage is at full detail on arrival rather than half dissolved.
   A halo on the text alone was not enough against boats and decking, so
   the light sits behind the whole group. Still a bloom, not a plate: it
   has no edge at any point. */
.hero-cue::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: -1;
  width: 168%;
  height: 190%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  background: radial-gradient(ellipse 50% 50% at 50% 50%,
      rgba(250,246,238,.92) 0%,
      rgba(250,246,238,.72) 42%,
      rgba(250,246,238,.34) 68%,
      rgba(250,246,238,0)   100%);
}
.cue-label {
  /* Was 9.3px of tracked caps over the busiest passage of the painting —
     the least readable text on the page, doing the most important job.
     Up to ~14px, tracking pulled in, and the halo below widened so it
     holds over the marina without becoming a plate. */
  font-family: var(--font-text);
  font-size: clamp(.85rem, 3vw, .92rem);
  font-weight: 600;
  letter-spacing: .18em;
  text-indent: .18em;
  text-transform: uppercase;
  color: var(--ink);
  /* The cue lands over the marina, which is the fussiest passage in the
     painting. A tight double halo lifts it without becoming a plate. */
  text-shadow:
    0 0 5px  rgba(250,246,238,1),
    0 0 12px rgba(250,246,238,1),
    0 0 26px rgba(250,246,238,.92),
    0 0 44px rgba(250,246,238,.75);
}
/* A plain down arrow, drifting. The botanical stem that used to be here
   drew itself over 4.4 seconds and read as decoration — at a glance it was
   a sprig, not an instruction, and the one thing this screen asks you to do
   should not need decoding. */
.cue-arrow {
  width: 26px;
  height: auto;
  animation: cue-drift 2.6s var(--ease-inout) infinite;
}
@keyframes cue-drift {
  0%, 100% { transform: translateY(-2px); opacity: .75; }
  50%      { transform: translateY(4px);  opacity: 1; }
}


/* Framing per breakpoint lives with the rest of the responsive rules in §8. */


/* --- §2.2 first-load reveal ---------------------------------------------
   Opacity and transform only, staggered, all on --ease-reveal. The hero
   is armed by save-the-date.js the moment the page becomes visible —
   after the envelope hands off, or immediately for anyone who skipped it.

   The whole staged entrance is scoped to html.js, and that scoping is
   load-bearing: `.is-revealed` is added by the script, so an unscoped
   `opacity: 0` here meant that with JavaScript off the hero never came
   back — no names, no date, no actions, just the painting's ivory wash.
   The page has a no-JS story everywhere else (the envelope hides itself,
   the copy is mirrored in the markup); this is it holding up here too. */
html.js .hero-media,
html.js .hero-title,
html.js .hero-divider,
html.js .hero-meta,
html.js .hero-cue {
  opacity: 0;
  transform: translateY(14px);
  transition:
    opacity   .9s var(--ease-reveal),
    transform 1.1s var(--ease-reveal);
}
html.js .hero-media { transform: none; }   /* the painting fades, it never slides */
html.js .hero-cue   { transform: translateX(-50%) translateY(14px); }

.hero.is-revealed .hero-media,
.hero.is-revealed .hero-title,
.hero.is-revealed .hero-divider,
.hero.is-revealed .hero-meta {
  opacity: 1;
  transform: none;
}
.hero.is-revealed .hero-cue { opacity: 1; transform: translateX(-50%); }

.hero.is-revealed .hero-media    { transition-delay: .10s; }
.hero.is-revealed .hero-title    { transition-delay: .50s; }
.hero.is-revealed .hero-divider  { transition-delay: .68s; }
.hero.is-revealed .hero-meta     { transition-delay: .80s; }
.hero.is-revealed .hero-cue      { transition-delay: 1.50s; }

/* The cue has said its piece the moment you start scrolling, so it goes.

   This has to sit after the reveal rules: it and `.hero.is-revealed
   .hero-cue` are both three classes, so source order alone decides. It
   used to be higher up and was only ever beaten to the punch by a
   `.hero.is-scrolling` rule in the old scroll-response section — which
   went with the parallax, taking the cue's fade-out with it. The entrance
   delay is dropped too: waiting 1.5s to fade out is not a fade-out. */
.hero.is-scrolled .hero-cue {
  opacity: 0;
  pointer-events: none;
  transition-delay: 0s;
}


/* --- §2.3 scroll response ------------------------------------------------
   There isn't one any more, and that is the point.

   What used to live here: --hero-p (0 → 1 across a 118svh runway) drove
   the copy's opacity and lift, the arrival of the ivory at the foot of the
   painting, an ivory wash climbing 74% up over it, and three depth planes
   falling at 2.5 / 5.5 / 9%. With the pane pinned, all of that moved while
   the painting did not, which is what made the scroll down from the hero
   feel wrong rather than smooth.

   Now the hero is one screen, --hero-ivory stays at its default of 1, and
   the painting's foot carries the same gentle dissolve at every moment —
   nothing to time, nothing to catch up with. The one scroll-linked thing
   left is the cue fading out once you have taken its advice, which is a
   plain opacity transition on a class, up in §2.2. */


/* ==========================================================================
   3 · PAPER
   ========================================================================== */

.paper {
  position: relative;
  /* laps up into the tail of the hero dissolve; its own fill starts fully
     transparent so the two never meet on an edge */
  margin-top: -9svh;
  background:
    linear-gradient(to bottom,
      rgba(242,235,221,0) 0,
      rgba(242,235,221,.55) 16rem,
      var(--paper-warm) 40rem,
      var(--paper) 100%);
}
/* fine tooth, so it reads as stock rather than a flat fill */
.paper::before {
  content: '';
  position: absolute;
  inset: 12rem 0 0;
  pointer-events: none;
  opacity: .45;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='.045'/%3E%3C/svg%3E");
}

.sheet { position: relative; z-index: 2; padding: 0 var(--gutter); }
.sheet-inner { max-width: 33rem; margin: 0 auto; }
/* A prose measure is the wrong width for a multi-column card grid — this
   modifier is the only place on the site that needs a wider column.
   Double-class selector deliberately: CSS §8's responsive `.sheet-inner`
   overrides at 768/1024 have equal specificity and come later in the
   file, and would win the cascade over a single-class `.sheet-inner--wide`
   at those breakpoints otherwise. */
.sheet-inner.sheet-inner--wide { max-width: 68rem; }

.eyebrow {
  font-family: var(--font-text);
  font-size: var(--t-micro);   /* was .68rem — 10.9px */
  font-weight: 600;
  letter-spacing: .3em;        /* was .42em; wide tracking hurts tiny text */
  text-indent: .3em;
  text-transform: uppercase;
  color: var(--gold-text);
  text-align: center;
}
/* Display type is where Cormorant belongs and the only place it is used. */
.sheet-title {
  margin-top: .8rem;
  font-family: var(--font-display);
  font-size: clamp(2.2rem, 8.4vw, 3.1rem);
  font-weight: 500;            /* was 400: Cormorant 400 is a hairline at size */
  line-height: 1.06;
  /* Was +.012em — the one display heading on the site tracking open
     instead of tight. .celebrate proves the rule at a bigger size (see
     its own comment); this is the same face a tier down, so it gets the
     same direction at half the magnitude. */
  letter-spacing: -.006em;
  color: var(--ink);
  text-align: center;
  text-wrap: balance;
}
/* The single worst offender on the page: the first sentence a guest reads,
   set in a display serif at 300 in the lightest ink available. */
.sheet-lede {
  margin: 1.1rem auto 0;
  max-width: 30rem;
  font-size: clamp(var(--t-body), 4.1vw, 1.16rem);
  font-weight: 400;
  line-height: 1.65;
  color: var(--ink-body);
  text-align: center;
  text-wrap: pretty;
}



/* ==========================================================================
   3.5 · THE INVITATION LINE
   One sentence, one screen. It is the handshake between the painting and
   everything that follows, so it gets room and nothing else gets to share
   the view with it.
   ========================================================================== */

.celebrate-sheet {
  padding-top: clamp(4.5rem, 16vh, 9rem);
  padding-bottom: clamp(4rem, 14vh, 8rem);
  /* The hero cue links here (#celebrate) — cleared against the fixed
     hero-nav's real rendered height rather than a guessed constant; see
     --nav-h in hero-nav.js and the identical note on .travel-section. */
  scroll-margin-top: calc(var(--nav-h, 100px) + .75rem + var(--safe-t));
}

.celebrate {
  max-width: 20ch;              /* ~3 lines at every width — set by the
                                   measure, not by manual line breaks */
  margin: 0 auto;
  font-family: var(--font-display);
  /* Genuinely large. Cormorant is built for exactly this size and this is
     the only place on the page it gets to behave like a display face. */
  font-size: clamp(2.6rem, 11vw, 5.5rem);
  font-weight: 500;
  line-height: 1.02;
  letter-spacing: -.012em;
  color: var(--ink);
  text-align: center;
  text-wrap: balance;
}
.celebrate em {
  display: block;
  font-style: italic;
  color: var(--gold-text);
}

/* --- the calendar action ---
   The one thing to do on this screen, and the reason the hero's "Scroll for
   detail" now leads somewhere. It gets real space above it so it reads as a
   consequence of the sentence rather than a caption attached to it, and it
   is --quiet rather than --gold: the invitation is the loud thing here. */
.date-actions {
  max-width: 22rem;
  margin: clamp(2.8rem, 10vh, 4.5rem) auto 0;
  text-align: center;
}
.date-btn { width: 100%; }

/* The Android/Gmail path. A second full button would read as a choice
   between two equal things, which it is not — almost everyone wants the
   file. This is the escape hatch for the people who do not. */
.date-alt {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-top: 1.1rem;
  min-height: 44px;              /* the padding alone landed at ~41px */
  padding: .5rem .4rem;
  font-size: var(--t-small);
  color: var(--stone-text);
  text-decoration: underline;
  text-underline-offset: .3em;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--rule);
  transition: color .3s var(--ease-soft), text-decoration-color .3s var(--ease-soft);
}
.date-alt:hover, .date-alt:focus-visible {
  color: var(--ink);
  text-decoration-color: var(--gold-text);
}


/* The sentence arrives a word at a time, each one lifting out of a soft
   blur into its resting position as the section scrolls through view —
   tied to the scroll itself, so scrolling back up dissolves the line
   again, last word home first.

   Nothing is pinned to do this. The section scrolls normally; only the
   words read the progress.

   --celebrate-p (0 → 1) is published by the script as the section crosses
   the viewport. --word-dur and --word-off are published once, from the
   word count: each word's own local progress --lp is the slice of
   --celebrate-p between (word-i × word-off) and that plus word-dur, so
   the words overlap into a cascade rather than blinking through in strict
   sequence, and the last word always lands exactly as --celebrate-p
   reaches 1. --ew shapes --lp with a quick ease-out (matching --ease-out
   elsewhere on the page) so each word settles rather than arriving at a
   constant rate. */
.celebrate {
  --celebrate-p: 0;
}
html.js .celebrate .word {
  --lp: clamp(0, calc((var(--celebrate-p, 0) - (var(--word-i, 0) * var(--word-off, 0))) / var(--word-dur, .4)), 1);
  --ew: calc(2 * var(--lp) - var(--lp) * var(--lp));
  display: inline-block;
  opacity: var(--ew);
  filter: blur(calc((1 - var(--ew)) * 6px));
  transform: translateY(calc((1 - var(--ew)) * 14px));
  /* Deliberately no transition: the scroll position *is* the timeline, and
     a transition on top of it would lag behind the finger. The ease-out
     lives in --ew above, not in a transition-timing-function. */
  will-change: opacity, filter, transform;
}
/* Without the script there is no progress to read, so the line is simply
   set — same rule the hero and the mosaic follow. */
html:not(.js) .celebrate .word { opacity: 1; filter: none; transform: none; }


/* ==========================================================================
   3.6 · THE MOSAIC
   One photograph fills the screen, then shrinks into a framed card while
   five more fly in from the edges around it.

   Restored from the original site's "Cordially-style scroll-driven hero
   mosaic". That version wrote width/height/border-radius/box-shadow and a
   transform onto six elements on every scroll frame — a layout pass per
   card per frame. Here JS publishes exactly one number, --mosaic-p, and
   every card composes itself from it with a transform and an opacity,
   which is all the compositor needs.

   The centre card does not animate width/height either: it is laid out at
   its FINAL size and scaled UP at p=0, so the expensive state is the one
   that never moves and the whole shrink is a single scale().
   ========================================================================== */

.mosaic {
  position: relative;
  z-index: 2;
  /* The runway. The sticky pane below holds still for this whole distance
     while the arrangement assembles. */
  height: 260svh;
  --mosaic-p: 1;
}
@supports not (height: 100svh) { .mosaic { height: 260vh; } }

/* Only a page with a script to drive it starts at 0. Without JS nothing
   ever advances --mosaic-p, so an unscoped 0 would leave one full-bleed
   photograph followed by two and a half screens of empty sticky pane —
   the same trap the hero and photo reveals had. Assembled is the correct
   resting state; the animation is the enhancement. */
html.js .mosaic { --mosaic-p: 0; }
html:not(.js) .mosaic { height: auto; }
html:not(.js) .mosaic-viewport { position: relative; height: auto; padding: 4rem 0; }
html:not(.js) .mosaic-stage { height: min(80svh, 46rem); }

.mosaic-viewport {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
  display: grid;
  place-items: center;
}
@supports not (height: 100svh) { .mosaic-viewport { height: 100vh; } }

/* The frame the whole arrangement is composed inside. */
.mosaic-stage {
  position: relative;
  width: min(100%, 78rem);
  height: min(86svh, 52rem);
}

.m-card {
  position: absolute;
  overflow: hidden;
  border-radius: .9rem;
  background: var(--paper-deep);
  will-change: transform, opacity;
}
.m-card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* When config supplies a WebP source, mosaic() wraps the <img> in a
   <picture> — display:contents drops the wrapper out of the box tree so
   the <img> above still sizes exactly as if it were .m-card's direct
   child, same as every card without a WebP source. */
.m-card picture { display: contents; }
.m-frame {
  position: absolute;
  inset: 0;
  pointer-events: none;
  border: 1px solid rgba(157,144,120,.3);
  border-radius: inherit;
}

/* --- the centre card ---
   Interpolates from exactly the viewport to exactly its framed size. A
   fixed scale factor was tried first and cannot be right at more than one
   window width: the stage is capped at 78rem, so one multiplier that fills
   a 1280px window leaves a letterbox on a 1920px one. Starting at 100vw /
   100svh is correct at every size by construction.

   This is the one element on the page that animates width and height. It
   is absolutely positioned inside a fixed-size stage, so the layout it
   dirties is itself and its own <img> — no sibling reflows. The five
   satellites stay transform-only. */
.m-hero {
  top: 50%;
  left: 50%;
  z-index: 2;
  --m-w: 54%;      /* framed size, as a share of the stage */
  --m-h: 74%;
  width:  calc(100vw  + (var(--m-w) - 100vw)  * var(--mosaic-p));
  height: calc(100svh + (var(--m-h) - 100svh) * var(--mosaic-p));
  transform: translate(-50%, -50%);
  border-radius: calc(.9rem * var(--mosaic-p));
  box-shadow: 0 calc(22px * var(--mosaic-p)) calc(60px * var(--mosaic-p))
              rgba(70,58,42, calc(.3 * var(--mosaic-p)));
}
@supports not (height: 100svh) {
  .m-hero { height: calc(100vh + (var(--m-h) - 100vh) * var(--mosaic-p)); }
}

/* --- the satellites ---
   Each declares where it flies in FROM in its own percentages; the shared
   rule interpolates that to zero. Opacity and scale come along for the ride
   so they arrive rather than simply appear. They start at 0 and hold there
   until the centre card is well clear — see --m-in below. */
.m-sat {
  z-index: 1;
  /* satellite progress: nothing until 18%, complete by 88% */
  --m-in: clamp(0, calc((var(--mosaic-p) - .18) / .7), 1);
  opacity: var(--m-in);
  transform:
    translate(calc(var(--m-fx) * (1 - var(--m-in))), calc(var(--m-fy) * (1 - var(--m-in))))
    scale(calc(.72 + .28 * var(--m-in)));
  box-shadow: 0 10px 38px rgba(70,58,42,.16);
}

/* .m-tl/.m-tr sit under the fixed hero-nav bar, which spans the width at
   phone sizes with a control in each corner — not just one corner — so
   both top cards can end up under it, not only the one nearer either
   control. Measured before hero-nav replaced the old floating pill: that
   pill could wrap to two lines on some phone widths and overlap .m-tl by
   13px, .m-tr by 7px. hero-nav's height doesn't vary with content the way
   the old pill did, but --nav-h (published by hero-nav.js — see CSS §5.5)
   still reads its real rendered height rather than a hardcoded one, so
   this stays correct if that ever changes (a taller safe-area inset, a
   larger touch target). margin-top only engages past a single-line pill's
   worth of height, so this is a small constant buffer today, not a
   response to active wrapping. */
.m-tl, .m-tr { margin-top: max(0px, calc(var(--nav-h, 57px) - 45px)); }

.m-tl { top:  3%; left:  1%; width: 22%; height: 46%; --m-fx: -130%; --m-fy:  -80%; }
.m-bl { bottom: 5%; left:  3%; width: 20%; height: 34%; --m-fx: -120%; --m-fy:  110%; }
.m-tr { top:  4%; right: 2%; width: 18%; height: 31%; --m-fx:  120%; --m-fy: -110%; }
.m-br { bottom: 3%; right: 1%; width: 24%; height: 41%; --m-fx:  130%; --m-fy:   90%; }
.m-mr { top: 39%; right: 0%; width: 14%; height: 23%; --m-fx:  160%; --m-fy:   20%; }

/* --- the "photograph to come" frame, same language as the old strip --- */
.m-card.is-empty img { display: none; }
.m-inner {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .8rem;
  padding: 1rem;
  background: linear-gradient(158deg, #f4eee1 0%, var(--paper-deep) 100%);
}
.m-empty-mark { width: clamp(20px, 3.4vw, 34px); color: var(--leaf); opacity: .75; }
.m-empty-mark svg { width: 100%; height: auto; }
.m-empty-note {
  font-family: var(--font-text);
  font-size: clamp(.6rem, 1.5vw, var(--t-micro));
  font-weight: 600;
  letter-spacing: .18em;
  text-indent: .18em;
  text-transform: uppercase;
  color: var(--stone-text);
  text-align: center;
}
/* The satellites are small; at their size the label is unreadable, so they
   show the mark alone rather than a caption nobody can read. */
.m-sat .m-empty-note { display: none; }

/* Phones: the stage is portrait, so the satellites move to the corners of a
   taller frame and the centre card starts less oversized. */
@media (max-width: 767px) {
  .mosaic { height: 220svh; }
  .mosaic-stage { width: 100%; height: min(80svh, 40rem); }
  .m-hero { --m-w: 68%; --m-h: 58%; }
  .m-tl { top: 2%;    left: 1%;  width: 30%; height: 22%; }
  .m-bl { bottom: 3%; left: 2%;  width: 27%; height: 19%; }
  .m-tr { top: 3%;    right: 1%; width: 26%; height: 18%; }
  .m-br { bottom: 2%; right: 2%; width: 31%; height: 21%; }
  .m-mr { display: none; }        /* six cards is a crowd at this width */
}


/* ==========================================================================
   4 · FORM
   ========================================================================== */

/* The form is now the last thing on the page — the footer that used to
   follow it, and cushion the bottom of the screen, is gone. The extra
   bottom padding (and the safe-area inset a device's home indicator
   needs) replaces that cushion so the page doesn't end abruptly against
   the edge of the viewport. */
.form-sheet {
  padding-top: clamp(4rem, 13vh, 7rem);
  padding-bottom: calc(clamp(3.5rem, 11vh, 5.5rem) + var(--safe-b));
  /* The skip-link targets #details directly — same reasoning as
     .celebrate-sheet above. */
  scroll-margin-top: calc(var(--nav-h, 100px) + .75rem + var(--safe-t));
}

.gform { margin-top: clamp(2.2rem, 7vw, 3rem); }

.field { position: relative; margin-bottom: 1.6rem; }
.field-row { display: grid; gap: 0 1.6rem; }

/* The label carries the field's name and nothing else, so it can be small
   caps — but 11.2px of tracked 300-weight serif was past the point where
   small caps stay readable. Up to 12.5px, up to 600, and the tracking
   pulled back from .3em to .22em: wide tracking on tiny text is exactly
   what stops the eye assembling it into a word. */
.field label {
  display: block;
  margin-bottom: .55rem;
  font-family: var(--font-text);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: .22em;
  text-indent: .22em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* A bottom rule is a beautiful field and a poor one: there is no enclosed
   shape, so nothing tells you where the target begins, how tall it is, or
   that it is a target at all — and on touch the hit area was the text line
   plus whatever padding happened to be there. These are wells now: a real
   boundary, a warm fill that separates them from the paper, and a 3.4rem
   floor so every one of them clears the 44px touch minimum. The engraved
   feel survives in the fill and the hairline; only the affordance changed. */
/* Same frosted-glass family as the Travel & Lodging cards: a translucent
   warm gradient over backdrop blur and a soft ambient shadow, layered on
   top of the well's own inset shadow rather than replacing it — the
   "engraved" affordance above still needs its real boundary, this just
   makes the fill read as glass instead of flat paint. Hover and focus
   both brighten the border and deepen the glow, the same feedback
   language as the rest of the site's frosted surfaces. */
.field input,
.field textarea {
  width: 100%;
  min-height: 3.4rem;
  padding: .85rem 1rem;
  border: 1px solid rgba(196,159,108,.28);
  border-radius: .7rem;
  background: linear-gradient(168deg, rgba(255,255,255,.62) 0%, rgba(251,247,240,.4) 100%);
  box-shadow: inset 0 1px 2px rgba(93,74,44,.05), 0 10px 24px -20px rgba(93,74,44,.35);
  -webkit-backdrop-filter: blur(10px);
          backdrop-filter: blur(10px);
  font-family: var(--font-text);
  font-size: 1.12rem;          /* >=16px: stops iOS zooming on focus */
  font-weight: 400;
  line-height: 1.45;
  color: var(--ink);
  transition:
    border-color .3s var(--ease-soft),
    background   .3s var(--ease-soft),
    box-shadow   .3s var(--ease-soft);
  -webkit-appearance: none;
          appearance: none;
}
.field textarea { resize: vertical; min-height: 5.4rem; line-height: 1.55; }
.field input::placeholder,
.field textarea::placeholder { color: var(--stone-text); opacity: .7; }

.field input:hover, .field textarea:hover {
  border-color: rgba(196,159,108,.5);
  box-shadow: inset 0 1px 2px rgba(93,74,44,.05), 0 14px 30px -20px rgba(93,74,44,.42);
}
.field input:focus, .field textarea:focus {
  outline: none;
  border-color: var(--gold-deep);
  background: linear-gradient(168deg, rgba(255,253,249,.95) 0%, rgba(255,250,240,.85) 100%);
  box-shadow: inset 0 1px 2px rgba(93,74,44,.04), 0 0 0 3px rgba(196,159,108,.2), 0 16px 34px -20px rgba(93,74,44,.4);
}
/* Keyboard focus gets a real ring on top of the soft treatment — the fill
   change alone is too quiet to navigate by, and the ring only ever shows
   for keyboard. */
.field input:focus-visible,
.field textarea:focus-visible {
  outline: 2.5px solid var(--gold-text);
  outline-offset: 3px;
}

.field-hint {
  margin-top: .55rem;
  font-size: var(--t-small);
  font-weight: 400;
  line-height: 1.5;
  color: var(--stone-text);
}
/* The way out for a guest who is mid-move and genuinely does not have an
   address yet. Text, not a button shape — it is an escape hatch, not a
   second call to action competing with "Send our details" below it. */
.field-skip {
  display: block;
  margin-top: .8rem;
  border: 0;
  padding: .4rem 0;
  background: none;
  font: inherit;
  font-size: var(--t-small);
  text-align: left;
  text-decoration: underline;
  text-underline-offset: .25em;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--rule);
  color: var(--stone-text);
  cursor: pointer;
  transition: color .3s var(--ease-soft), text-decoration-color .3s var(--ease-soft);
}
.field-skip:hover, .field-skip:focus-visible {
  color: var(--ink);
  text-decoration-color: var(--gold-text);
}

/* Errors led with italic text of the same weight and nearly the same
   colour as the hint sitting three pixels away. This one gets a mark, a
   solid weight and a colour that is only ever used for errors. */
.field-err {
  display: flex;
  align-items: flex-start;
  gap: .45em;
  margin-top: .55rem;
  font-size: var(--t-small);
  font-weight: 500;
  line-height: 1.5;
  color: var(--err);
  /* Everything else in the form eases in (border-color, background,
     box-shadow all transition); the error message itself was popping in
     via the bare hidden→flex toggle below with no motion at all. A
     transition can't animate that toggle (display can't tween), so this
     is an animation, same as .gform.is-stepped .step's step-in — it
     restarts for free every time [hidden] flips off and display goes
     from none to flex. */
  animation: field-err-in .2s var(--ease-out) both;
}
@keyframes field-err-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
/* `display: flex` above outranks the UA's `[hidden] { display: none }`,
   which would leave an empty error marker sitting under every field. */
.field-err[hidden] { display: none; }
.field-err::before {
  content: '';
  flex: none;
  width: 1.05em;
  height: 1.05em;
  margin-top: .18em;
  background: var(--err);
  -webkit-mask: var(--ico-alert) center / contain no-repeat;
          mask: var(--ico-alert) center / contain no-repeat;
}
.field.has-error input,
.field.has-error textarea {
  border-color: var(--err);
  background: #fdf6f3;
}
.field.has-error input:focus,
.field.has-error textarea:focus { box-shadow: 0 0 0 3px rgba(140,79,57,.18); }
/* A gold focus ring around a field flagged in red says two things at once.
   While the field is in error, the ring speaks the error's colour. */
.field.has-error input:focus-visible,
.field.has-error textarea:focus-visible { outline-color: var(--err); }

/* honeypot */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* --- §4.1 THE STEPPER ----------------------------------------------------
   Four short fields presented one at a time, with the progress made
   visible. The same information, but the guest is answering a question
   instead of facing a form — and each answer is checked as it is given
   rather than all four at the end.

   Ported from the React multistep component. Everything below is
   progressive enhancement: the chrome ships hidden and only the script
   turns it on, so with JavaScript off the same markup is one plain form
   with all four fields visible and a working native submit.
   -------------------------------------------------------------------------- */
.stepper { margin-bottom: 2.4rem; }

.stepper-dots {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .55rem;
  list-style: none;
  margin-bottom: 1.5rem;
}
.stepper-dots li { display: flex; align-items: center; gap: .55rem; }

.step-dot {
  position: relative;
  display: grid;
  place-items: center;
  /* 2.75rem = 44px: the tap-target floor, not just the visible size —
     was 2.3rem (36.8px), under it on every phone. */
  width: 2.75rem;
  height: 2.75rem;
  border: 1px solid var(--rule);
  border-radius: 50%;
  background: rgba(255,255,255,.5);
  font-family: var(--font-text);
  font-size: 1rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--stone-text);
  transition:
    background   .5s var(--ease-out),
    border-color .5s var(--ease-out),
    color        .5s var(--ease-out),
    box-shadow   .5s var(--ease-out),
    transform    .5s var(--ease-out);
}
/* Only steps already answered can be jumped back to. Ahead is not a
   target, so it is not focusable and does not pretend to be pressable. */
.step-dot[disabled] { cursor: default; opacity: .75; }
.step-dot:not([disabled]) { cursor: pointer; }

.step-dot.is-done {
  border-color: var(--gold-deep);
  background: rgba(196,159,108,.16);
  color: var(--gold-text);
}
/* The tick replaces the numeral once the step is behind you. */
.step-dot.is-done .step-dot-num { display: none; }
.step-dot.is-done::after {
  content: '';
  width: 1rem;
  height: 1rem;
  background: var(--gold-text);
  -webkit-mask: var(--ico-check) center / contain no-repeat;
          mask: var(--ico-check) center / contain no-repeat;
  animation: dot-tick .45s var(--ease-out) both;
}
@keyframes dot-tick { from { transform: scale(.4); opacity: 0; } to { transform: scale(1); opacity: 1; } }

.step-dot.is-current {
  border-color: transparent;
  background: var(--ink);
  color: var(--card);
  box-shadow: 0 4px 14px -4px rgba(69,66,62,.55);
  transform: scale(1.08);
}

/* The connector between two dots, filling left to right as you advance. */
.step-link {
  position: relative;
  width: clamp(1.6rem, 7vw, 2.6rem);
  height: 1.5px;
  background: var(--rule);
  overflow: hidden;
}
.step-link::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--gold-deep);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .7s var(--ease-out);
}
.step-link.is-filled::after { transform: scaleX(1); }

/* The overall progress rail. */
.stepper-rail {
  height: 2px;
  border-radius: 2px;
  background: var(--rule);
  overflow: hidden;
}
.stepper-rail-fill {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 2px;
  background: linear-gradient(to right, var(--gold-deep), var(--gold-text));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .9s var(--ease-out);
}

/* Step panes. Only the stepped form hides anything — without JS every
   pane stays in flow and the form reads as one page, as it always did. */
.gform.is-stepped .step[hidden] { display: none; }
.gform.is-stepped .step {
  animation: step-in .6s var(--ease-out) both;
}
@keyframes step-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

/* The question itself gets a second, livelier entrance on top of the pane's
   plain fade: each word blurs, drifts up and scales in, one after another,
   settling with a small overshoot rather than just easing to a stop — the
   bit of bounce that makes a four-question form feel like it is greeting
   you instead of processing you. Ported from the React DepthParallaxWords
   component (animate-text catalog's depth-parallax-words), retimed a touch
   snappier since this plays on every "Continue" tap rather than once.
   Words are wrapped by save-the-date.js's buildStepper(); the animation
   itself needs nothing from JS — it replays for free whenever go() lifts
   [hidden] off a pane, the same mechanic step-in above already relies on. */
.step-q .word {
  display: inline-block;
  will-change: filter, opacity, transform;
}
html.js .gform.is-stepped .step-q .word {
  animation: step-q-word-in .6s var(--ease-reveal) both;
  animation-delay: calc(var(--word-i, 0) * 55ms);
}
@keyframes step-q-word-in {
  0% {
    filter: blur(4px);
    opacity: 0;
    transform: translateY(16px) scale(.9);
  }
  65% {
    filter: blur(0);
    opacity: 1;
    transform: translateY(-2px) scale(1.02);
  }
  100% {
    transform: none;
  }
}

/* One field per screen means the label is no longer a caption on a list —
   it is the question being asked, so it is set as one. It stays the real
   <label>, so this is a size change, not an extra layer of text for a
   screen reader to read past. */
.step-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1rem;
}
.field label.step-q {
  margin-bottom: 0;
  font-family: var(--font-display);
  font-size: clamp(1.45rem, 5.6vw, 1.75rem);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: .005em;
  text-indent: 0;
  text-transform: none;
  color: var(--ink);
}
/* A required-looking question that turns out to be optional reads as a
   small betrayal three fields in. Said plainly, in the question itself,
   rather than left for the guest to discover by trying to skip it. */
.step-optional {
  font-style: normal;
  font-size: .62em;
  font-weight: 400;
  color: var(--stone-text);
}
/* "2 of 4". Without it, a form that reveals itself one field at a time is
   a form with no visible end — which is the classic way multistep goes
   wrong. Announced separately by the live region, so it is decorative here. */
.step-count {
  flex: none;
  font-size: var(--t-micro);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: .14em;
  color: var(--stone-text);
}
/* With JavaScript off there are no steps to count. */
.gform:not(.is-stepped) .step-count { display: none; }
.gform:not(.is-stepped) .step { margin-bottom: 2rem; }

.form-actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  margin-top: 2.2rem;
}
/* The send button is the flow button at full width — see §2.4. */
.btn-send { width: 100%; }
/* Deliberately not `disabled` until a field is filled. A dead button gives
   no reason for being dead; pressing this one always does something, and
   an empty field answers with the inline error that says what it wants. */
.btn-send[aria-disabled="true"] { opacity: .55; }
.btn-send.is-sending { pointer-events: none; }

.step-back {
  border: 0;
  padding: .55rem 1rem;
  background: none;
  font-family: var(--font-text);
  font-size: var(--t-small);
  color: var(--stone-text);
  text-decoration: underline;
  text-underline-offset: .3em;
  cursor: pointer;
  transition: color .3s var(--ease-soft);
}
.step-back:hover, .step-back:focus-visible { color: var(--ink); }

.form-status {
  margin-top: 1.2rem;
  min-height: 1.4em;
  font-size: var(--t-small);
  font-weight: 400;
  line-height: 1.55;
  color: var(--ink-soft);
  text-align: center;
  text-wrap: pretty;
}
.form-status.is-error { color: var(--err); font-weight: 500; }
/* The way out of a failed send. It sits inside an error sentence, so it
   inherits the error ink and is distinguished by the underline alone —
   a second colour here would be one accent too many. */
.form-status-link {
  color: inherit;
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: .25em;
  text-decoration-thickness: 1px;
}

/* Screen-reader-only, for the live region that narrates step changes. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The confirmation. It used to be a divider and two lines of text that
   looked like any other passage on the page; the reference component's
   medallion is the part worth keeping, because arrival needs a mark. */
.form-done {
  /* The lede is removed when this appears, so without a margin the card
     lands hard against the section heading. */
  margin-top: clamp(1.8rem, 6vw, 2.6rem);
  text-align: center;
  padding: clamp(1.6rem, 6vw, 2.6rem) clamp(1rem, 5vw, 2rem);
  border: 1px solid rgba(196,159,108,.34);
  border-radius: 1rem;
  background: linear-gradient(168deg, #fdfaf4 0%, var(--card) 60%, #f4ecdd 100%);
  box-shadow: 0 1px 2px rgba(93,74,44,.05), 0 18px 40px -28px rgba(93,74,44,.5);
  animation: done-in .8s var(--ease-out) both;
}
@keyframes done-in {
  from { opacity: 0; transform: scale(.97); }
  to   { opacity: 1; transform: none; }
}
.form-done:focus { outline: none; }
.form-done:focus-visible { outline: 2.5px solid var(--gold-text); outline-offset: 4px; }

.done-mark {
  display: grid;
  place-items: center;
  width: 3.6rem;
  height: 3.6rem;
  margin: 0 auto 1.4rem;
  border: 1px solid rgba(196,159,108,.5);
  border-radius: 50%;
  background: rgba(196,159,108,.14);
}
.done-mark::after {
  content: '';
  width: 1.6rem;
  height: 1.6rem;
  background: var(--gold-text);
  -webkit-mask: var(--ico-check) center / contain no-repeat;
          mask: var(--ico-check) center / contain no-repeat;
  animation: dot-tick .55s var(--ease-out) .2s both;
}

.done-line {
  font-family: var(--font-display);
  font-size: clamp(1.7rem, 6.6vw, 2.1rem);
  font-weight: 500;
  line-height: 1.15;
  color: var(--ink);
}
.done-sub {
  margin-top: .8rem;
  font-size: var(--t-body);
  font-weight: 400;
  color: var(--ink-soft);
}

/* The hand-off to Travel & Stay. A deliberate click rather than a timed
   redirect: the note underneath is the whole point of pausing here, and
   an automatic navigation would carry the guest past it unread. */
.done-next {
  margin-top: clamp(1.6rem, 5vw, 2.2rem);
}

/* --- "sent something already?" ---
   Only ever rendered on a return visit, where the done screen is shown
   from a remembered submission rather than one that just happened. It is
   the escape hatch for a guest who has moved house since August, so it
   has to be findable — and quiet enough that it never competes with the
   lodging hand-off above it. */
.done-again {
  margin-top: clamp(1.8rem, 6vw, 2.4rem);
  font-size: var(--t-small);
  line-height: 1.55;
  color: var(--stone-text);
}
.done-again-link {
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  color: var(--gold-text);
  text-decoration: underline;
  text-underline-offset: .25em;
  text-decoration-thickness: 1px;
  cursor: pointer;
  transition: color .3s var(--ease-soft);
}
.done-again-link:hover, .done-again-link:focus-visible { color: var(--ink); }


/* ---- Booking nudge popup ------------------------------------------------
   Same fixed-overlay / backdrop / dialog pattern as .lodge-lightbox in
   travel/index.html, condensed to one column since there is no photo
   here. save-the-date.js is the only thing that ever opens it, once,
   right after a fresh successful submission. */
.book-nudge[hidden] { display: none; }
.book-nudge {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--gutter);
}
html.book-nudge-open,
html.book-nudge-open body { overflow-y: hidden; }

.book-nudge-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(37,29,18,.66);
  opacity: 0;
  transition: opacity .25s var(--ease-out);
}
@supports (backdrop-filter: blur(1px)) {
  .book-nudge-backdrop { backdrop-filter: blur(3px); }
}
.book-nudge.is-open .book-nudge-backdrop { opacity: 1; }

.book-nudge-dialog {
  position: relative;
  z-index: 1;
  width: min(100%, 26rem);
  max-height: 88vh;
  overflow-y: auto;
  padding: clamp(1.8rem, 6vw, 2.4rem) clamp(1.4rem, 6vw, 2rem);
  border-radius: 1.1rem;
  text-align: center;
  background: linear-gradient(168deg, #fdfaf4 0%, var(--card) 60%, #f4ecdd 100%);
  box-shadow: 0 1px 2px rgba(93,74,44,.05), 0 30px 70px -28px rgba(37,29,18,.55);
  opacity: 0;
  transform: translateY(14px) scale(.98);
  transition: opacity .25s var(--ease-out), transform .25s var(--ease-out);
}
.book-nudge.is-open .book-nudge-dialog { opacity: 1; transform: none; }
.book-nudge-dialog:focus { outline: none; }
.book-nudge-dialog:focus-visible { outline: 2.5px solid var(--gold-text); outline-offset: 4px; }

.book-nudge-close {
  position: absolute;
  top: .5rem;
  right: .5rem;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  /* 2.75rem = 44px at the default root size — the accessible touch-target
     floor, matching .lodge-lightbox-close. */
  width: 2.75rem;
  height: 2.75rem;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--stone-text);
  cursor: pointer;
  transition: background .2s var(--ease-soft), color .2s var(--ease-soft), transform .2s var(--ease-soft);
}
.book-nudge-close:hover,
.book-nudge-close:focus-visible { background: rgba(196,159,108,.16); color: var(--gold-text); }
.book-nudge-close:hover { transform: scale(1.06); }
.book-nudge-close:focus-visible { outline: 2px solid var(--gold-text); outline-offset: 2px; }
.book-nudge-close svg { width: 42%; height: 42%; }

.book-nudge-eyebrow { margin-bottom: .7rem; }

.book-nudge-title {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 5.6vw, 1.8rem);
  font-weight: 500;
  line-height: 1.2;
  color: var(--ink);
}

.book-nudge-body {
  margin-top: .9rem;
  font-size: var(--t-body);
  line-height: 1.6;
  color: var(--ink-soft);
}
.book-nudge-body strong { color: var(--gold-text); font-weight: 600; }

.book-nudge-cta { margin-top: 1.7rem; }

.book-nudge-dismiss {
  display: block;
  margin: .9rem auto 0;
  border: 0;
  padding: .4rem;
  background: none;
  font-family: var(--font-text);
  font-size: var(--t-small);
  color: var(--stone-text);
  text-decoration: underline;
  text-underline-offset: .25em;
  text-decoration-thickness: 1px;
  cursor: pointer;
  transition: color .3s var(--ease-soft);
}
.book-nudge-dismiss:hover, .book-nudge-dismiss:focus-visible { color: var(--ink); }


/* §5 (the photo strip) is gone — the mosaic in §3.6 replaced it. */


/* ==========================================================================
   5.5 · SUB-PAGES (any page with no envelope and no hero of its own)
   Reuse .sheet / .sheet-inner / .sheet-title / .sheet-lede / .flow-btn as-is;
   this section is only what those pages need on top.
   ========================================================================== */

/* Clearance for the fixed hero nav (CSS §1.5), which occupies no layout
   space of its own. travel/index.html is the only .page-sub page today,
   and §11.1 zeroes this padding right back out there — its hero-nav sits
   over that page's own full-bleed hero, the same as index.html, so there
   is nothing to reserve room for. This stays in place as the generic
   fallback for any future .page-sub page with no full-bleed hero of its
   own, where the nav would otherwise sit directly over the page title.
   --nav-h is published by that page's own copy of the hero-nav script
   from the bar's real rendered height, not a guessed constant — see
   travel.js and index.html's hero-nav.js. The 100px fallback covers a
   generous worst case for the moment before the script runs. */
.page-sub .paper { margin-top: 0; padding-top: calc(var(--nav-h, 100px) + .9rem + var(--safe-t)); }
.page-sub .paper::before { inset: 0; }


/* ==========================================================================
   6 · END
   ========================================================================== */

.end-sheet {
  text-align: center;
  padding-top: clamp(3rem, 10vh, 5.5rem);
  padding-bottom: calc(clamp(3.5rem, 12vh, 6.5rem) + var(--safe-b));
}
.end-divider { width: 11rem; max-width: 54%; height: auto; margin: 0 auto clamp(1.6rem, 6vw, 2.4rem); }
.end-names {
  font-family: var(--font-display);
  font-size: clamp(1.45rem, 5.2vw, 1.8rem);
  font-weight: 600;            /* tracked caps need weight to survive */
  /* Was .24em — the widest tracked-caps value on the site, on the one
     tracked-caps label that isn't --t-micro-sized. In absolute pixels
     that was nearly double the eyebrow's gap; brought down to match the
     other "important, not tiny" tracked labels (.flow-btn, .cue-label). */
  letter-spacing: .18em;
  text-indent: .18em;
  text-transform: uppercase;
  color: var(--ink);
}
.end-meta {
  margin-top: 1rem;
  font-size: clamp(1rem, 3.6vw, 1.08rem);
  font-weight: 400;
  letter-spacing: .1em;
  color: var(--ink-soft);
}
.end-dot { margin: 0 .55em; color: var(--stone); }
.end-note {
  margin: 1.9rem auto 0;
  max-width: 26rem;
  font-size: var(--t-body);
  font-weight: 400;
  font-style: italic;
  line-height: 1.62;
  color: var(--ink-soft);
  text-wrap: pretty;
}

/* The footer's quiet links: the calendar catch-all, and the page links that
   are the no-JavaScript fallback for the site nav (which cannot show itself
   without a script to gate it off the hero). Both are text, both are quiet,
   and neither is allowed to turn the end card into a menu. */
.end-cal { margin-top: 1.8rem; }
.end-link {
  display: inline-block;
  padding: .5rem .4rem;          /* 44px tap target on a text link */
  font-size: var(--t-small);
  color: var(--stone-text);
  text-decoration: underline;
  text-underline-offset: .3em;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--rule);
  transition: color .3s var(--ease-soft), text-decoration-color .3s var(--ease-soft);
}
.end-link:hover, .end-link:focus-visible {
  color: var(--ink);
  text-decoration-color: var(--gold-text);
}
.end-nav {
  margin-top: .9rem;
  font-size: var(--t-small);
}
.end-nav a {
  padding: .4rem;
  color: var(--stone-text);
  text-decoration: underline;
  text-underline-offset: .3em;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--rule);
  transition: color .3s var(--ease-soft), text-decoration-color .3s var(--ease-soft);
}
.end-nav a:hover, .end-nav a:focus-visible {
  color: var(--ink);
  text-decoration-color: var(--gold-text);
}


/* ==========================================================================
   7 · AUDIO CONTROL
   ========================================================================== */

.audio-toggle {
  position: fixed;
  z-index: 60;
  left: calc(1rem + var(--safe-l));
  bottom: calc(1rem + var(--safe-b));
  /* 2.6rem is 41.6px — under the 44px floor, on the one control that
     floats over scrolling content and has to be hit on the move. */
  width: 2.85rem;
  height: 2.85rem;
  display: grid;
  place-items: center;
  border: 1px solid rgba(157,144,120,.5);
  border-radius: 50%;
  background: rgba(251,247,240,.88);
  -webkit-backdrop-filter: blur(8px);
          backdrop-filter: blur(8px);
  color: var(--ink-soft);
  cursor: pointer;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity .8s var(--ease-out), transform .8s var(--ease-out),
              background .4s ease, color .4s ease;
}
.audio-toggle.is-shown { opacity: 1; transform: none; }
.audio-toggle:hover { background: rgba(251,247,240,.95); color: var(--ink); }
.audio-toggle svg { width: 1.15rem; height: 1.15rem; }
.audio-toggle .ico-on { display: none; }
.audio-toggle[aria-pressed="true"] .ico-on  { display: block; }
.audio-toggle[aria-pressed="true"] .ico-off { display: none; }


/* ==========================================================================
   8 · RESPONSIVE
   ========================================================================== */

/* --- reframing the painting per breakpoint ------------------------------
   The artwork is 4:3 and almost no viewport is, so `object-position` is
   doing real compositional work. On a wide screen `cover` crops height, so
   only the vertical number matters; on a phone it crops width brutally —
   roughly a third of the painting survives — so the horizontal number
   decides what the picture is even about.

   Biasing right keeps the lodge, the tent and the ceremony chairs. Going
   too far right (we tried 74%) throws away the mountain gap and the lake
   opening, which is the subject the whole composition is built around.
   These values hold both: the valley on the left of frame, the lodge and
   tent on the right. */
@media (max-width: 1023px) {
  :root { --hero-framing: 58% 48%; }
}

@media (max-width: 767px) {
  :root { --hero-framing: 66% 50%; }

  /* The phone hero is not the reference 4:3 crop, so the composition opens
     up: the divider and the metadata grow against the title to stay legible.
     They grow together — 240/322 is the reference ratio between them. */
  .hero-content { width: min(84%, 1003px); }
  .hero-divider { width: 46%; margin-top: 1.1rem; }
  .hero-meta { margin-top: .85rem; }
  .hero-meta-art { width: 34.3%; }   /* 46% x 240/322 */
}

@media (max-width: 480px) {
  :root { --hero-framing: 69% 50%; }
}

/* .audio-toggle sits fixed in the bottom-left corner; .hero-cue is
   centred and sized to its own "Scroll for detail" label. Measured in a
   real render: the two hit targets physically overlap by 17px at 320px
   width and are within 3px of touching at 360px — a tap meant for one can
   land on the other. Trimming the cue's horizontal padding (its tap
   target, not its visible text) buys back some of that on its own; below
   350px it isn't quite enough, so the cue also steps a few px right of
   dead-centre, which is imperceptible against a column already reading
   as centred at a glance. */
@media (max-width: 400px) {
  .hero-cue { padding-inline: .8rem; }
}
@media (max-width: 350px) {
  .hero-cue { left: calc(50% + 20px); }
}

/* Stack the actions only once they genuinely run out of width, and give
   them the full column so both stay comfortably thumb-sized. */

/* Very short phones in landscape, or small windows. Wide and short crops
   like a desktop, so the framing goes back to centre. The actions stay —
   they are the point of the page — but the group tightens and the scroll
   cue steps aside rather than colliding with it. */
@media (max-height: 560px) and (max-width: 900px) {
  :root { --hero-framing: 50% 46%; }
  :root { --hero-anchor: 52%; }
  .hero-cue { display: none; }
}

/* --- tablet and up --- */
@media (min-width: 768px) {
  .field-row { grid-template-columns: 1fr 1fr; }
  .sheet-inner { max-width: 34rem; }
}

@media (min-width: 1024px) {
  .sheet-inner { max-width: 36rem; }
  .form-sheet { padding-top: clamp(6rem, 16vh, 9rem); padding-bottom: calc(5rem + var(--safe-b)); }
  .end-sheet { padding-top: 4rem; }
}

/* Stack the form actions comfortably and keep the stepper from crowding
   on the narrowest phones. */
@media (max-width: 380px) {
  .stepper-dots { gap: .4rem; }
  .stepper-dots li { gap: .4rem; }
  /* Used to shrink the dot itself below the 44px floor to fit narrow
     phones — four dots at the full 2.75rem plus the connectors between
     them still fit inside 320px, so only the gap needs to tighten. */
  .flow-btn { padding: .95em 1.5em; letter-spacing: .12em; text-indent: .12em; }
}

/* --- forced colours / high contrast --------------------------------------
   Every state on this page is carried by a colour or a soft shadow, both of
   which forced-colours mode discards. These give the same states a shape. */
@media (prefers-contrast: more) {
  :root {
    --ink-soft:   #4a443b;
    --stone-text: #514a3e;
    --gold-text:  #61481f;
    --rule:       #9c8e78;
  }
  .field input, .field textarea { border-width: 2px; }
  .flow-btn { border-width: 2px; }
}
@media (forced-colors: active) {
  .flow-btn { border: 2px solid ButtonBorder; }
  .flow-btn__ink { display: none; }          /* the flood cannot be tinted */
  .step-dot { border: 2px solid ButtonBorder; }
  .step-dot.is-current { border-color: Highlight; }
  .stepper-rail-fill, .step-link::after { background: Highlight; }
  .field.has-error input, .field.has-error textarea { border-style: dashed; }
}


/* ==========================================================================
   9 · REDUCED MOTION
   Everything still happens — it just stops moving.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }

  /* Belt and braces: save-the-date.js hides the whole scene outright under
     reduced motion before any of this could ever run — pinning it here
     costs nothing. */
  .env-scene { transition: opacity .5s linear !important; }
  .env { transition: opacity .45s linear !important; }

  .page { transition: opacity .5s linear !important; }
  html.is-sealed .page { transform: none; }

  /* The hero arrives whole: no stagger, no lift. The runway and the sticky
     hand-off are gone for everyone now, so the only thing left to undo
     here is the staged entrance. */
  .hero-media,
  .hero-title,
  .hero-divider,
  .hero-meta,
  .hero-cue { opacity: 1; transform: none; transition: none !important; }
  .hero-cue { transform: translateX(-50%); opacity: .85; }
  .hero-content { opacity: 1; transform: translate(-50%, -50%); }

  /* The flow button keeps its meaning and loses its choreography: no arrow
     flight, no radius morph, no expanding circle. The state change is a
     plain fill, which is the part that actually carries information.
     A 140ms crossfade is re-allowed on top of the global .01ms clamp —
     "reduce" asks for less motion, not for instant snapping. */
  .flow-btn {
    transition: background-color .14s linear, color .14s linear !important;
  }
  .flow-btn__arrow { display: none; }
  .flow-btn__label, .flow-btn__icon { transform: none !important; }
  .flow-btn__ink { display: none; }
  .flow-btn:hover, .flow-btn:focus-visible, .flow-btn:active {
    border-radius: 100px;
    background: var(--fb-ink);
    border-color: var(--fb-ink);
    color: var(--fb-on-ink);
  }
  .flow-btn:hover .flow-btn__icon,
  .flow-btn:focus-visible .flow-btn__icon,
  .flow-btn:active .flow-btn__icon { color: var(--fb-on-ink); }
  .flow-btn:active { transform: none; }

  /* Steps still change, they just do not slide or pop. */
  .gform.is-stepped .step { animation: none; }
  html.js .gform.is-stepped .step-q .word {
    animation: none; opacity: 1; filter: none; transform: none; will-change: auto;
  }
  .form-done { animation: none; }
  .step-dot.is-done::after, .done-mark::after { animation: none; }
  .step-dot.is-current { transform: none; }

  /* The invitation line is simply there — every letter, at once, in place.
     The script bails before it publishes any progress, so pinning the
     variable here is belt and braces rather than a second source of truth. */
  html.js .celebrate { --celebrate-p: 1 !important; }
  html.js .celebrate .word { filter: none; transform: none; will-change: auto; }

  /* The mosaic is assembled, not assembling: --mosaic-p pinned at 1 puts
     every card at its final position, the runway collapses to one screen,
     and the sticky pane becomes an ordinary block. The JS bails before it
     ever installs a scroll listener, so nothing fights this. */
  .mosaic { height: auto; --mosaic-p: 1 !important; }
  .mosaic-viewport { position: relative; height: auto; padding: 4rem 0; }
  .mosaic-stage { height: min(80svh, 46rem); }
  .m-card { will-change: auto; }
  .cue-arrow { animation: none; opacity: .9; }

}


/* ==========================================================================
   10 · PRINT
   Someone's grandmother will print this. Chrome (the envelope, the nav,
   the audio toggle, the scroll cue, the mosaic's flying satellites) has no
   job on paper — the content underneath it does. Background images are
   dropped outright: they cost ink for no information and most home
   printers render them badly or not at all.
   ========================================================================== */

@media print {
  .env-scene,
  .hero-nav,
  .audio-toggle,
  .hero-cue,
  .skip-link { display: none !important; }

  /* The painting is the one background image worth the ink — everything
     else (the paper's grain texture, the envelope's gradients, card
     shadows) is decoration that print strips for free by dropping
     background-image and box-shadow globally below. */
  .hero-media { position: static; }
  .hero-video { display: none; }

  * , *::before, *::after {
    box-shadow: none !important;
    text-shadow: none !important;
  }
  .paper::before { display: none; }   /* the paper-grain noise texture */

  body { background: #fff; color: #000; }

  /* The mosaic's scroll choreography has no meaning on a static page —
     lay every card out in normal flow, at its natural size, so all six
     print rather than five of them being cropped satellites mid-flight. */
  .mosaic-viewport, .mosaic-stage { position: static; height: auto; }
  .m-card {
    position: static;
    width: 100% !important;
    height: auto !important;
    aspect-ratio: 4 / 3;
    margin-bottom: 1rem;
    page-break-inside: avoid;
  }

  a { color: inherit; text-decoration: underline; }
  /* A printed page cannot follow a link, so a URL worth having is worth
     printing next to the words that carry it. Scoped to the content
     links most likely to matter on paper (booking, mailto, tel); nav
     chrome above is already display:none. */
  .form-status a[href]::after,
  .end-link[href]::after { content: " (" attr(href) ")"; font-size: .85em; }

  /* The lodging gallery and its lightbox have no job on paper — the
     static no-JS fallback list is the whole answer, and it prints even
     though html.js's is-superseded rule (§11.4) has hidden it on screen. */
  .lodge-carousel, .lodge-lightbox { display: none !important; }
  .lodge-fallback { display: block !important; }
}


/* ==========================================================================
   11 · TRAVEL PAGE
   /travel/ reuses every token and structural class above — .paper, .sheet,
   .sheet-inner, .flow-btn, .hero-nav — and adds only what those don't
   cover. Grows a subsection at a time as the page does; see travel.js.
   ========================================================================== */

/* --- 11.1 · Page background ---
   This page reads white, not the site's tan --paper: the hero photo
   fades into it directly (11.2), so anything less than true white would
   show a seam where the mask ends. Scoped to body.travel-page rather
   than touching --paper itself, which the save-the-date still needs at
   its original tan.

   .page-sub .paper's padding-top (§7) reserved clearance for a full-width
   nav bar; the nav is a floating fixed pill now (it overlays instead of
   sitting in flow), so that reserved space would just be a blank white
   strip above the photo. Zeroed here, scoped to the travel page only —
   any future .page-sub page without a full-bleed hero still gets the
   original clearance. */
body.travel-page .paper { background: #fff; padding-top: 0; }

/* --- 11.2 · Hero ---
   Opens full-bleed on the Glacier Image — right at the top of the
   viewport, under the floating nav pill, with the peak itself as the
   top edge of the frame (the source file is cropped to the summit; see
   the note by .travel-hero-media below) — then masked so its foot
   dissolves into the white page background rather than cutting to a
   hard edge. Quieter than the save-the-date's hero — no calendar action
   — but keeps the location map, a small tactile "here's where" before
   the page gets to Where to Stay. */
.travel-hero { text-align: center; }

/* Full-bleed, edge to edge — breaks out of .travel-hero-inner's measure
   deliberately, the one element on this page that isn't gutter-padded.
   The mask (not a solid overlay) is what makes the fade actually read as
   the photo dissolving into paper rather than a rectangle laid over it;
   it only works because the page behind it is the same white the mask
   fades to (11.1).

   aspect-ratio is set to the source file's own ratio (1537:760), not a
   round design number — with the box shaped exactly like the image,
   object-fit: cover has nothing left to crop, at any viewport width.
   An earlier version filled the viewport height with flex-grow, which
   forced a vertical crop on short/wide windows and ate the boat at the
   bottom of the frame; matching the ratio exactly is what guarantees
   the whole photo — summit to boat — is always in frame. */
.travel-hero-media { width: 100%; line-height: 0; }
.travel-hero-media img {
  width: 100%;
  aspect-ratio: 1537 / 760;
  object-fit: cover;
  -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent 94%);
          mask-image: linear-gradient(to bottom, #000 62%, transparent 94%);
}

.travel-hero-inner {
  max-width: 34rem;
  margin: 0 auto;
  padding: 1.4rem var(--gutter) clamp(3rem, 9vh, 4.5rem);
}
.travel-hero-title {
  margin-top: .6rem;
  font-family: var(--font-display);
  font-size: clamp(2.4rem, 10vw, 3.6rem);
  font-weight: 500;
  line-height: 1.05;
  /* Same tier and same treatment as .sheet-title — this reaches the
     largest size of the two, so it gets the tighter neighbor's value. */
  letter-spacing: -.008em;
  color: var(--ink);
  text-wrap: balance;
}
.travel-hero-divider { width: 9rem; height: auto; margin: 1.2rem auto 0; }
.travel-hero-lede {
  margin: 1.1rem auto 0;
  max-width: 26rem;
  font-size: clamp(var(--t-body), 3.6vw, 1.1rem);
  line-height: 1.6;
  color: var(--ink-body);
  text-wrap: pretty;
}

/* --- 11.2b · Location map ---
   Replaces the old countdown clock. A small card that tilts toward the
   cursor (desktop hover only — see locationMap() in travel.js) and
   expands on select into a real map, built from OpenStreetMap-family
   tiles, centered on Whitefish. Colors are pulled from the site's own
   cream/gold/ink palette rather than the neon-on-dark treatment a map
   widget like this usually gets, so it reads as part of the page
   instead of an embedded dev tool. */
.location-map {
  display: flex;
  justify-content: center;
  margin: clamp(2rem, 7vw, 2.8rem) auto 0;
  perspective: 1000px;
}
.location-map-fallback { margin: 0; font-size: var(--t-small); color: var(--stone-text); }

.location-map-card {
  position: relative;
  display: block;
  width: clamp(200px, 58vw, 240px);
  height: clamp(122px, 34vw, 140px);
  padding: 0;
  border: 1px solid rgba(196,159,108,.3);
  border-radius: 1.1rem;
  background: linear-gradient(168deg, rgba(253,250,244,.92) 0%, rgba(244,236,221,.78) 100%);
  box-shadow: 0 1px 2px rgba(93,74,44,.05), 0 14px 30px -22px rgba(93,74,44,.45);
  overflow: hidden;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transform-style: preserve-3d;
  transition: width .5s cubic-bezier(.34,1.56,.64,1), height .5s cubic-bezier(.34,1.56,.64,1), box-shadow .3s var(--ease-soft);
}
.location-map-card:hover,
.location-map-card:focus-visible {
  box-shadow: 0 2px 4px rgba(93,74,44,.08), 0 18px 34px -20px rgba(93,74,44,.55);
}
.location-map-card:focus-visible { outline: 2px solid var(--gold-deep); outline-offset: 3px; }
.location-map-card.is-expanded {
  width: clamp(280px, 82vw, 360px);
  height: clamp(210px, 58vw, 280px);
}

.location-map-grid {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  color: var(--ink);
  opacity: .05;
  transition: opacity .3s var(--ease-soft);
}
.location-map-card.is-expanded .location-map-grid { opacity: 0; }

.location-map-tiles {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  transition: opacity .4s var(--ease-soft) .1s;
}
.location-map-card.is-expanded .location-map-tiles { opacity: 1; }
/* Muted toward the site's cream palette — see the note on `tileProvider`
   in travel-data.js: these are real OpenStreetMap tiles, full color by
   default, and this filter is what keeps them from reading as a raw
   embedded map dropped into an otherwise warm, hand-illustrated page. */
.location-map-tiles-inner {
  position: absolute;
  width: 768px;
  height: 768px;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  filter: saturate(.55) sepia(.16) brightness(1.04) contrast(.96);
}
.location-map-tile {
  position: absolute;
  width: 256px;
  height: 256px;
  opacity: 0;
  transition: opacity .3s var(--ease-soft);
}
.location-map-tile.is-loaded { opacity: 1; }

.location-map-fade {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transition: opacity .3s var(--ease-soft);
}
.location-map-fade--top { background: linear-gradient(to top, transparent 55%, rgba(253,250,244,.92) 100%); }
.location-map-fade--bottom { background: linear-gradient(to bottom, rgba(253,250,244,.5), transparent 45%); }
.location-map-card.is-expanded .location-map-fade { opacity: 1; }

.location-map-marker {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 3;
  display: block;
  width: 28px;
  height: 28px;
  opacity: 0;
  transform: translate(-50%, -70%) scale(0);
  filter: drop-shadow(0 2px 6px rgba(93,74,44,.5));
  pointer-events: none;
  transition: opacity .3s ease, transform .3s ease;
}
.location-map-marker svg { display: block; width: 100%; height: 100%; }
.location-map-marker-hole { fill: #fff8ec; }
.location-map-card.is-expanded .location-map-marker {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
  transition: opacity .3s ease .28s, transform .5s cubic-bezier(.34,1.56,.64,1) .28s;
}

.location-map-content {
  position: relative;
  z-index: 4;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  height: 100%;
  padding: .9rem 1rem;
  text-align: left;
}
.location-map-icon {
  display: inline-flex;
  width: 18px;
  height: 18px;
  color: var(--gold-text);
  opacity: 1;
  transition: opacity .3s var(--ease-soft);
}
.location-map-icon svg { width: 100%; height: 100%; }
.location-map-card.is-expanded .location-map-icon { opacity: 0; }

.location-map-text { display: block; }
.location-map-title {
  display: block;
  font-family: var(--font-text);
  font-size: var(--t-small);
  font-weight: 600;
  color: var(--ink);
  transition: transform .3s var(--ease-out);
}
.location-map-card:hover .location-map-title { transform: translateX(3px); }
.location-map-coords {
  display: block;
  overflow: hidden;
  max-height: 0;
  margin-top: 0;
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: var(--t-micro);
  color: var(--stone-text);
  opacity: 0;
  transition: max-height .3s var(--ease-soft), opacity .25s var(--ease-soft), margin-top .3s var(--ease-soft);
}
.location-map-card.is-expanded .location-map-coords { max-height: 1.4em; margin-top: .2rem; opacity: 1; }
.location-map-underline {
  display: block;
  height: 1px;
  margin-top: .4rem;
  background: linear-gradient(90deg, var(--gold-deep) 0%, rgba(180,140,80,.35) 60%, transparent 100%);
  transform: scaleX(.3);
  transform-origin: left;
  transition: transform .4s var(--ease-out);
}
.location-map-card:hover .location-map-underline,
.location-map-card.is-expanded .location-map-underline { transform: scaleX(1); }

@media (prefers-reduced-motion: reduce) {
  .location-map-card,
  .location-map-grid,
  .location-map-tiles,
  .location-map-tile,
  .location-map-fade,
  .location-map-marker,
  .location-map-icon,
  .location-map-title,
  .location-map-coords,
  .location-map-underline { transition: none !important; }
}

/* --- 11.3 · shared section rhythm --- */
.travel-section { padding-top: clamp(4rem, 12vh, 6rem); padding-bottom: clamp(2.25rem, 6.5vh, 3.25rem); }
.travel-section + .travel-section { padding-top: clamp(2.25rem, 6.5vh, 3.25rem); }
/* Only the first section inherits enough top padding from .page-sub .paper
   (above) to already clear the fixed nav; the second and third sections'
   own padding-top is smaller and was never meant to carry that job alone.
   scroll-margin-top is what actually matters here: it's what a hash link
   or a restored scroll position to #where-to-stay/#the-weekend/#questions
   lands against (none of the three is in the hero-nav's menu any more,
   but all three ids are still real anchors other links and bookmarks can
   still point at), not the padding used for the first paint. Measured
   against the fixed nav's real rendered height via --nav-h, not a
   hardcoded constant — see the note by .travel-section above. */
.travel-section { scroll-margin-top: calc(var(--nav-h, 100px) + .75rem + var(--safe-t)); }

/* --- 11.4 · Where to Stay (lodging gallery + lightbox) --- */

/* Context for the $ signs on every lodging card — see travel-data.js
   priceScale. Quiet meta text, not an alert like .lodge-urgency below. */
.lodge-price-legend {
  margin-top: .6rem;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--stone-text);
  text-align: center;
}
.lodge-price-legend:empty { display: none; }

/* The "book by" urgency line — see travel-data.js lodgingBookBy for
   where the copy comes from. A soft callout, not an alarm: it's real,
   useful guidance, not a warning. */
.lodge-urgency {
  margin-top: clamp(1rem, 4vw, 1.4rem);
  padding: .7em 1.2em;
  border-radius: .7rem;
  background: linear-gradient(168deg, rgba(253,250,244,.88) 0%, rgba(244,236,221,.72) 100%);
  border: 1px solid rgba(196,159,108,.24);
  box-shadow: 0 1px 2px rgba(93,74,44,.04), 0 12px 26px -22px rgba(93,74,44,.4);
  -webkit-backdrop-filter: blur(12px);
          backdrop-filter: blur(12px);
  font-size: var(--t-small);
  font-weight: 500;
  line-height: 1.5;
  color: var(--gold-text);
  text-align: center;
}
.lodge-urgency:empty { display: none; }

/* Expand-on-select carousel, not a filtered/reflowing grid — one row of
   five panels on tablet+, stacked into one column on phones (the site's
   own mobile-first rule from the file header applies here too). One
   panel is active at a time; travel.js drives which. */
.lodge-carousel {
  display: flex;
  flex-direction: column;
  gap: .7rem;
  height: clamp(30rem, 118vw, 36rem);
  margin-top: clamp(2rem, 6vw, 2.6rem);
  list-style: none;
}
@media (min-width: 768px) {
  .lodge-carousel {
    flex-direction: row;
    height: clamp(21rem, 32vw, 27rem);
    gap: .55rem;
  }
}

/* Same fade + rise as .weekend-card — travel.js toggles .is-revealed
   per panel, staggered slightly, the first time each scrolls into view.
   flex-grow lives HERE, on the <li>, not on the button inside it — the
   <li> is the direct child of the flex row/column in .lodge-carousel, so
   it's the only element whose flex-grow actually reflows its siblings.
   Split into longhand so travel.js can stagger just the reveal (opacity,
   transform) without also delaying every hover-triggered expand by the
   same stagger — flex-grow always transitions with zero delay. */
.lodge-carousel-item {
  display: flex;
  flex: 1 1 0%;
  min-height: 0;
  min-width: 0;
  opacity: 0;
  transform: translateY(18px);
  transition-property: opacity, transform, flex-grow;
  transition-duration: .6s, .6s, .6s;
  transition-timing-function: var(--ease-out), var(--ease-out), var(--ease-soft);
}
.lodge-carousel-item.is-revealed { opacity: 1; transform: none; }
/* The hover/focus preview is desktop-only — see the (hover: hover) note
   below on the photo zoom. Below 768px, panels are a fixed-height
   column: two panels independently satisfying "expand" at once (one
   hovered, one is-active from a prior tap) would each claim a share of
   that fixed height and cramp both. Gating hover out down there leaves
   .is-active — always exactly one panel, set by travel.js — as the only
   thing that expands a panel on touch. */
@media (hover: hover) and (pointer: fine) and (min-width: 768px) {
  .lodge-carousel-item:hover,
  .lodge-carousel-item:focus-within { flex-grow: 5; }
}
.lodge-carousel-item.is-active { flex-grow: 5; }

.lodge-carousel-panel {
  position: relative;
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  width: 100%;
  min-height: 3.6rem;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: .9rem;
  background: var(--card);
  overflow: hidden;
  font: inherit;
  text-align: left;
  color: inherit;
  cursor: pointer;
  transition: box-shadow .3s var(--ease-soft), border-color .3s var(--ease-soft);
}
@media (hover: hover) and (pointer: fine) and (min-width: 768px) {
  .lodge-carousel-item:hover .lodge-carousel-panel,
  .lodge-carousel-item:focus-within .lodge-carousel-panel {
    box-shadow: 0 18px 34px -22px rgba(93,74,44,.4);
    border-color: var(--gold);
  }
}
.lodge-carousel-item.is-active .lodge-carousel-panel {
  box-shadow: 0 18px 34px -22px rgba(93,74,44,.4);
  border-color: var(--gold);
}
.lodge-carousel-panel:focus-visible { outline: 2.5px solid var(--gold-text); outline-offset: 3px; }

.lodge-carousel-media {
  position: absolute;
  inset: 0;
  /* Same gradient as the mosaic's "photograph to come" card — one
     placeholder language, not two. */
  background: linear-gradient(158deg, #f4eee1 0%, var(--paper-deep) 100%);
}
.lodge-carousel-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .6s var(--ease-out);
}
/* Hover zoom is desktop-only — (hover: hover) keeps a lingering touch
   from leaving the image scaled on a phone with no way to "unhover" it. */
@media (hover: hover) and (pointer: fine) and (min-width: 768px) {
  .lodge-carousel-item:hover .lodge-carousel-media img,
  .lodge-carousel-item:focus-within .lodge-carousel-media img { transform: scale(1.05); }
}
.lodge-carousel-item.is-active .lodge-carousel-media img { transform: scale(1.05); }

/* A bare icon centered in a lot of empty space reads as "nothing
   loaded" rather than a considered placeholder — the soft vignette and
   medallion ring below give it enough presence to read as intentional,
   without turning five identical panels into five competing focal points. */
.lodge-carousel-media.is-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  background-image:
    radial-gradient(circle at 50% 42%, rgba(158,156,132,.16) 0%, rgba(158,156,132,0) 62%),
    linear-gradient(158deg, #f4eee1 0%, var(--paper-deep) 100%);
}
.lodge-carousel-media.is-empty .m-empty-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: clamp(34px, 8vw, 52px);
  height: clamp(34px, 8vw, 52px);
  padding: clamp(9px, 2vw, 13px);
  border: 1px solid rgba(158,156,132,.38);
  border-radius: 50%;
  color: var(--leaf);
  opacity: .85;
}
.lodge-carousel-media.is-empty .m-empty-mark svg { width: 100%; height: 100%; }
.lodge-carousel-media.is-empty .m-empty-note {
  font-size: var(--t-micro);
  font-style: italic;
  color: var(--stone-text);
  opacity: 0;
  transition: opacity .35s var(--ease-soft);
}
@media (hover: hover) and (pointer: fine) and (min-width: 768px) {
  .lodge-carousel-item:hover .lodge-carousel-media.is-empty .m-empty-note,
  .lodge-carousel-item:focus-within .lodge-carousel-media.is-empty .m-empty-note { opacity: 1; }
}
.lodge-carousel-item.is-active .lodge-carousel-media.is-empty .m-empty-note { opacity: 1; }

/* The shaded scrim the name/tagline sit on — always present, low and
   soft when collapsed, deeper once the panel is active/hovered so the
   full label stays readable over a bright photo. */
.lodge-carousel-shadow {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 55%;
  background: linear-gradient(to top, rgba(35,27,16,.72), rgba(35,27,16,0));
  pointer-events: none;
  opacity: .7;
  transition: opacity .4s var(--ease-soft);
}
@media (hover: hover) and (pointer: fine) and (min-width: 768px) {
  .lodge-carousel-item:hover .lodge-carousel-shadow,
  .lodge-carousel-item:focus-within .lodge-carousel-shadow { opacity: 1; }
}
.lodge-carousel-item.is-active .lodge-carousel-shadow { opacity: 1; }

/* Badges pin over the photo regardless of where they fall in panel's
   own document flow — position: absolute lifts them into the same
   painted-after-static-content layer as .lodge-carousel-media, with a
   higher z-index so they land above it rather than under it. */
.lodge-gallery-badges {
  position: absolute;
  top: .6rem;
  left: .6rem;
  right: .6rem;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: .5rem;
  /* Collapsed panels in the mobile column are short enough that a
     top-pinned badge and the bottom-pinned .lodge-carousel-spine name
     would overlap (see the note on that element) — so on phones, badges
     only show once a panel is active. Desktop's collapsed panels are
     narrow-but-tall columns with room for both, so badges stay shown
     there unconditionally, as they always have. */
  opacity: 0;
  transition: opacity .3s var(--ease-soft);
}
.lodge-carousel-item.is-active .lodge-gallery-badges { opacity: 1; }
@media (min-width: 768px) {
  .lodge-gallery-badges { opacity: 1; }
}

/* Collapsed name — always visible so a guest can tell the five
   properties apart before expanding any of them. A vertical spine on
   the desktop row (where a collapsed panel is narrow and tall); a small
   horizontal bar on the phone column (where a collapsed panel is wide
   and short) — same element, the breakpoint just changes its axis.
   Fades out the moment .lodge-carousel-label takes over. */
.lodge-carousel-spine {
  position: absolute;
  left: .8rem;
  bottom: .6rem;
  z-index: 1;
  max-width: calc(100% - 1.6rem);
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 600;
  color: #fff8ec;
  text-shadow: 0 1px 6px rgba(0,0,0,.5);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  opacity: 1;
  transition: opacity .35s var(--ease-soft);
}
@media (min-width: 768px) {
  .lodge-carousel-spine {
    left: .6rem;
    top: .8rem;
    bottom: .8rem;
    max-width: none;
    max-height: calc(100% - 1.6rem);
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    text-overflow: clip;
  }
}
@media (hover: hover) and (pointer: fine) and (min-width: 768px) {
  .lodge-carousel-item:hover .lodge-carousel-spine,
  .lodge-carousel-item:focus-within .lodge-carousel-spine { opacity: 0; }
}
.lodge-carousel-item.is-active .lodge-carousel-spine { opacity: 0; }

/* The full label — name, tagline, price — that replaces the spine the
   moment a panel is hovered, focused, or made active. This is the part
   that changes as you move between panels: it fades and slides in on
   whichever one is expanded, and back out the instant it isn't. */
.lodge-carousel-label {
  position: relative;
  z-index: 1;
  margin-top: auto;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: .6rem;
  width: 100%;
  padding: .8rem .9rem;
}
.lodge-carousel-text {
  display: flex;
  flex-direction: column;
  gap: .15rem;
  min-width: 0;
  opacity: 0;
  transform: translateX(14px);
  transition: opacity .5s var(--ease-out), transform .5s var(--ease-out);
}
@media (hover: hover) and (pointer: fine) and (min-width: 768px) {
  .lodge-carousel-item:hover .lodge-carousel-text,
  .lodge-carousel-item:focus-within .lodge-carousel-text { opacity: 1; transform: none; }
}
.lodge-carousel-item.is-active .lodge-carousel-text { opacity: 1; transform: none; }
.lodge-carousel-name {
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 2.6vw, 1.3rem);
  font-weight: 600;
  line-height: 1.2;
  color: #fff8ec;
}
.lodge-carousel-sub {
  font-size: var(--t-small);
  line-height: 1.35;
  color: rgba(255,248,236,.84);
}
.lodge-carousel-price {
  flex: none;
  font-size: var(--t-small);
  font-weight: 600;
  letter-spacing: .05em;
  color: #fff8ec;
  opacity: 0;
  transition: opacity .5s var(--ease-out);
}
@media (hover: hover) and (pointer: fine) and (min-width: 768px) {
  .lodge-carousel-item:hover .lodge-carousel-price,
  .lodge-carousel-item:focus-within .lodge-carousel-price { opacity: .92; }
}
.lodge-carousel-item.is-active .lodge-carousel-price { opacity: .92; }

.lodge-gallery-badge {
  display: inline-flex;
  align-items: center;
  padding: .3em .7em;
  border-radius: 999px;
  background: radial-gradient(circle at 32% 28%, #d59a63, #b0743f 55%, #70441f 100%);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: .08em;
  color: #fff8ec;
}
/* "Details coming" reads as a status, not an achievement — a muted
   scrim badge rather than the gold treatment .lodge-gallery-badge
   otherwise gets, so the two are never mistaken for each other. */
.lodge-gallery-badge--pending {
  margin-left: auto;
  background: rgba(37,29,18,.62);
  font-weight: 500;
  letter-spacing: .04em;
  text-transform: none;
}

.lodge-fallback { margin-top: clamp(1.6rem, 5vw, 2.2rem); list-style: none; }
.lodge-fallback li {
  padding: .7rem 0;
  font-size: var(--t-small);
  line-height: 1.6;
  color: var(--ink-body);
  border-bottom: 1px solid rgba(205,188,166,.4);
}
.lodge-fallback li:last-child { border-bottom: none; }
.lodge-fallback strong { color: var(--ink); }
.lodge-fallback-todo { font-style: italic; color: var(--stone-text); }
html.js .lodge-fallback.is-superseded { display: none; }


/* ---- Lodging lightbox ---------------------------------------------------
   One dialog, reused for all five properties — see the markup note in
   travel/index.html and the open() function in travel.js. Desktop is a
   two-column dialog (media / detail panel); phone stacks them as a
   bottom sheet. Motion is a fade + slight rise, ~220ms, and the sitewide
   reduced-motion clamp (§9) already reduces its duration to ~0, so no
   separate reduced-motion override is needed here. */
.lodge-lightbox[hidden] { display: none; }
.lodge-lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--gutter);
}
html.lodge-lightbox-open,
html.lodge-lightbox-open body { overflow-y: hidden; }

.lodge-lightbox-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(37,29,18,.66);
  opacity: 0;
  transition: opacity .25s var(--ease-out);
}
@supports (backdrop-filter: blur(1px)) {
  .lodge-lightbox-backdrop { backdrop-filter: blur(3px); }
}
.lodge-lightbox.is-open .lodge-lightbox-backdrop { opacity: 1; }

.lodge-lightbox-dialog {
  position: relative;
  z-index: 1;
  display: grid;
  width: min(100%, 62rem);
  max-height: min(88vh, 46rem);
  border-radius: 1.1rem;
  background: var(--card);
  box-shadow: 0 1px 2px rgba(93,74,44,.05), 0 30px 70px -28px rgba(37,29,18,.55);
  overflow: hidden;
  opacity: 0;
  transform: translateY(14px) scale(.98);
  transition: opacity .25s var(--ease-out), transform .25s var(--ease-out);
}
.lodge-lightbox.is-open .lodge-lightbox-dialog { opacity: 1; transform: none; }
.lodge-lightbox-dialog:focus { outline: none; }
@media (min-width: 768px) {
  .lodge-lightbox-dialog { grid-template-columns: 1.1fr 1fr; max-height: min(84vh, 40rem); }
}

/* Phone: a near-bottom-sheet feel — full-bleed to the viewport edges,
   anchored low, scrollable as one column rather than a centered card. */
@media (max-width: 767px) {
  .lodge-lightbox { padding: 0; align-items: flex-end; }
  .lodge-lightbox-dialog {
    grid-template-rows: auto auto;
    width: 100%;
    max-height: 92svh;
    border-radius: 1.1rem 1.1rem 0 0;
    overflow-y: auto;
  }
}

.lodge-lightbox-close {
  position: absolute;
  top: .6rem;
  right: .6rem;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  /* 2.75rem = 44px at the default root size — the accessible touch-target
     floor (WCAG 2.5.5 / Apple HIG), not just whatever looked balanced
     against the icon glyph inside it. */
  width: 2.75rem;
  height: 2.75rem;
  border: 0;
  border-radius: 50%;
  background: rgba(37,29,18,.5);
  color: #fff8ec;
  cursor: pointer;
  transition: background .2s var(--ease-soft), transform .2s var(--ease-soft);
}
.lodge-lightbox-close:hover,
.lodge-lightbox-close:focus-visible { background: rgba(37,29,18,.75); }
.lodge-lightbox-close:hover { transform: scale(1.06); }
.lodge-lightbox-close:focus-visible { outline: 2px solid #fff8ec; outline-offset: 2px; }
.lodge-lightbox-close svg { width: 40%; height: 40%; }

.lodge-lightbox-media {
  position: relative;
  min-height: 15rem;
  background-image:
    radial-gradient(circle at 50% 42%, rgba(158,156,132,.16) 0%, rgba(158,156,132,0) 60%),
    linear-gradient(158deg, #f4eee1 0%, var(--paper-deep) 100%);
  overflow: hidden;
  /* The photo swipe (travel.js) reads pointermove for 1:1 drag tracking —
     pan-y leaves vertical page scroll to the browser but stops it from
     also trying to interpret the horizontal drag as a scroll gesture. */
  touch-action: pan-y;
}
.lightbox-photo-empty {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .8rem;
}
.lightbox-photo-empty .m-empty-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: clamp(76px, 13vw, 100px);
  height: clamp(76px, 13vw, 100px);
  padding: clamp(19px, 3.4vw, 25px);
  border: 1px solid rgba(158,156,132,.38);
  border-radius: 50%;
  color: var(--leaf);
  opacity: .85;
}
.lightbox-photo-empty .m-empty-mark svg { width: 100%; height: 100%; }
.lightbox-photo-empty .m-empty-note { font-size: var(--t-small); font-style: italic; color: var(--stone-text); }
.lightbox-photo-track {
  display: flex;
  height: 100%;
  transition: transform .35s var(--ease-out);
}
.lightbox-photo-track img { flex: 0 0 100%; width: 100%; height: 100%; object-fit: cover; }
.lightbox-photo-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border: 0;
  border-radius: 50%;
  background: rgba(37,29,18,.5);
  color: #fff8ec;
  cursor: pointer;
  transition: background .2s var(--ease-soft);
}
.lightbox-photo-nav:hover,
.lightbox-photo-nav:focus-visible { background: rgba(37,29,18,.75); }
.lightbox-photo-nav:focus-visible { outline: 2px solid #fff8ec; outline-offset: 2px; }
.lightbox-photo-nav--prev { left: .6rem; }
.lightbox-photo-nav--next { right: .6rem; }
.lightbox-photo-nav svg { width: 38%; height: 38%; }
.lightbox-photo-dots {
  position: absolute;
  bottom: .8rem;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: .4rem;
}
/* The visible dot stays small and quiet; padding pads the actual hit
   area out to a real touch target without inflating the dot itself. */
.lightbox-photo-dot {
  width: .5rem;
  height: .5rem;
  padding: .55rem;
  background-clip: content-box;
  border: 0;
  border-radius: 50%;
  background-color: rgba(255,248,236,.55);
  cursor: pointer;
  transition: background-color .2s var(--ease-soft);
}
.lightbox-photo-dot[aria-selected="true"] { background-color: #fff8ec; }
.lightbox-photo-dot:focus-visible { outline: 2px solid #fff8ec; outline-offset: 2px; }

.lodge-lightbox-panel {
  padding: 1.6rem 1.7rem clamp(1.6rem, 4vh, 2rem);
  overflow-y: auto;
}
.lodge-lightbox-badges { display: flex; flex-wrap: wrap; gap: .5rem; }
.lodge-lightbox-badges .lodge-gallery-badge { color: #fff8ec; }
.lodge-lightbox-name {
  margin-top: .7rem;
  font-family: var(--font-display);
  font-size: clamp(1.4rem, 3.4vw, 1.7rem);
  font-weight: 600;
  color: var(--ink);
}
.lodge-lightbox-price { margin-top: .2rem; font-size: var(--t-small); font-weight: 600; letter-spacing: .05em; color: var(--gold-text); }
.lodge-lightbox-blurb { margin-top: .9rem; font-size: var(--t-small); line-height: 1.65; color: var(--ink-body); }
.lodge-lightbox-blurb.is-todo { font-style: italic; color: var(--stone-text); }
.lodge-lightbox-proximity { margin-top: .6rem; font-size: var(--t-small); color: var(--ink-soft); }
.lodge-lightbox-proximity[hidden] { display: none; }
.lodge-lightbox-meta { margin-top: .9rem; font-size: var(--t-small); }
.lodge-lightbox-amenities {
  padding-bottom: .7rem;
  margin-bottom: .7rem;
  border-bottom: 1px solid rgba(205,188,166,.4);
  letter-spacing: .01em;
  color: var(--ink-soft);
}
.lodge-lightbox-contact { line-height: 1.7; color: var(--ink-soft); }
.lodge-lightbox-meta .is-todo { font-style: italic; color: var(--stone-text); }
.lodge-lightbox-actions { margin-top: 1.2rem; display: flex; flex-direction: column; gap: .7rem; }

.lodge-detail-code {
  display: inline-grid;
  place-items: center;
  padding: .5em 1em;
  border: 1px dashed var(--gold-text);
  border-radius: .6rem;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--gold-text);
  background: none;
  cursor: pointer;
}
/* Both labels share one grid cell — the box sizes to the taller of the
   two, so swapping which one is visible never shifts the layout. The
   toast (role="status", below) still carries the accessible "Copied."
   announcement; these are aria-hidden, purely the eye-level feedback
   right where the guest already clicked, faster to notice than a toast
   half a line away. */
.lodge-detail-code-label {
  grid-area: 1 / 1;
  transition: opacity .25s var(--ease-soft);
}
.lodge-detail-code-label--done { opacity: 0; color: var(--olive); }
.lodge-detail-code.is-copied .lodge-detail-code-label--done { opacity: 1; }
.lodge-detail-code.is-copied .lodge-detail-code-label:not(.lodge-detail-code-label--done) { opacity: 0; }
.lodge-detail-toast {
  margin-top: -.3rem;
  font-size: var(--t-micro);
  color: var(--olive);
  opacity: 0;
  transition: opacity .3s ease;
}
.lodge-detail-toast.is-shown { opacity: 1; }
.lodge-detail-meta { font-size: var(--t-small); line-height: 1.6; color: var(--ink-soft); }
.lodge-detail-meta.is-todo { font-style: italic; color: var(--stone-text); }
.lodge-detail-book { width: 100%; }
.lodge-detail-directions {
  align-self: flex-start;
  font-size: var(--t-small);
  color: var(--gold-text);
  text-decoration: underline;
  text-decoration-color: transparent;
  transition: text-decoration-color .25s var(--ease-soft);
}
.lodge-detail-directions:hover,
.lodge-detail-directions:focus-visible { text-decoration-color: currentColor; }

@media (prefers-reduced-motion: reduce) {
  .lodge-carousel-item { opacity: 1; transform: none; }
}

/* --- 11.5 · The Weekend ---
   Three cards, a fixed count (Friday/Saturday/Sunday — see the note on
   travel-data.js's schedule array), stacked on phones and tablets same
   as before. At 768px+ — the same breakpoint the lodging carousel above
   already switches on — they move into a 3-up row inside the section's
   own sheet-inner--wide (see travel/index.html), instead of staying a
   single ~36rem column stranded in the middle of a desktop viewport
   with the wide lodging gallery directly above it for comparison. */
.weekend-list { margin-top: clamp(2rem, 6vw, 2.6rem); list-style: none; }
@media (min-width: 768px) {
  .weekend-list {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    align-items: stretch;
    gap: var(--space-sm);
  }
}
/* Frosted-glass treatment: a warm, translucent card over the plain white
   .paper background rather than a flat fill — a soft gradient tint,
   a hairline gold-tinted border, backdrop blur (so anything that ever
   scrolls beneath, e.g. a future photo, reads through softly), and a
   deep, diffuse shadow instead of a hard one. Hover lifts the card and
   warms the border/shadow, giving the same quiet "alive" feedback as
   the rest of the site's interactive elements — this content isn't
   clickable, but it isn't inert either. */
.weekend-card {
  position: relative;
  padding: var(--space-md);
  margin-bottom: var(--space-sm);
  border-left: 3px solid var(--gold);
  border-radius: 0 1rem 1rem 0;
  background: linear-gradient(168deg, rgba(253,250,244,.85) 0%, rgba(251,247,240,.66) 100%);
  border-top: 1px solid rgba(196,159,108,.2);
  border-right: 1px solid rgba(196,159,108,.2);
  border-bottom: 1px solid rgba(196,159,108,.2);
  box-shadow: 0 1px 2px rgba(93,74,44,.04), 0 16px 34px -26px rgba(93,74,44,.4);
  -webkit-backdrop-filter: blur(14px);
          backdrop-filter: blur(14px);
  opacity: 0;
  transform: translateY(14px);
  transition:
    opacity .6s var(--ease-out),
    transform .45s var(--ease-soft),
    box-shadow .45s var(--ease-soft),
    border-color .45s var(--ease-soft);
}
@media (min-width: 768px) {
  /* The grid's own gap replaces the stacked list's margin-bottom; align-
     items: stretch (the grid default) then gives all three cards the
     same height regardless of which have real copy yet and which are
     still the "details coming" placeholder — no card reads as the odd
     one out just because its neighbor's sentence is longer. */
  .weekend-card { margin-bottom: 0; height: 100%; }
}
.weekend-card.is-revealed { opacity: 1; transform: none; }
.weekend-card:hover {
  transform: translateY(-3px);
  border-color: rgba(196,159,108,.42);
  box-shadow: 0 1px 2px rgba(93,74,44,.06), 0 26px 48px -26px rgba(93,74,44,.5);
}
/* A quiet display-face numeral (01/02/03) that gives each card in the
   3-up row a typographic anchor to differentiate it at a glance, rather
   than three identical-looking text blocks whose only difference is a
   small uppercase day label. Decorative only (aria-hidden — see
   travel.js), so it never duplicates weekend-day for a screen reader. */
.weekend-card-head { display: flex; align-items: baseline; gap: .6rem; }
.weekend-num {
  flex: none;
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 4vw, 1.9rem);
  font-weight: 600;
  line-height: 1;
  color: var(--gold);
  opacity: .6;
}
.weekend-day {
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--gold-text);
}
.weekend-date { margin-left: .5em; color: var(--stone-text); font-weight: 400; letter-spacing: normal; text-transform: none; }
.weekend-title {
  margin-top: .5rem;
  font-family: var(--font-display);
  font-size: 1.3rem;
  font-weight: 600;
  color: var(--ink);
}
.weekend-detail { margin-top: .5rem; font-size: var(--t-small); line-height: 1.6; color: var(--ink-body); }
.weekend-detail.is-todo { font-style: italic; color: var(--stone-text); }
.weekend-time { color: var(--gold-text); font-weight: 600; }
.weekend-detail a { color: var(--gold-text); text-decoration: underline; text-underline-offset: 2px; }
.weekend-detail a:hover, .weekend-detail a:focus-visible { color: var(--ink); }

@media (prefers-reduced-motion: reduce) {
  .weekend-card { opacity: 1; transform: none; }
  .weekend-card:hover { transform: none; }
}

/* --- 11.6 · Questions (FAQ + local contact) ---
   Same frosted-glass family as .weekend-card: each question becomes its
   own translucent card (gap between them replaces the old border-bottom
   dividers) with a hairline gold border, backdrop blur and a soft deep
   shadow. Hovering — or opening — lifts the card and warms its border,
   so the row reads as "alive" before a guest even taps it. */
.faq { margin-top: clamp(1.6rem, 5vw, 2rem); display: flex; flex-direction: column; gap: .8rem; }
.faq-item {
  border-radius: 1rem;
  background: linear-gradient(168deg, rgba(253,250,244,.85) 0%, rgba(251,247,240,.66) 100%);
  border: 1px solid rgba(196,159,108,.2);
  box-shadow: 0 1px 2px rgba(93,74,44,.04), 0 14px 30px -26px rgba(93,74,44,.38);
  -webkit-backdrop-filter: blur(14px);
          backdrop-filter: blur(14px);
  transition:
    transform .45s var(--ease-soft),
    box-shadow .45s var(--ease-soft),
    border-color .45s var(--ease-soft);
}
.faq-item:hover,
.faq-item[open] {
  transform: translateY(-3px);
  border-color: rgba(196,159,108,.42);
  box-shadow: 0 1px 2px rgba(93,74,44,.06), 0 24px 46px -26px rgba(93,74,44,.48);
}
.faq-item summary {
  padding: 1.1rem var(--space-md);
  position: relative;
  font-family: var(--font-text);
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
  transition: color .25s var(--ease-soft);
}
/* The chevron rotates open already; the row itself gave no sign it was
   clickable until then. Same gold the nav links and links elsewhere use
   on hover, so this reads as "interactive" rather than "static text". */
.faq-item summary:hover { color: var(--gold-text); }
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: '+';
  position: absolute;
  right: var(--space-md);
  top: 1.1rem;
  font-size: 1.3rem;
  color: var(--gold-text);
  transition: transform .25s var(--ease-out);
}
.faq-item[open] summary::after { transform: rotate(45deg); }
.faq-answer { padding: 0 var(--space-md) 1.3rem; font-size: var(--t-small); line-height: 1.65; color: var(--ink-body); }
.faq-answer.is-todo { font-style: italic; color: var(--stone-text); }

@media (prefers-reduced-motion: reduce) {
  .faq-item:hover, .faq-item[open] { transform: none; }
}

.local-contact {
  margin-top: clamp(2.4rem, 7vw, 3rem);
  padding: var(--space-md);
  border-radius: 1rem;
  text-align: center;
  background: linear-gradient(168deg, rgba(253,250,244,.92) 0%, rgba(251,247,240,.85) 60%, rgba(244,236,221,.85) 100%);
  border: 1px solid rgba(196,159,108,.22);
  box-shadow: 0 1px 2px rgba(93,74,44,.05), 0 18px 40px -28px rgba(93,74,44,.5);
  -webkit-backdrop-filter: blur(14px);
          backdrop-filter: blur(14px);
  transition:
    transform .45s var(--ease-soft),
    box-shadow .45s var(--ease-soft),
    border-color .45s var(--ease-soft);
}
.local-contact:hover {
  transform: translateY(-3px);
  border-color: rgba(196,159,108,.42);
  box-shadow: 0 1px 2px rgba(93,74,44,.06), 0 26px 48px -26px rgba(93,74,44,.58);
}
@media (prefers-reduced-motion: reduce) {
  .local-contact:hover { transform: none; }
}
.local-contact-mark { display: inline-block; width: 2.2rem; color: var(--gold-deep); opacity: .8; }
.local-contact-mark svg { width: 100%; height: auto; }
.local-contact-line { margin-top: .6rem; font-size: var(--t-body); line-height: 1.6; color: var(--ink-body); }
.local-contact-meta { margin-top: .7rem; font-size: var(--t-small); color: var(--ink-soft); }
.local-contact-meta .is-todo { font-style: italic; color: var(--stone-text); }
