/* tastemaker: macro=Long-Scroll-Narrative | mood=elegant | palette=photo-extracted(crimson 341-357deg + cobalt outlier)
   archetypes: nav-minimal-floating-rule / hero-fullbleed-film-masthead / feature-fullbleed-alternating-panel /
   proof-orientation-diagram / cta-inline-form-panel / footer-editorial-colophon
   contrast: 21 pairs checked, 11 text-safe, 2 UI-safe | anti-ref: tiffandko (cream/gold/Cormorant)
   Photography and film (c) the couple, with one exception below.

   Third-party asset, voluntary credit (CC0, no attribution required, and it
   must never appear as visible text on the page):
     design/assets/photos/inle-lake-*  --  "Fishing Inle lake", CC0 1.0,
     via Wikimedia Commons / Openverse. Chosen after the client's own
     reference turned out to be a watermarked Unsplash+ preview. */

/* ============================================================
   1. TOKENS
   ============================================================ */
:root {
  /* Colour. Every pairing used below appears in the style-lock contract */
  --ink: #14060B;
  --surface: #24090F;
  --paper: #F7EDEA;
  --scarlet: #E4405F;
  --amber: #E8B58A;
  --border: #4A1F2A;
  --on-scarlet: #1A0308;
  --cobalt: #26436F;
  --indigo: #1B2446;
  --oxblood: #520115;
  --crimson: #940D2A;
  --blush: #DA95AA;

  --paper-70: rgba(247, 237, 234, 0.70);
  --paper-46: rgba(247, 237, 234, 0.46);
  --paper-14: rgba(247, 237, 234, 0.14);
  --paper-08: rgba(247, 237, 234, 0.08);

  /* Spacing on an 8px base */
  --s1: 8px;   --s2: 16px;  --s3: 24px;  --s4: 32px;
  --s6: 48px;  --s8: 64px;  --s12: 96px; --s16: 128px; --s24: 192px;

  /* Section rhythm. Fluid rather than fixed so the page stays compact on a
     phone and breathes on a desktop, instead of doing one or the other badly.
     Ceilings are deliberately short of the old 164/208: at those values a
     desktop scrolled through more empty crimson than content between bands. */
  --section: clamp(68px, 7.5vw, 124px);
  --section-lg: clamp(84px, 9.5vw, 152px);

  /* Type */
  --display: "Instrument Serif", "Times New Roman", serif;
  --ui: "Archivo", "Helvetica Neue", system-ui, sans-serif;

  /* The tracked uppercase labels that carry the page's structure: eyebrows,
     units, table heads, tags. They were set to 9px in fourteen places, which
     is a considered detail at desk distance and a squint on a phone -- and
     the wide tracking that makes them elegant is the same thing that makes
     them hard to read small. One value, raised to where a phone can take it
     at a glance, since the page is read on one far more often than not. */
  --micro: 11px;

  --radius: 2px;

  /* Motion. Emil curves, no ease-in anywhere. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);

  --gutter: clamp(32px, 6.5vw, 72px);
  --measure: 62ch;
}

/* ============================================================
   2. RESET & BASE
   ============================================================ */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  background: var(--ink);
  color: var(--paper);
  font-family: var(--ui);
  font-size: 16px;
  line-height: 1.6;
  font-weight: 380;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Clip, not hidden: overflow-x:hidden forces overflow-y to compute to auto,
     which can make body a second scroll container and leave window.scrollTo
     looking like a no-op when the RSVP links try to move the page. */
  overflow-x: clip;
}

img, video, svg { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }

::selection { background: var(--scarlet); color: var(--on-scarlet); }

:focus-visible {
  outline: 2px solid var(--amber);
  outline-offset: 3px;
  border-radius: var(--radius);
}
:focus:not(:focus-visible) { outline: none; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ============================================================
   3. THE INVITATION
   A sealed envelope over the page, opened by hand. It is the only
   thing on the site that will not proceed on its own, which is the
   point of an invitation, and the pause pays for itself twice: the
   film loads through it, and the tap that breaks the seal is the user
   gesture iOS wants before it will start a video.
   ============================================================ */
.seal {
  position: fixed;
  inset: 0;
  z-index: 300;              /* over the nav, which sits at 100 */
  display: grid;
  place-items: center;
  /* Vertical inset clears the notch and home bar. Horizontal inset stays
     light so the card can centre in the viewport; the page gutter is for
     site chrome, not this overlay. */
  padding:
    calc(max(24px, var(--gutter) * 0.75) + env(safe-area-inset-top, 0px))
    max(16px, env(safe-area-inset-right, 0px))
    calc(max(24px, var(--gutter) * 0.75) + env(safe-area-inset-bottom, 0px))
    max(16px, env(safe-area-inset-left, 0px));
}
.seal[hidden] { display: none; }

/* Seal markup starts with [hidden] so a no-JS guest is never locked out.
   While is-loading is on (inline in <head>, before paint), show it anyway:
   otherwise the first frame is the naked site and the envelope arrives late. */
html.is-loading .seal[hidden] {
  display: grid !important;
}

/* The film plays underneath this, and this is what makes it read as a
   backdrop rather than as the page: a dark crimson wash with the light
   gathered behind the envelope. No photograph in the stack any more -- the
   veil is transparent where the gradients are, and what shows through is the
   hero itself, running. See `.is-loading .hero__media` further down for the
   soft focus, and `.is-loading .nav` for why nothing else shows with it. */
.seal__veil {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(120% 84% at 50% 44%, rgba(20, 6, 11, 0.42) 0%, rgba(20, 6, 11, 0.92) 100%),
    radial-gradient(104% 74% at 50% 58%, rgba(140, 12, 40, 0.28) 0%, rgba(120, 10, 34, 0.08) 76%),
    linear-gradient(rgba(20, 6, 11, 0.72), rgba(20, 6, 11, 0.88));
}

/* The film, while the invitation is over it. Scaled a touch because a blur
   samples past its own edges and would otherwise show the page behind as a
   soft rim. Both come off in one transition when the letter clears.

   The hero's own scrims come off with it. They are there to hold type over a
   moving image, and while the envelope is up there is no type over it -- left
   on, they stack under the veil and the film goes to black, which is the one
   thing this screen is not supposed to show. */
.is-veiled .hero__media {
  filter: blur(11px) saturate(1.06);
  transform: scale(1.06);
}
.is-veiled .hero__media::before { opacity: 0; }
.is-veiled .hero__media::after { opacity: 0.34; }
.hero__media::before,
.hero__media::after { transition: opacity 1100ms var(--ease-in-out); }
.hero__media {
  transition: filter 1100ms var(--ease-in-out), transform 1400ms var(--ease-in-out);
}

/* The brief was the film and nothing else behind the envelope. Hide every
   site surface (nav, copy, sections, footer) so a guest never reads the page
   through the veil or on the first paint before the seal script runs. The
   hero media stays: it is the soft backdrop, not the website.

   Opacity only (no visibility:hidden): visibility cannot interpolate and was
   the pop when Continue handed the page over. */
.is-veiled .nav,
.is-veiled .hero__body,
.is-veiled .hero__foot,
.is-veiled .approach,
.is-veiled .panel,
.is-veiled .section,
.is-veiled .footer {
  opacity: 0;
  pointer-events: none;
}
.hero__body,
.hero__foot {
  transition: opacity 1000ms var(--ease-out);
}
.approach,
.panel,
.section,
.footer {
  transition: opacity 900ms var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  .is-veiled .hero__media { filter: none; transform: none; }
}

/* The envelope and its prompt are one object, centred together, rather than
   an envelope in the middle and a caption stranded at the foot of the
   screen. The prompt reads as belonging to the thing it describes. */
.seal__stage {
  position: relative;
  display: grid;
  justify-items: center;
  justify-self: center;
  /* Explicit vw sizing (not % of an indefinite grid area) so the card cannot
     collapse to 0 or drift off-centre on phones. */
  width: min(92vw, 580px);
  max-width: calc(100vw - 32px);
  min-width: 0;
  gap: clamp(30px, 6.5vh, 58px);
}
/* Perspective only while the lid is turning. A live preserve-3d scene
   rasterises the type onto a texture and leaves the sealed card looking soft.
   Once the letter is resting, flatten again so the resting card stays sharp
   without a layout rebake (which is what used to snap). */
.seal__scene {
  width: 100%;
  perspective: none;
  perspective-origin: 50% 38%;
}
.seal[data-state="opening"] .seal__scene { perspective: 1600px; }
.seal[data-state="held"] .envelope,
.seal[data-state="open"] .envelope { transform-style: flat; }

/* --- The envelope ------------------------------------------------ */
/* Landscape, at 7:5, which is the proportion an A7 invitation envelope
   actually comes in and the shape everyone pictures when you say envelope.
   The letter inside is landscape with it: it has to be, or it would not fit
   through the opening, and a card that is visibly taller than the envelope it
   came out of is the sort of thing you cannot stop seeing once you have. */
.envelope {
  position: relative;
  width: 100%;
  aspect-ratio: 7 / 5;
  transform-style: flat;
  cursor: pointer;
}
.seal[data-state="opening"] .envelope { transform-style: preserve-3d; }

.lid,
.letter,
.envelope__body { position: absolute; inset: 0; }

/* --- The lid ------------------------------------------------------ */
/* Both faces are the same triangle hinged on the same edge, and they run the
   same rotation. Only their fill and their moment differ: the face is the
   outside of the paper and holds the seal, the liner is what is inside the
   fold. They trade places as the lid passes upright, where a triangle seen
   exactly edge-on has no width and neither can be seen. */
.lid {
  height: 52%;
  transform-origin: 50% 0;
}
.lid__shape {
  position: absolute;
  inset: 0;
  clip-path: polygon(0 0, 100% 0, 50% 100%);
}
.lid--face .lid__shape {
  background: linear-gradient(176deg, #FBF4F0 0%, #F0E2DC 62%, #E5D2CB 100%);
}
/* The liner is the one flourish inside the envelope, and it is the site's
   own oxblood rather than a decorative pattern nothing else on the page
   would explain. */
.lid--liner { opacity: 0; }
.lid--liner .lid__shape {
  background: linear-gradient(2deg, #63071A 0%, #46030F 58%, #300309 100%);
  box-shadow: inset 0 -1px 0 rgba(247, 237, 234, 0.16);
}

/* --- The letter --------------------------------------------------- */
.letter {
  left: 4.5%;
  width: 91%;
  top: 2.2%;
  height: 95.6%;
  display: grid;
  align-content: center;
  justify-items: center;
  /* Percentage padding resolves against width on all four sides, which is
     what a pressed border wants: equal in millimetres, not equal in percent
     of a side. Same reason the two frame rules below are inset in percent. */
  padding: 7% 8%;
  text-align: center;
  /* The card's own base size. Every measurement inside it is set in em off
     this, so the composition scales as one rather than the type shrinking
     while the space around it stays put.

     Proportional to the viewport, because the card is too, and flat above the
     width the card stops growing at. A landscape card is short, so height is
     what runs out: at 320px there is room for 24em of content and no more,
     and every multiple below was chosen against that. See .tooling/fit.mjs,
     which measures what is set against the paper it is set on. */
  font-size: min(2.05vw, 11px);
  color: #2A0710;
  /* Cream card: subpixel smoothing reads sharper than the page-wide
     antialiased setting, which softens dark type on light paper. */
  -webkit-font-smoothing: auto;
  background:
    linear-gradient(158deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0) 42%),
    linear-gradient(172deg, #FEFBF9 0%, #F6EDE7 54%, #EFE1D9 100%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.92),
              0 26px 52px -20px rgba(10, 2, 5, 0.66);
}
.letter.is-resting {
  /* Resting after the open timeline: still at its GSAP y/scale, but flat so
     type is not soft. No layout rebake. */
  transform-style: flat;
  -webkit-font-smoothing: auto;
}
/* The pressed border every letterpress invitation has. It is the detail that
   separates a card with words on it from an invitation. */
.letter::before {
  content: "";
  position: absolute;
  inset: 4.4%;
  border: 1px solid rgba(42, 7, 16, 0.20);
}
.letter::after {
  content: "";
  position: absolute;
  inset: calc(4.4% + 4px);
  border: 1px solid rgba(42, 7, 16, 0.09);
}

/* Sized off the viewport rather than the card, and that works because the
   card is a fixed fraction of it: min(84vw, 580px) for the envelope, 91% of
   that for the letter. Every clamp below is tuned so the longest line clears
   the inner width at 320px, where it is tightest. Container queries would say
   this more directly, but `container-type` brings containment with it and
   this card is inside a preserve-3d scene that must not be flattened. */
/* The signature where the ampersand was. It is taller than the glyph it
   replaces, so it borrows a little of the space below it and its tail runs
   down past the top of the eyebrow, as in the approved mockup. The card is
   short and height is what runs out first, so the box it takes in the flow is
   no taller than the old mark's: fit.mjs checks this at twelve widths. */
/* No position or z-index here: inside the preserve-3d envelope either one lifts
   the mark out of the card's stacking and it shows through the closed flap. */
.letter__mark {
  display: flex;
  justify-content: center;
  font-size: 2.6em;
  line-height: 0;
  /* Centred between the card's frame and "Together with their families", at
     the couple's request. The content block is centred as a whole, so a margin
     here moves the mark by half as much as it says; these two were solved from
     measured gaps rather than eyeballed, and the phone pair below differs
     because the frame is a share of the card's height while this is in em. */
  margin-top: -1em;
  margin-bottom: 0.8em;
}
/* Below 537px the card's type scales with the screen and height is what runs
   out (fit.mjs). The logo keeps its size; the room for its clearance comes out
   of the space around the hairline further down instead. */
@media (max-width: 536px) {
  /* Scoped under .letter so they outrank the base .letter__hair rule, which
     comes later in this file and would otherwise win on source order. */
  .letter .letter__mark { margin-top: -0.8em; margin-bottom: 0.37em; }
  .letter .letter__hair { margin-block: 1.2em 1.1em; }
}
.letter__mark .logo { height: 1.75em; }

.letter__eyebrow,
.letter__ask {
  font-family: var(--ui);
  font-variation-settings: "wdth" 108;
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: rgba(42, 7, 16, 0.52);
}

/* Both names on one line, which is what the width is for, and 3.4em is the
   largest that holds on one line at 320px. Stacked they cost a second line
   of a card that only has room for seven, so if it ever does stack -- a font
   that fails to load and falls back wider, say -- each name is kept whole:
   the only two places this may break are either side of the "and", never
   "Steph Koko and Saw / Min Maw". */
.letter__names {
  margin-block: 0.7em 0.62em;
  font-family: var(--display);
  font-size: 3.15em;
  line-height: 1.08;
  letter-spacing: -0.008em;
  text-wrap: balance;
}
.letter__who { white-space: nowrap; }
/* The same italic ampersand as the envelope's cover. It was spelled out while
   the wax carried an ampersand too; the wax now carries the signature. */
.letter__and {
  font-style: italic;
  color: #A8603F;
  padding-inline: 0.08em;
}

.letter__hair {
  width: 3.4em;
  height: 1px;
  background: rgba(42, 7, 16, 0.26);
  margin-block: 1.9em 1.75em;
}

.letter__when,
.letter__where {
  font-family: var(--ui);
  font-variation-settings: "wdth" 106;
  font-weight: 500;
  letter-spacing: 0.11em;
  line-height: 1.7;
  text-transform: uppercase;
  color: rgba(42, 7, 16, 0.74);
}
.letter__amp2 { font-family: var(--display); font-style: italic; color: #A8603F; }
.letter__where { margin-top: 0.55em; color: rgba(42, 7, 16, 0.5); letter-spacing: 0.15em; }

/* --- The body of the envelope ------------------------------------- */
.envelope__body {
  display: grid;
  align-content: end;
  justify-items: center;
  gap: 6px;
  padding: 0 var(--s3) clamp(18px, 5vw, 34px);
  background: linear-gradient(168deg, #F8EFEB 0%, #EBDCD6 72%, #E3D0C9 100%);
  box-shadow: 0 34px 62px -20px rgba(10, 2, 5, 0.72);
  -webkit-font-smoothing: auto;
}
/* Where the lid's point lands, and the two folded side panels behind it.
   Without them the body reads as a plain card rather than the back of a
   folded envelope. */
.envelope__body::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to bottom right, transparent calc(50% - 0.5px), rgba(42, 7, 16, 0.11) 50%, transparent calc(50% + 0.5px)) left top / 50% 52% no-repeat,
    linear-gradient(to bottom left, transparent calc(50% - 0.5px), rgba(42, 7, 16, 0.11) 50%, transparent calc(50% + 0.5px)) right top / 50% 52% no-repeat,
    linear-gradient(rgba(42, 7, 16, 0.07), rgba(42, 7, 16, 0)) top / 100% 8% no-repeat;
}

.envelope__eyebrow {
  font-family: var(--ui);
  font-variation-settings: "wdth" 112;
  font-size: clamp(10px, 2.3vw, 11px);
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: rgba(42, 7, 16, 0.5);
}
.envelope__names {
  font-family: var(--display);
  /* Both full names, not the short wordmark this was first set for: roughly
     twice the characters across the same 7:5 envelope, so the type comes down
     and the tracking closes up to keep it on one line at 320px. */
  font-size: clamp(0.95rem, 3.5vw, 1.55rem);
  line-height: 1.08;
  letter-spacing: -0.005em;
  text-align: center;
  text-wrap: balance;
  color: #2A0710;
}
.envelope__names .amp { color: #A8603F; font-style: italic; }
.envelope__names img { height: clamp(20px, 4.6vw, 32px); width: auto; object-fit: contain; }

/* --- The wax ------------------------------------------------------ */
/* Sits on the point of the lid and rides it, so opening the envelope lifts
   the seal away whole rather than breaking it. Outside .lid__shape, because
   the shape is clipped to the triangle and would cut the wax in half.

   18% of the envelope's width, which on a 7:5 landscape works out at just
   under half the lid's depth. Percentages here resolve against width whatever
   they are written against, so this is the number that has to change if the
   envelope's proportions ever do. */
.wax {
  position: absolute;
  top: 100%;
  left: 50%;
  width: 18%;
  aspect-ratio: 1;
  margin: -9% 0 0 -9%;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  /* No box-shadow here: it follows the button's round box, not the poured
     edge, and showed as a disc behind the seal. The shadow is drawn inside
     the SVG instead (.wax__shadow). */
}
/* No scale pulse on the SVG: fractional scale left the seal soft for the
   whole sealed beat, which is when guests look at it hardest. */
.wax__svg {
  width: 100%; height: 100%;
  overflow: visible;
}
.wax__rim { fill: #4C0512; transform: translate(2.5px, 4px); }
.wax__face { fill: url(#waxFill); }
/* The signature, pressed in: a lit copy below, a shadow copy above, gilt on top. */
.wax__mark--lit { fill: #D9667E; fill-opacity: 0.42; }
.wax__mark--shade { fill: #3A0010; fill-opacity: 0.55; }
/* The blur needs room outside the 220 box, so the SVG is not clipped. */
.wax__svg { overflow: visible; }
.wax__shadow { fill: #1E0006; fill-opacity: 0.5; transform: translate(1.5px, 7px); }

/* The round outline every other control gets would follow the button's
   circle, so the wax draws its own ring around the poured edge instead. */
.wax:focus,
.wax:focus-visible { outline: none; }
.wax__focus {
  fill: none;
  stroke: var(--amber);
  stroke-width: 4;
  transform-box: view-box;
  transform-origin: 50% 50%;
  transform: scale(1.12);
  opacity: 0;
  transition: opacity 160ms var(--ease-out);
}
.wax:focus-visible:not([data-quiet]) .wax__focus { opacity: 1; }
/* Once it is opening, it is no longer a control, so no ring rides the lid. */
.seal:not([data-state="in"]) .wax__focus { opacity: 0; }

/* Focus the seal is holding only because the script put it there. */
.wax[data-quiet]:focus, .wax[data-quiet]:focus-visible,
.seal__continue[data-quiet]:focus, .seal__continue[data-quiet]:focus-visible { outline: none; }

/* Wax has weight. It should give a little before it goes. */
.wax { transition: transform 170ms var(--ease-out); }
.wax__face { transition: fill 240ms var(--ease-out); }
.wax:hover .wax__face { fill: #A6152F; }
.wax:active { transform: scale(0.93); }

/* --- The prompt --------------------------------------------------- */
/* Set brighter than the site's other small labels, and carrying its own
   shadow. It is the only piece of type on the page sitting over a moving
   image with nothing between them: a frame of the film goes near-white every
   few seconds, and the usual muted grey vanishes into it. */
.seal__prompt {
  font-family: var(--ui);
  font-variation-settings: "wdth" 116;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--paper-70);
  text-shadow: 0 1px 12px rgba(16, 2, 7, 0.9), 0 1px 3px rgba(16, 2, 7, 0.7);
  position: relative;
}

/* Everything arrives together once the fonts have swapped, so no part of
   the envelope is seen setting itself. Until then the veil alone holds the
   screen: dark, sealed, no website. */
.seal:not([data-state]) .seal__stage,
.seal:not([data-state]) .seal__prompt { opacity: 0; }

.seal[data-state="in"] .seal__stage,
.seal[data-state="in"] .seal__prompt { animation: sealArrive 900ms var(--ease-out) both; }
.seal[data-state="in"] .seal__prompt { animation-delay: 260ms; }

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

/* --- The second press -------------------------------------------- */
/* Anchored to the viewport, not to the stage: the stage travels downwards as
   the envelope is drawn off the letter, and a button riding along with it
   would leave the frame. The slot does the centring so the button's own
   transform is left free for GSAP to animate. */
.seal__gate {
  position: absolute;
  inset-inline: 0;
  bottom: calc(var(--gutter) + env(safe-area-inset-bottom));
  display: flex;
  justify-content: center;
  pointer-events: none;
}
.seal__continue {
  pointer-events: auto;
  opacity: 0;
  /* Carries its own shadow for the same reason the prompt does: it sits over
     a moving image with nothing between them. */
  box-shadow: 0 10px 34px -12px rgba(16, 2, 7, 0.9);
}
.seal__continue[hidden] { display: none; }

@media (max-width: 520px) {
  .seal__gate { bottom: calc(var(--s3) + env(safe-area-inset-bottom)); }
  .seal__continue { width: min(100%, 320px); }
}

/* Once it is opening, the envelope is no longer a control. */
.seal[data-state="opening"],
.seal[data-state="held"],
.seal[data-state="open"] { cursor: default; }
.seal[data-state="opening"] .envelope,
.seal[data-state="held"] .envelope,
.seal[data-state="open"] .envelope,
.seal[data-state="opening"] .wax,
.seal[data-state="held"] .wax,
.seal[data-state="open"] .wax { pointer-events: none; cursor: default; }

/* The no-JavaScript-motion path: same beats, no movement. GSAP failing to
   load lands here too, which is why it is CSS rather than a fallback branch
   of the timeline. The letter is what is left standing, so everything that
   was covering it goes and it alone remains. */
.seal[data-state="open"] .wax,
.seal[data-state="open"] .envelope__body,
.seal[data-state="open"] .lid,
.seal[data-state="open"] .seal__prompt {
  opacity: 0;
  transition: opacity 420ms var(--ease-out);
}
.seal[data-state="open"] .seal__veil {
  opacity: 0.5;
  transition: opacity 700ms var(--ease-out) 260ms;
}
.seal[data-state="out"] {
  opacity: 0;
  pointer-events: none;
  transition: opacity 620ms var(--ease-out);
}

/* Scroll stays locked underneath so the page cannot be read past the edges. */
.is-loading, .is-loading body { overflow: hidden; }

/* Without JavaScript there is no way to open it, so it is never shown. */
.no-js .seal { display: none; }

@media (prefers-reduced-motion: reduce) {
  .seal[data-state="in"] .seal__stage,
  .seal[data-state="in"] .seal__prompt {
    animation: sealFadeIn 300ms linear both;
  }
  @keyframes sealFadeIn { from { opacity: 0; } to { opacity: 1; } }
}

/* ============================================================
   4. TYPE PRIMITIVES
   ============================================================ */

/* The masthead rule: Archivo at full width axis, heavily tracked. Every
   section label is a fact about the section (a date, a place, a deadline)
   rather than an index number, so the device carries information. */
.rule {
  font-family: var(--ui);
  font-variation-settings: "wdth" 118;
  font-weight: 500;
  font-size: var(--micro);
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--paper-46);
  display: flex;
  align-items: center;
  gap: var(--s2);
}
.rule::after {
  content: "";
  flex: 1;
  height: 1px;
  background: currentColor;
  opacity: 0.42;
}
.rule--bare::after, .rule--bare::before { display: none; }
/* A rule made of several parts (text, a dot, text) cannot wrap as a sentence:
   each run is its own flex item, so on a phone the row either crushes the
   words into stacks or runs off the side. The Venue's eyebrow did the second,
   25px past a 320px screen, once the resort name was added in front of it.
   Below 620px the parts stack one per line and the dots go, the same thing
   the hero's date line already does. */
@media (max-width: 620px) {
  .rule--parts { flex-direction: column; align-items: flex-start; gap: 2px; line-height: 1.7; }
  .rule--parts .sep,
  .rule--parts::after { display: none; }
}
.rule--amber { color: var(--amber); }

.display {
  font-family: var(--display);
  font-weight: 400;
  line-height: 0.92;
  letter-spacing: -0.015em;
  text-wrap: balance;
}

/* ------------------------------------------------------------
   The signature
   One alpha mask, coloured here, so it can be gilt on the dark page and on the
   seal and copper on the paper without a second file. Aspect is the mask's own
   637 x 720.
   ------------------------------------------------------------ */
.logo {
  display: inline-block;
  height: 40px;
  aspect-ratio: 637 / 720;
  flex: none;
  background-color: currentColor;
  -webkit-mask: url("../design/assets/brand/logo-mask.png?v=1") center / contain no-repeat;
          mask: url("../design/assets/brand/logo-mask.png?v=1") center / contain no-repeat;
}
/* Gold leaf. Wider than the mark so hovering can slide a highlight across it. */
.logo--gilt {
  background-image: linear-gradient(118deg,
    #B9854A 0%, #E9C58C 22%, #FBE6BC 34%, #D9A764 48%, #F1CF95 64%, #A9743A 100%);
  background-size: 220% 100%;
  background-position: 0% 0;
  transition: background-position 1100ms var(--ease-out);
}
.logo--copper { background-color: #A8603F; }
@media (hover: hover) and (pointer: fine) {
  .nav__mark--logo:hover .logo--gilt { background-position: 100% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .logo--gilt { transition: none; }
}

.amp {
  font-family: var(--display);
  font-style: italic;
  color: var(--amber);
}

.prose {
  max-width: var(--measure);
  color: var(--paper-70);
  font-size: clamp(15px, 1.05vw, 17px);
  line-height: 1.72;
  text-wrap: pretty;
}
.prose + .prose { margin-top: var(--s3); }
.prose strong { color: var(--paper); font-weight: 500; }

.num { font-variant-numeric: tabular-nums; }

/* The house separator, set as a mark rather than a character so it keeps its
   colour and optical size wherever it lands. Named .sep, not .dot: the
   dashboard imports this file and already owns .dot for its table markers. */
.sep {
  display: inline-block;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--scarlet);
  vertical-align: middle;
  margin-inline: 8px;
  flex: none;
}

/* ============================================================
   5. LAYOUT
   ============================================================ */
.shell {
  width: 100%;
  max-width: 1440px;
  margin-inline: auto;
  padding-inline: max(var(--gutter), env(safe-area-inset-left, 0px))
                  max(var(--gutter), env(safe-area-inset-right, 0px));
}

/* The nav is fixed, so anchor jumps must stop clear of it or the section
   heading lands underneath the header. */
:target,
section[id],
main[id] { scroll-margin-top: clamp(64px, 8vh, 88px); }

/* Section padding is weighted by role, never uniform. */
.section { position: relative; padding-block: var(--section); }
.section--pivotal { padding-block: var(--section-lg); }

.section-head { margin-bottom: clamp(28px, 3.6vw, 60px); }
/* The Venue's heading introduces the copy directly beneath it rather than a
   new band of content, so it sits closer to it than the other section heads. */
.section--venue .section-head { margin-bottom: clamp(20px, 2.4vw, 36px); }
.section-head__title {
  font-family: var(--display);
  font-size: clamp(2.4rem, 5.6vw, 4.6rem);
  line-height: 0.98;
  margin-top: var(--s3);
  letter-spacing: -0.02em;
}

/* ============================================================
   6. THE CHROMATIC FIELD
   The signature move: the crimson of the photographs continues past the
   image edges into the page. Three fixed gradient layers sit behind
   everything and are re-tinted per section as the reader scrolls.
   ============================================================ */
.chroma {
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background: var(--ink);
  /* The layers inside run to -20% so the gradients fall off past the edges.
     Unclipped, that bleed was being counted as page width and gave a 320px
     phone 25px of sideways scroll. */
  overflow: hidden;
}
.chroma__layer {
  position: absolute;
  inset: -20%;
  opacity: 0;
  transition: opacity 900ms var(--ease-in-out);
  will-change: opacity;
}
.chroma__layer[data-active="true"] { opacity: 1; }

.chroma__layer--crimson {
  background:
    radial-gradient(58% 44% at 78% 12%, rgba(228, 64, 95, 0.30), transparent 68%),
    radial-gradient(70% 56% at 12% 88%, rgba(148, 13, 42, 0.36), transparent 72%),
    radial-gradient(46% 40% at 46% 42%, rgba(82, 1, 21, 0.55), transparent 76%);
}
.chroma__layer--cobalt {
  background:
    radial-gradient(62% 48% at 20% 14%, rgba(38, 67, 111, 0.44), transparent 70%),
    radial-gradient(54% 44% at 84% 76%, rgba(232, 181, 138, 0.16), transparent 70%),
    radial-gradient(50% 46% at 50% 50%, rgba(27, 36, 70, 0.52), transparent 74%);
}
.chroma__layer--blush {
  background:
    radial-gradient(64% 50% at 70% 22%, rgba(218, 149, 170, 0.24), transparent 70%),
    radial-gradient(58% 48% at 18% 82%, rgba(148, 13, 42, 0.30), transparent 72%);
}

/* Fine film grain, so the large flat gradients never band on wide screens. */
.chroma__grain {
  position: absolute; inset: 0;
  opacity: 0.34;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}

/* ============================================================
   7. NAV
   ============================================================ */
.nav {
  position: fixed;
  inset-block-start: 0; inset-inline: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  /* Overlay only: the film keeps the first screen. Safe-area clears the notch. */
  padding:
    calc(10px + env(safe-area-inset-top, 0px))
    max(var(--gutter), env(safe-area-inset-right, 0px))
    10px
    max(var(--gutter), env(safe-area-inset-left, 0px));
  transition: background-color 280ms var(--ease-out), border-color 280ms var(--ease-out),
              padding 280ms var(--ease-out), opacity 1000ms var(--ease-out);
  border-bottom: 1px solid transparent;
}
.nav[data-stuck="true"] {
  background: rgba(20, 6, 11, 0.82);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  border-bottom-color: var(--border);
  padding-block: 8px;
  padding-block-start: calc(8px + env(safe-area-inset-top, 0px));
}

/* Reading progress, sitting on the nav's own bottom hairline. */
.nav__progress {
  position: absolute;
  inset-inline: 0; inset-block-end: -1px;
  height: 1px;
  pointer-events: none;
}
.nav__progress span {
  display: block;
  height: 100%; width: 100%;
  background: var(--scarlet);
  transform: scaleX(0);
  transform-origin: left;
}

.nav__mark {
  font-family: var(--ui);
  font-variation-settings: "wdth" 118;
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 0.36em;
  text-transform: uppercase;
  color: var(--paper);
  text-decoration: none;
  flex: none;
  /* Negative margin keeps the extra tap area off the layout. */
  padding: 8px;
  margin: -8px;
}
.nav__mark .amp { font-size: 13px; letter-spacing: 0; }
/* Client logo drops in here without touching the layout. */
.nav__mark--logo {
  display: inline-flex;
  align-items: center;
  letter-spacing: 0;
  transition: transform 160ms var(--ease-out);
}
/* Signature needs height to read; floor is set for phones, ceiling for desk. */
.nav__mark--logo .logo { height: clamp(38px, 3.2vw, 46px); }
.nav__mark--logo:active { transform: scale(0.96); }

.nav__links {
  display: flex;
  align-items: center;
  gap: clamp(12px, 1.8vw, 28px);
  list-style: none;
  padding: 0;
}
@media (max-width: 860px) { .nav__links { display: none; } }

.nav__link {
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--paper-46);
  text-decoration: none;
  padding-block: 6px;
  position: relative;
  transition: color 200ms var(--ease-out);
}
.nav__link::after {
  content: "";
  position: absolute;
  inset-block-end: 0; inset-inline-start: 0;
  width: 100%; height: 1px;
  background: var(--scarlet);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 240ms var(--ease-out);
}
.nav__link:hover, .nav__link[aria-current="true"] { color: var(--paper); }
.nav__link[aria-current="true"]::after { transform: scaleX(1); }
@media (hover: hover) and (pointer: fine) {
  .nav__link:hover::after { transform: scaleX(1); }
}

/* Nav RSVP stays shorter than the page CTAs so the bar stays slim. */
.nav .btn--sm {
  padding: 8px 16px;
  font-size: 10px;
  letter-spacing: 0.16em;
}

/* Phones: more air from the edges, a larger mark, a thumb-friendly RSVP. */
@media (max-width: 860px) {
  .nav {
    gap: var(--s3);
    padding:
      calc(14px + env(safe-area-inset-top, 0px))
      max(var(--gutter), calc(16px + env(safe-area-inset-right, 0px)))
      14px
      max(var(--gutter), calc(16px + env(safe-area-inset-left, 0px)));
  }
  .nav[data-stuck="true"] {
    padding-block: 12px;
    padding-block-start: calc(12px + env(safe-area-inset-top, 0px));
  }
  .nav__mark--logo .logo { height: clamp(44px, 12vw, 52px); }
  .nav .btn--sm {
    /* 12px block keeps the thumb target at 44px without growing the bar. */
    padding: 12px 18px;
    font-size: 11px;
    letter-spacing: 0.18em;
  }
}
@media (max-width: 380px) {
  .nav { gap: var(--s2); }
  .nav .btn--sm { padding-inline: 14px; letter-spacing: 0.14em; }
}

/* ============================================================
   8. BUTTONS
   ============================================================ */
.btn {
  --btn-bg: var(--scarlet);
  --btn-fg: var(--on-scarlet);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s1);
  padding: 15px var(--s4);
  background: var(--btn-bg);
  color: var(--btn-fg);
  border: 1px solid var(--btn-bg);
  border-radius: var(--radius);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: transform 160ms var(--ease-out),
              background-color 200ms var(--ease-out),
              border-color 200ms var(--ease-out),
              color 200ms var(--ease-out);
}
.btn:active { transform: scale(0.97); }
@media (hover: hover) and (pointer: fine) {
  .btn:hover { background: var(--paper); border-color: var(--paper); color: var(--on-scarlet); }
}

.btn--ghost {
  --btn-bg: transparent;
  --btn-fg: var(--paper);
  border-color: var(--paper-14);
}
@media (hover: hover) and (pointer: fine) {
  .btn--ghost:hover { background: transparent; border-color: var(--paper); color: var(--paper); }
}

.btn--sm { padding: 13px var(--s3); font-size: var(--micro); letter-spacing: 0.2em; flex: none; }
.btn--block { width: 100%; }
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }
.btn[disabled]:active { transform: none; }

/* ============================================================
   9. HERO
   The film is the first screen. It is locked to one viewport tall on every
   device so the full frame is in view without scrolling; type sits inside
   that frame rather than stretching the section past it.
   ============================================================ */
.hero {
  position: relative;
  /* Exact viewport, not min-height: masthead type used to push this past
     100svh on wide monitors, so the lower half of the film sat below the fold. */
  height: 100svh;
  height: 100dvh;
  max-height: 100svh;
  max-height: 100dvh;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  isolation: isolate;
  overflow: hidden;
}

.hero__media {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  isolation: isolate;
  background: var(--ink);
}
.hero__media img,
.hero__media video {
  width: 100%; height: 100%;
  /* Cover on typical screens. Prefer the couple over empty sky. */
  object-fit: cover;
  object-position: 50% 42%;
  will-change: transform;
  /* The opening clip is a misty hillside, and stacking scrims on low-contrast
     footage turns it to haze. A small lift before the grade holds it open. */
  filter: contrast(1.1) saturate(1.08);
}
/* Viewports wider than the film (ultrawide, many desktops): cover would crop
   the top and bottom hard. Contain keeps the full vertical frame in view and
   letters the sides with ink. */
@media (min-aspect-ratio: 18/10) {
  .hero__media img,
  .hero__media video {
    object-fit: contain;
    object-position: center center;
  }
}
/* Tall phones already get the upright cut; keep cover so the frame fills. */
@media (max-aspect-ratio: 86/100) {
  .hero__media img,
  .hero__media video {
    object-fit: cover;
    object-position: 50% 40%;
  }
}

/* The film was shot in cool daylight and the rest of the site is oxblood.
   A vignetting multiply pass pulls the footage into the page's colour without
   flattening the middle of frame, where the couple always are. */
.hero__media::before {
  content: "";
  position: absolute; inset: 0;
  mix-blend-mode: multiply;
  background: radial-gradient(84% 74% at 50% 42%,
              rgba(82, 1, 21, 0.04), rgba(82, 1, 21, 0.52) 100%);
  pointer-events: none;
}
/* Legibility scrim. A short band under the nav and a stronger band under the
   masthead; the middle of the frame stays open. Kept shallow at the top so
   the bar does not read as a second header. */
.hero__media::after {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom,
      rgba(20, 6, 11, 0.55) 0%,
      rgba(20, 6, 11, 0.18) 8%,
      rgba(20, 6, 11, 0.04) 28%,
      rgba(20, 6, 11, 0.42) 62%,
      rgba(20, 6, 11, 0.88) 86%,
      var(--ink) 100%),
    linear-gradient(to right, rgba(20, 6, 11, 0.52), transparent 58%);
}

.hero__body {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: flex-start;
  min-height: 0;
  /* Clear the fixed nav, then sit the type in the lower third. Side inset
     comes from .shell (do not zero padding-inline here). */
  padding-block:
    calc(64px + env(safe-area-inset-top, 0px))
    var(--s3);
}

.hero__names {
  font-size: clamp(2rem, 5vw, 4.4rem);
  margin-block: var(--s2) var(--s3);
  max-width: 17ch;
}
.hero__names .amp {
  display: inline-block;
  padding-inline: 0.1em;
  font-size: 0.84em;
}

.hero__line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  font-size: clamp(11px, 0.85vw, 12px);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--paper-70);
  line-height: 1.85;
}
/* Three facts on one line wrap on a phone, and what wraps first is the dot
   between two of them, leaving a bullet marooned at the end of a line. Given
   a column they stop being a sentence and become an address block, which is
   what they always were, and the dots are not needed to say so. */
@media (max-width: 620px) {
  .hero__line { flex-direction: column; align-items: flex-start; gap: 0.35em; line-height: 1.55; }
  .hero__line .sep { display: none; }
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-top: var(--s4);
}

.hero__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding-block: 14px;
  padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--paper-08);
}

.scroll-cue {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-size: var(--micro);
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--paper-46);
}
.scroll-cue__line {
  display: block;
  width: 1px; height: 28px;
  background: linear-gradient(to bottom, var(--paper-46), transparent);
  position: relative;
  overflow: hidden;
}
.scroll-cue__line::after {
  content: "";
  position: absolute; inset-inline: 0; top: 0;
  height: 40%;
  background: var(--scarlet);
  animation: cue 2.4s var(--ease-in-out) infinite;
}
@keyframes cue {
  0%   { transform: translateY(-100%); }
  60%  { transform: translateY(240%); }
  100% { transform: translateY(240%); }
}

@media (max-width: 620px) {
  .scroll-cue { display: none; }
  .hero__foot { justify-content: flex-end; }
}

/* Phones and narrow tablets: give the masthead room from the edges and lift
   the type so it is not crushed into the foot. */
@media (max-width: 860px) {
  .hero__body {
    padding-block:
      calc(96px + env(safe-area-inset-top, 0px))
      var(--s4);
  }
  .hero__names {
    font-size: clamp(2.25rem, 9.2vw, 3.1rem);
    margin-block: var(--s3) var(--s3);
    max-width: 12ch;
  }
  .hero__line {
    font-size: 11px;
    letter-spacing: 0.14em;
  }
  .hero__actions {
    margin-top: 40px;
    gap: 12px;
    width: 100%;
  }
  .hero__actions .btn {
    padding: 14px 20px;
  }
  .hero__foot {
    padding-block: 12px;
    padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  }
}

/* Short viewports (laptops with big UI chrome, landscape phones): keep the
   masthead inside one screen rather than competing with the film. */
@media (max-height: 720px) {
  .hero__names { font-size: clamp(1.7rem, 6.2vh, 3.2rem); margin-block: var(--s1) var(--s2); }
  .hero__actions { margin-top: var(--s3); }
  .hero__body { padding-block-start: calc(56px + env(safe-area-inset-top, 0px)); }
  .hero .btn { padding-block: 12px; }
}
@media (max-width: 860px) and (max-height: 720px) {
  .hero__body { padding-block-start: calc(72px + env(safe-area-inset-top, 0px)); }
  .nav__mark--logo .logo { height: 38px; }
}

/* Sound. Film starts with audio on after Continue; this control lets a guest
   mute without leaving the page or hunting for device volume.

   The meter is the whole control. A labelled pill was the largest object in
   the hero foot and read as a call to action, which it is not: it is a state
   anyone can see at a glance and only some people will ever touch. */
.sound {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Tap target is the box; the meter inside stays small. */
  width: 36px;
  height: 36px;
  margin-inline-end: -8px;   /* optical: the meter, not the box, aligns right */
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 50%;
  color: var(--paper-46);
  cursor: pointer;
  transition: background-color 200ms var(--ease-out), transform 160ms var(--ease-out);
}
.sound:active { transform: scale(0.94); }
@media (hover: hover) and (pointer: fine) {
  .sound:hover { background: var(--paper-08); }
}
@media (pointer: coarse) {
  .sound { width: 44px; height: 44px; margin-inline-end: -10px; }
}

.sound__bars {
  display: flex;
  align-items: center;
  gap: 2px;
  height: 12px;
  flex: none;
}
.sound__bars i {
  display: block;
  width: 2px;
  height: 13px;
  /* Muted reads as inert paper; playing lifts to scarlet, which is the only
     signal the control needs. Resting height stays tall enough to read as a
     meter rather than a row of dots. */
  background: var(--paper-70);
  transform: scaleY(0.45);
  transform-origin: 50% 50%;
  transition: transform 200ms var(--ease-out), background-color 200ms var(--ease-out);
}
.sound__bars i:nth-child(2) { transform: scaleY(0.75); }
.sound__bars i:nth-child(3) { transform: scaleY(0.6); }
.sound[aria-pressed="true"] .sound__bars i { background: var(--scarlet); }
.sound[aria-pressed="true"] .sound__bars i { animation: eq 900ms var(--ease-in-out) infinite; }
.sound[aria-pressed="true"] .sound__bars i:nth-child(2) { animation-delay: 150ms; }
.sound[aria-pressed="true"] .sound__bars i:nth-child(3) { animation-delay: 300ms; }
.sound[aria-pressed="true"] .sound__bars i:nth-child(4) { animation-delay: 90ms; }
/* Scaled rather than resized, so the meter never triggers layout. */
@keyframes eq {
  0%, 100% { transform: scaleY(0.25); }
  50%      { transform: scaleY(1); }
}

/* ============================================================
   10. THE APPROACH
   The held breath between the film and the plan, and the one piece
   of live data on the page.
   ============================================================ */
.approach {
  position: relative;
  padding-block: clamp(44px, 5.5vw, 84px);
  border-bottom: 1px solid var(--border);
}
.approach__inner {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s4) var(--s8);
  flex-wrap: wrap;
}

.countdown { display: flex; gap: clamp(18px, 3.2vw, 46px); }
.countdown__cell { display: flex; flex-direction: column; gap: 8px; }
.countdown__value {
  font-family: var(--display);
  font-size: clamp(2.1rem, 5vw, 3.9rem);
  line-height: 0.9;
  font-variant-numeric: tabular-nums;
}
.countdown__label {
  font-size: var(--micro);
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--paper-46);
}

@media (max-width: 720px) {
  .approach__inner { align-items: flex-start; flex-direction: column; gap: var(--s4); }
  .countdown { gap: 0; justify-content: space-between; width: 100%; }
}

/* ============================================================
   11. FULL-BLEED PHOTO PANELS
   ============================================================ */
.panel {
  position: relative;
  display: grid;
  align-items: center;
  min-height: 92svh;
  overflow: hidden;
  isolation: isolate;
}
.panel__media { position: absolute; inset: 0; z-index: -1; overflow: hidden; }
.panel__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  will-change: transform;
}

/* Photos held back so the hero film gets the bandwidth first. A 4KB blur
   stands in until site.js swaps the real file in. Only `filter` is animated:
   these carry data-parallax, which owns `transform`.
   Without JS the swap never happens, so the <noscript> copy is shown instead. */
.media-hold { filter: blur(16px); transition: filter 600ms cubic-bezier(0.23, 1, 0.32, 1); }
.media-hold.is-loaded { filter: none; }
.no-js .media-hold { display: none; }
/* Top and bottom edges dissolve into the page. Without this a panel meets the
   section above it on a hard horizontal line and the scroll reads as a stack
   of separate pages rather than one continuous one. */
.panel__media::before {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(to bottom,
    var(--ink) 0%, transparent 13%, transparent 87%, var(--ink) 100%);
}
.panel__media::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(to right, rgba(20, 6, 11, 0.90) 0%, rgba(20, 6, 11, 0.62) 44%, rgba(20, 6, 11, 0.20) 100%);
}
.panel--right .panel__media::after {
  background: linear-gradient(to left, rgba(20, 6, 11, 0.90) 0%, rgba(20, 6, 11, 0.62) 44%, rgba(20, 6, 11, 0.20) 100%);
}
.panel__content { max-width: 52ch; padding-block: var(--section); }
.panel__content--wide { max-width: 68ch; }
.panel--right .panel__content { margin-inline-start: auto; }

/* The veil frame is an abstraction rather than a portrait, so it can be let
   through at close to full strength once the copy column is clear of it.
   Scrimmed evenly it just goes to haze. */
.panel--weekend .panel__media::after {
  background: linear-gradient(to right,
    rgba(20, 6, 11, 0.94) 0%, rgba(20, 6, 11, 0.82) 38%,
    rgba(20, 6, 11, 0.34) 72%, rgba(20, 6, 11, 0.14) 100%);
}

/* Below 900px the copy stops sitting on one side of the frame, so a
   directional scrim no longer protects it, so go flat and darker instead. */
@media (max-width: 900px) {
  .panel__media::after,
  .panel--right .panel__media::after {
    background: linear-gradient(to bottom,
      rgba(20, 6, 11, 0.90) 0%,
      rgba(20, 6, 11, 0.80) 40%,
      rgba(20, 6, 11, 0.88) 100%);
  }
}

/* ============================================================
   12. GLASS RAIL / content sitting over photography
   ============================================================ */
.rail {
  background: rgba(36, 9, 15, 0.62);
  backdrop-filter: blur(18px) saturate(130%);
  -webkit-backdrop-filter: blur(18px) saturate(130%);
  border: 1px solid var(--paper-08);
  border-radius: var(--radius);
  padding: clamp(24px, 3.4vw, 44px);
}

/* ============================================================
   14. VENUE / orientation diagram
   ============================================================ */
/* --- The Venue backdrop -------------------------------------------
   The lake, behind the section about the lake. Scrimmed hard, because this
   section carries two columns of copy, an SVG map and the contact block: the
   picture is there to place you, not to be looked at. Measured at this
   strength the worst region still clears 11:1 for paper text. */
.section--venue { position: relative; isolation: isolate; }
.venue__media {
  position: absolute; inset: 0; z-index: -1; overflow: hidden;
}
.venue__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% 62%;
}
/* Edges dissolve into the page, the same way the photographic panels do, so
   the section does not meet its neighbours on a hard line. */
.venue__media::before {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(to bottom,
    var(--ink) 0%, transparent 9%, transparent 91%, var(--ink) 100%);
}
.venue__media::after {
  content: "";
  position: absolute; inset: 0;
  /* Held at roughly seven tenths rather than nine: at nine the lake was
     technically behind the section and visible in none of it. Measured on the
     rendered composite, the copy still clears 8:1 here. */
  background:
    linear-gradient(to bottom, rgba(20, 6, 11, 0.70), rgba(20, 6, 11, 0.78)),
    linear-gradient(to right, rgba(27, 36, 70, 0.22), transparent 76%);
}

.place__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.92fr);
  gap: clamp(32px, 5vw, 88px);
  /* The copy starts at the top of the row so it reads as belonging to the
     heading above it. Centred against the much taller map, as it was, it
     floated down the page and left a gap under "The Venue" that no amount of
     trimming the heading's own margin could close. Client's note. */
  align-items: start;
}
/* The diagram keeps its centring, so it still sits opposite the middle of the
   column rather than hanging from the top of it. */
.map__figure { align-self: center; }
@media (max-width: 900px) {
  .place__grid { grid-template-columns: 1fr; }
  .map__figure { align-self: start; }
}

/* The facts band runs the full width underneath. Left inside the text
   column, it sat in a pool of dead space beside the diagram. */
.place__copy > .prose + .prose { margin-top: var(--s3); }

/* The diagram is tall and narrow like the valley it describes. Left to fill
   the column it towers over the copy beside it, so it is capped and centred. */
.map { width: 100%; max-width: 430px; height: auto; margin-inline: auto; }
.map__water { fill: rgba(38, 67, 111, 0.34); stroke: rgba(232, 181, 138, 0.34); stroke-width: 1; }
.map__pin { fill: var(--scarlet); }
.map__pin--muted { fill: var(--amber); }
.map__halo { fill: none; stroke: var(--scarlet); stroke-width: 1; opacity: 0.5; }
/* Type inside the drawing is measured in the drawing's own units, so this is
   thirteen 520ths of however wide the map ends up, not thirteen pixels. At
   the map's full 430 that lands a shade under 11px; squeezed onto a phone the
   same figure would land near 7px, which is a label you can see but not read,
   so the phone is given its own size and its tracking eased to pay for it. */
.map__label {
  font-family: var(--ui);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  fill: var(--paper-70);
}
@media (max-width: 480px) {
  .map__label { font-size: 15px; letter-spacing: 0.09em; }
}
.map__label--key { fill: var(--paper); }
.map__hair { stroke: var(--paper-14); stroke-width: 1; }
.map__link { stroke: var(--scarlet); stroke-width: 1; stroke-dasharray: 3 4; opacity: 0.7; }

/* The Shan hills flanking the valley on both sides, and the floating garden
   strips in the lake's southern reach. Both are real, named features of the
   place, kept quiet enough not to compete with the route lines and pins. */
.map__ridge { fill: none; stroke: var(--paper-14); stroke-width: 1; stroke-linejoin: round; }
.map__garden { stroke: var(--amber); stroke-width: 1.5; stroke-linecap: round; opacity: 0.4; }
.map__compass { stroke: var(--paper-46); stroke-width: 1; fill: none; }
.map__compass-fill { fill: var(--paper-46); }

/* Transparent, so the lake shows through, which matters most on a phone where
   the band is a tall stack. Dividers are borders on the tiles rather than the
   container's background showing through a 1px gap, because that trick needs
   an opaque tile to work. */
.facts {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  border: 1px solid rgba(247, 237, 234, 0.18);
}
@media (max-width: 620px) { .facts { grid-template-columns: 1fr; } }
.fact {
  background: transparent;
  padding: var(--s3);
}
.fact + .fact { border-left: 1px solid rgba(247, 237, 234, 0.18); }
@media (max-width: 620px) {
  .fact + .fact { border-left: 0; border-top: 1px solid rgba(247, 237, 234, 0.18); }
}
.fact__value {
  font-family: var(--display);
  font-size: clamp(1.5rem, 2.6vw, 2.1rem);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.fact__unit { font-size: 0.5em; }
.fact__label {
  margin-top: var(--s1);
  font-size: var(--micro);
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--paper-70);
}

/* The practical block. Everything needed to actually book a room. */
.contact {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s4) var(--s6);
  margin-top: clamp(40px, 4.6vw, 72px);
  padding-top: var(--s4);
  border-top: 1px solid var(--border);
}
/* The contact block is the last thing in The Venue, and Getting There opens
   with its own full section padding directly underneath. Two full paddings
   back to back read as a gap rather than a break, so this one gives way. */
.section--venue { padding-bottom: clamp(48px, 5vw, 84px); }
@media (max-width: 780px) { .contact { grid-template-columns: 1fr; gap: var(--s4); } }
.contact__label {
  font-size: var(--micro);
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--amber);
  margin-bottom: var(--s2);
}
.contact__value { font-size: 14px; line-height: 1.8; color: var(--paper-70); }

.contact__actions {
  list-style: none;
  padding: 0;
  margin-top: 2px;
  display: grid;
  justify-items: start;
}

/* ============================================================
   15. TRAVEL / the journey ladder
   ============================================================ */
.routes { display: grid; gap: var(--s2); }

.route {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s3);
  align-items: start;
  padding: var(--s4) var(--s3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: rgba(36, 9, 15, 0.42);
  transition: border-color 240ms var(--ease-out), background-color 240ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .route:hover { border-color: var(--scarlet); background: rgba(36, 9, 15, 0.72); }
}

/* These are two alternatives, not two steps, so the marker names the mode
   rather than counting. Reading down the column tells you the actual choice
   you are making: air or road. */
.route__mode {
  font-family: var(--ui);
  font-size: var(--micro);
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--scarlet);
  padding-top: 7px;
  writing-mode: vertical-rl;
  text-orientation: mixed;
}
/* Upright, the mode label is a spine down the left edge of the card. On a
   phone that spine costs the itinerary a third of its width, so the label
   lies down above the route and gives the column back. */
@media (max-width: 640px) {
  .route { grid-template-columns: minmax(0, 1fr); gap: var(--s2); }
  .route__mode { writing-mode: horizontal-tb; padding-top: 0; }
}
.route__title {
  font-family: var(--display);
  font-size: clamp(1.3rem, 2.2vw, 1.75rem);
  line-height: 1.1;
  font-weight: 400;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s2);
}
.route__flag {
  font-family: var(--ui);
  font-size: var(--micro);
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--amber);
}

/* Times sit on the legs they belong to rather than as one total, because a
   total would be a guess and a leg time is a fact. */
.route__legs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s1) 12px;
  margin-top: var(--s3);
  list-style: none;
  padding: 0;
}
.route__legs li { display: flex; align-items: center; gap: 12px; }
.route__stop {
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper);
}
.route__hop {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--micro);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--amber);
}
.route__hop::before, .route__hop::after {
  content: "";
  width: 14px; height: 1px;
  background: var(--scarlet);
  opacity: 0.65;
  flex: none;
}
/* On a narrow screen the legs wrap into ragged rows and the hop labels break
   mid-phrase, so the row becomes a vertical ladder instead: one stop per line,
   with the leg that gets you there sitting above it. */
@media (max-width: 560px) {
  .route__legs { display: grid; gap: var(--s2); }
  .route__legs li { display: grid; gap: 5px; justify-items: start; }
  .route__hop::after { display: none; }
}

.route__note {
  margin-top: var(--s3);
  font-size: 14px;
  line-height: 1.7;
  color: var(--paper-70);
  max-width: 56ch;
}

/* On a wide screen the itinerary is one line and the note is two sentences, and
   stacked in one column they left the right half of every card empty. Side by
   side, the route reads left to right and the note sits alongside it. */
@media (min-width: 1180px) {
  .route > div {
    display: grid;
    grid-template-columns: minmax(0, 1.4fr) minmax(0, 0.8fr);
    column-gap: clamp(40px, 5vw, 88px);
    align-items: center;
  }
  .route__title { grid-column: 1; }
  .route__legs { grid-column: 1; }
  .route__note { grid-column: 2; grid-row: 1 / span 2; margin-top: 0; max-width: 42ch; }
}

.notes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s3);
  margin-top: var(--s6);
}
@media (max-width: 820px) { .notes { grid-template-columns: 1fr; } }

.advisory {
  display: grid;
  gap: var(--s2);
  padding: var(--s3);
  border-inline-start: 2px solid var(--scarlet);
  background: rgba(228, 64, 95, 0.07);
}
.advisory__tag {
  font-size: var(--micro);
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--scarlet);
}
.advisory p { font-size: 14px; line-height: 1.7; color: var(--paper-70); }

/* Underline rather than a bottom border, so the rule stays welded to the
   words: a border sits at the bottom of the box, and the moment the box is
   padded out to something a thumb can hit, it detaches and floats. */
.link {
  color: var(--paper);
  text-decoration: underline;
  text-decoration-color: var(--scarlet);
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  transition: color 200ms var(--ease-out), text-decoration-color 200ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .link:hover { color: var(--scarlet); text-decoration-color: var(--paper-46); }
}

/* A link that is the whole line gets a row a thumb can find. */
.link--row {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  text-underline-offset: 5px;
}

/* Links inside running text cannot have a row of their own, so they get depth
   instead: vertical padding on an inline box overflows the line rather than
   reflowing it, and the paragraph is left exactly where it was. */
@media (pointer: coarse) {
  .prose .link, .route__note .link { padding-block: 15px; }
}

/* ============================================================
   16. THE WEEKEND / two days
   ============================================================ */
.agenda {
  margin-top: clamp(40px, 5vw, 72px);
  border-top: 1px solid var(--border);
}

.event {
  display: grid;
  grid-template-columns: minmax(88px, 0.3fr) minmax(0, 1fr);
  gap: var(--s3) var(--s4);
  align-items: start;
  padding-block: clamp(28px, 3.2vw, 40px);
  border-bottom: 1px solid var(--border);
}
@media (max-width: 640px) {
  .event { grid-template-columns: 1fr; gap: var(--s2); padding-block: var(--s3); }
}

/* The day is the anchor: numeral in the display face, ordinal in amber,
   weekday as a tracked label underneath. */
.event__date {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 6px;
}
.event__num {
  font-family: var(--display);
  font-size: clamp(2.6rem, 5vw, 3.9rem);
  line-height: 0.85;
}
.event__ord {
  font-family: var(--display);
  font-style: italic;
  font-size: clamp(0.95rem, 1.5vw, 1.25rem);
  color: var(--amber);
  margin-inline-start: -4px;
}
.event__mon {
  font-family: var(--display);
  font-size: clamp(1.05rem, 1.7vw, 1.4rem);
}
.event__day {
  flex-basis: 100%;
  margin-top: 10px;
  font-family: var(--ui);
  font-size: var(--micro);
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--paper-46);
}

.event__title {
  font-family: var(--display);
  font-size: clamp(1.7rem, 3.2vw, 2.6rem);
  line-height: 1;
  font-weight: 400;
}
.event__meta {
  margin-top: var(--s2);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s1) 12px;
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--amber);
}
.event__pipe { color: var(--border); }
.event__note {
  margin-top: var(--s3);
  font-size: 14px;
  line-height: 1.72;
  color: var(--paper-70);
  max-width: 52ch;
}

.agenda__foot {
  margin-top: var(--s4);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--paper-46);
}

/* ============================================================
   17. RSVP FORM
   ============================================================ */
/* The weekend panel still stacks below 900px, so its scrim stops being
   directional there. */
@media (max-width: 900px) {
  .panel--weekend .panel__media::after {
    background: linear-gradient(to bottom,
      rgba(20, 6, 11, 0.90) 0%, rgba(20, 6, 11, 0.82) 40%, rgba(20, 6, 11, 0.90) 100%);
  }
}

.section--rsvp { padding-block: 0; }

/* Native #rsvp hash jumps still need clearance under the fixed nav. The
   click handler in site.js centres the photo+form pair itself; this margin
   is only the fallback if JS does not run. */
#rsvp { scroll-margin-top: clamp(56px, 8vh, 80px); }

.rsvp__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(28px, 4vw, 72px);
  /* Not stretched. Choosing "yes" roughly doubles the form's height, and a
     photograph stretched to match it collapsed to a 0.4 aspect sliver showing
     only the right third of the frame. The picture keeps its own proportion
     and the form grows past it. */
  align-items: start;
  justify-items: stretch;
  width: 100%;
  margin-inline: auto;
  padding-block: var(--section-lg);
}
@media (max-width: 940px) {
  .rsvp__grid { grid-template-columns: 1fr; gap: var(--s6); }
}

/* The photograph and the words are one object: the picture fills the cell and
   the copy sits in the bottom-left of it. Stretching the cell rather than
   fixing a height lets the form on the right set how tall this gets, so the
   two columns finish level instead of one running past the other. */
.rsvp__stage {
  position: relative;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  border-radius: var(--radius);
  /* One ratio at every width, so the crop is deterministic rather than a
     function of how tall the form happens to be. Wider than the source's 2:3,
     so the crop is only ever vertical and the full width always shows. At the
     desktop column width this lands within a few pixels of the collapsed
     form's height, so the two columns still read as a pair. */
  aspect-ratio: 4 / 5;
}
@media (max-width: 520px) {
  .rsvp__stage { aspect-ratio: 3 / 4; }
}

.rsvp__photo { position: absolute; inset: 0; }
.rsvp__photo img {
  width: 100%; height: 100%;
  object-fit: cover;
  /* Anchored to the top of the frame. The groom's head begins 44px into an
     1800px photograph, so there is essentially no headroom to spend: any crop
     taken off the top takes the top of his head with it. Every container this
     sits in is wider than 2:3, so the crop is only ever vertical and the whole
     width is always shown; the 150-odd pixels that have to go come off the
     bottom, which is grass the copy is sitting on anyway. */
  /* Right edge, top edge. Vertical crop comes off the bottom, which is grass.
     Horizontal crop, on the narrow-and-tall cells where the column is taller
     than 2:3, comes off the left, which is also grass. The couple are at the
     top right of the frame, so both crops take only the empty half. */
  object-position: 100% 0;
}

/* The grass under the copy already measures 14.5:1 against paper text, so this
   is insurance for the crop moving rather than the thing making it legible.
   It also stops the corner going muddy where the sunlit patch drifts in. */
.rsvp__stage::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to top,
    rgba(20, 6, 11, 0.86) 0%, rgba(20, 6, 11, 0.62) 32%,
    rgba(20, 6, 11, 0.18) 62%, rgba(20, 6, 11, 0.10) 100%);
}

.rsvp__intro {
  position: relative;
  z-index: 1;
  padding: clamp(24px, 3.2vw, 44px);
  max-width: 34ch;
}
.rsvp__intro .prose { margin-top: var(--s3); }
@media (max-width: 940px) {
  .rsvp__intro { max-width: 44ch; }
}

.form { display: grid; gap: var(--s4); }
.form__row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s3); }
@media (max-width: 620px) { .form__row { grid-template-columns: 1fr; } }

.field { display: grid; gap: 10px; align-content: start; }
.fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
.fieldset legend { padding: 0; }
.field__label {
  font-size: var(--micro);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--paper-70);
}
.field__opt { color: var(--paper-46); margin-inline-start: 8px; }
.field__hint { font-size: 12px; color: var(--paper-46); line-height: 1.5; }
/* Sits with the field above it rather than floating in the form's own gap. */
.field__hint--full { margin-top: calc(10px - var(--s4)); }

.input {
  width: 100%;
  padding: 14px var(--s2);
  background: rgba(20, 6, 11, 0.6);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--paper);
  font-size: 15px;
  transition: border-color 200ms var(--ease-out), background-color 200ms var(--ease-out);
}
/* iOS Safari zooms the whole page into any focused field set under 16px, and
   leaves it zoomed after the field is left. Every field on this form was 15px,
   so every tap on a phone started with the page jumping. Touch devices and
   narrow screens get 16px; a mouse on a desktop keeps the 15px it was set at. */
@media (pointer: coarse), (max-width: 940px) {
  .input { font-size: 16px; }
}
.input::placeholder { color: var(--paper-46); }
.input:hover { border-color: rgba(228, 64, 95, 0.5); }
.input:focus { border-color: var(--scarlet); outline: none; background: rgba(20, 6, 11, 0.85); }
.input:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
textarea.input { resize: vertical; min-height: 92px; line-height: 1.6; }

.select { position: relative; display: grid; }
.select select.input { appearance: none; cursor: pointer; padding-inline-end: var(--s6); }
.select select.input:invalid { color: var(--paper-46); }
.select__caret {
  position: absolute;
  inset-inline-end: var(--s2);
  top: 50%;
  width: 7px; height: 7px;
  border-right: 1px solid var(--paper-46);
  border-bottom: 1px solid var(--paper-46);
  transform: translateY(-70%) rotate(45deg);
  pointer-events: none;
  transition: border-color 200ms var(--ease-out);
}
.select:focus-within .select__caret { border-color: var(--scarlet); }
/* The native menu paints in the OS palette, so give it a matching one. */
.input option { background: var(--surface); color: var(--paper); }

.input[aria-invalid="true"] { border-color: var(--scarlet); }
.field__error {
  font-size: 12px;
  color: var(--scarlet);
  letter-spacing: 0.04em;
}
.field__error:empty { display: none; }

/* Conditional blocks. Grid rows animate a real height without measuring. */
.reveal {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows 340ms var(--ease-out), opacity 240ms var(--ease-out);
}
.reveal[data-open="true"] { grid-template-rows: 1fr; opacity: 1; }
.reveal__inner { overflow: hidden; display: grid; gap: var(--s4); }
.reveal[data-open="false"] { pointer-events: none; }

/* Segmented choice, replacing a plain radio pair.
   The input is a 1px element inside the label rather than a transparent layer
   stretched over it: stretched, it paints above its own label (it is
   positioned, the label is not) and swallows the click without registering it,
   which leaves the control working by keyboard and dead to the mouse. */
.choice { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s1); }
.choice__item { position: relative; display: block; cursor: pointer; }
.choice__item input {
  position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0;
}
.choice__face {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s1);
  padding: 14px var(--s2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--paper-70);
  text-align: center;
  cursor: pointer;
  transition: border-color 200ms var(--ease-out), color 200ms var(--ease-out),
              background-color 200ms var(--ease-out), transform 160ms var(--ease-out);
}
.choice__item input:active + .choice__face { transform: scale(0.97); }
@media (hover: hover) and (pointer: fine) {
  .choice__item input:hover + .choice__face { border-color: rgba(228, 64, 95, 0.55); color: var(--paper); }
}
.choice__item input:checked + .choice__face {
  background: var(--scarlet);
  border-color: var(--scarlet);
  color: var(--on-scarlet);
  font-weight: 600;
}
.choice__item input:focus-visible + .choice__face { outline: 2px solid var(--amber); outline-offset: 2px; }

/* Toggle list for per-event attendance */
.events-pick { display: grid; gap: var(--s1); }
.pick {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  transition: border-color 200ms var(--ease-out), background-color 200ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .pick:hover { border-color: rgba(228, 64, 95, 0.55); }
}
.pick input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.pick__box {
  width: 18px; height: 18px;
  border: 1px solid var(--paper-46);
  border-radius: var(--radius);
  display: grid;
  place-items: center;
  flex: none;
  transition: background-color 180ms var(--ease-out), border-color 180ms var(--ease-out);
}
.pick__box svg { opacity: 0; transform: scale(0.9); transition: opacity 160ms var(--ease-out), transform 160ms var(--ease-out); }
.pick input:checked + .pick__box { background: var(--scarlet); border-color: var(--scarlet); }
.pick input:checked + .pick__box svg { opacity: 1; transform: scale(1); }
.pick:has(input:checked) { border-color: var(--scarlet); }
.pick input:focus-visible + .pick__box { outline: 2px solid var(--amber); outline-offset: 2px; }
.pick__name { font-size: 14px; }
.pick__when { font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--paper-46); }

/* Form status */
.form__status {
  display: none;
  padding: var(--s3);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  font-size: 14px;
  line-height: 1.65;
}
.form__status[data-state="error"] {
  display: block;
  border-color: var(--scarlet);
  background: rgba(228, 64, 95, 0.09);
}

/* Success replaces the form entirely */
.rsvp__done {
  display: none;
  padding: clamp(28px, 4vw, 56px);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: rgba(36, 9, 15, 0.6);
}
.rsvp__done[data-visible="true"] { display: block; }
.rsvp__done h3 {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.9rem, 3.4vw, 2.8rem);
  line-height: 1;
  margin-bottom: var(--s3);
}

.spinner {
  width: 13px; height: 13px;
  border: 1.5px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin 620ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ============================================================
   18. FAQ
   ============================================================ */
.faq { border-top: 1px solid var(--border); }
.faq__item { border-bottom: 1px solid var(--border); }
/* The trigger is wrapped in a heading so screen readers can jump between
   questions; the heading itself carries no visual weight of its own. */
.faq__heading { margin: 0; font: inherit; font-weight: inherit; }

.faq__trigger {
  width: 100%;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s3);
  align-items: center;
  padding-block: var(--s4);
  padding-inline: 0;   /* buttons carry a default inline padding that would
                          otherwise indent every question off the page grid */
  background: none;
  border: 0;
  text-align: left;
  cursor: pointer;
  color: var(--paper);
  transition: color 200ms var(--ease-out);
}
.faq__q {
  font-family: var(--display);
  font-size: clamp(1.15rem, 1.9vw, 1.55rem);
  line-height: 1.25;
}
@media (hover: hover) and (pointer: fine) {
  .faq__trigger:hover .faq__q { color: var(--scarlet); }
}

/* Plus/minus built from two rules so the state change is a rotation, not a glyph swap */
.faq__sign { position: relative; width: 13px; height: 13px; flex: none; }
.faq__sign::before, .faq__sign::after {
  content: "";
  position: absolute;
  inset-block-start: 50%; inset-inline-start: 0;
  width: 100%; height: 1px;
  background: var(--scarlet);
  transition: transform 240ms var(--ease-out);
}
.faq__sign::after { transform: rotate(90deg); }
.faq__trigger[aria-expanded="true"] .faq__sign::after { transform: rotate(0deg); }

.faq__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 260ms var(--ease-out);
}
.faq__panel[data-open="true"] { grid-template-rows: 1fr; }
.faq__panel > div { overflow: hidden; }
.faq__panel p {
  padding-bottom: var(--s4);
  max-width: 66ch;
  color: var(--paper-70);
  font-size: 15px;
  line-height: 1.72;
}

/* ============================================================
   19. FOOTER
   ============================================================ */
.footer {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-top: 1px solid var(--border);
  padding-block: clamp(72px, 8vw, 124px) var(--s4);
  text-align: center;
}
.footer__media { position: absolute; inset: 0; z-index: -1; }
.footer__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% 26%;
  opacity: 0.3;
}
.footer__media::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(to bottom, var(--ink) 0%, rgba(20, 6, 11, 0.62) 46%, var(--ink) 100%);
}
.footer__names {
  font-family: var(--display);
  font-size: clamp(2.2rem, 6vw, 4.4rem);
  line-height: 1;
  margin-bottom: var(--s3);
}
/* Client logo drops in here without touching the layout. Sized to the display
   line it replaces, so the footer keeps its proportions either way. */
.footer__names .logo { height: clamp(104px, 13vw, 168px); }
.footer__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  font-size: var(--micro);
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--paper-46);
  line-height: 2;
}

/* ============================================================
   20. REVEAL (GSAP targets; CSS holds the pre-state so there is no flash)
   ============================================================ */
[data-reveal] { opacity: 0; transform: translateY(18px); }
[data-reveal-group] > * { opacity: 0; transform: translateY(18px); }
.no-js [data-reveal],
.no-js [data-reveal-group] > * { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  [data-reveal], [data-reveal-group] > * { opacity: 1 !important; transform: none !important; }
  .scroll-cue__line::after { animation: none; }
  .chroma__layer { transition: none; }
  .sound[aria-pressed="true"] .sound__bars i { animation: none; transform: scaleY(0.7); }
  .spinner { animation-duration: 1600ms; }
  .approach__fill { transition-duration: 1ms; }
}
