/* ==========================================================================
   Ристе & Катерина — 25.09.2026
   Design system + layout + motion primitives
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */
:root {
  /* Palette — warm autumnal ivory / espresso / antique gold / wine / olive */
  --ivory:        #F6F1E8;
  --ivory-lift:   #FBF8F2;
  --ivory-sink:   #EFE7DA;
  --paper:        #FDFBF7;

  --ink:          #241E1B;
  --ink-soft:     #4A403A;
  --ink-mute:     #7C6F66;
  --ink-faint:    #A89A8E;

  --wine:         #4A2231;
  --wine-deep:    #2E1520;
  --olive:        #6E7256;
  --olive-deep:   #4C4F3A;
  --blush:        #E7D3C8;

  /* Dusty rose, used as the field the RSVP card sits on. Deliberately muted so
     it reads as a warm backdrop next to the blush swatch, not a second accent. */
  --rose:         #BE9995;
  --rose-deep:    #A87F7B;

  /* Terracotta clay for the invitation band, where it is multiplied over a
     floral photograph. Picked warmer and more orange than --rose so the two
     coloured sections do not read as the same band repeated. */
  --clay:         #B8836C;
  --clay-deep:    #99604B;

  /* The colour those clay bands actually RENDER as, once --clay is multiplied
     over the floral photo and the dark wash is composited on top. Measured, not
     guessed: the photo averages rgb(184,177,151), which multiplied by --clay
     gives rgb(133,91,64), and the wash lands it between #7B513A (mid-band) and
     #6A422F (top and bottom). Used for flat elements that need to match the
     bands without carrying the photograph — currently the announcement strip. */
  --clay-band:    #73442E;

  --gold:         #BFA06A;
  --gold-lift:    #DCC79B;
  --gold-deep:    #96794A;

  --line:         rgba(36, 30, 27, 0.14);
  --line-soft:    rgba(36, 30, 27, 0.08);
  --line-gold:    rgba(191, 160, 106, 0.42);

  /* Type */
  --font-display: "Cormorant Garamond", "Iowan Old Style", Georgia, "Times New Roman", serif;
  --font-body:    "Montserrat", "Segoe UI", system-ui, -apple-system, sans-serif;

  --step--1: clamp(0.75rem, 0.72rem + 0.15vw, 0.82rem);
  --step-0:  clamp(0.9rem, 0.86rem + 0.2vw, 1rem);
  --step-1:  clamp(1.05rem, 0.98rem + 0.35vw, 1.25rem);
  --step-2:  clamp(1.35rem, 1.2rem + 0.7vw, 1.85rem);
  --step-3:  clamp(1.8rem, 1.5rem + 1.4vw, 2.9rem);
  --step-4:  clamp(2.3rem, 1.7rem + 2.9vw, 4.6rem);
  /* Space */
  --gutter:  clamp(1.25rem, 4vw, 3.5rem);
  --section: clamp(5.5rem, 11vw, 11rem);
  --measure: 62ch;
  --shell:   1320px;

  /* Depth */
  --shadow-soft:    0 1px 40px rgba(36, 30, 27, 0.06);
  --shadow-frame:   0 40px 90px -36px rgba(46, 21, 32, 0.42);
  --shadow-deep:    0 60px 140px -50px rgba(46, 21, 32, 0.55);

  /* Motion */
  --ease-out:   cubic-bezier(0.16, 1, 0.3, 1);
  --ease-inout: cubic-bezier(0.62, 0.05, 0.2, 1);
  --ease-soft:  cubic-bezier(0.4, 0, 0.2, 1);
  --dur-fast:   0.35s;
  --dur:        0.7s;
  --dur-slow:   1.2s;

  --radius:     2px;
  --arch:       50% 50% 6px 6px / 42% 42% 6px 6px;

  /* Height of the thin announcement bar. The header is NOT measured here on
     purpose — it sits in normal flow and sticks, so nothing has to know how
     tall it is (see section 7). */
  --bar-h:      34px;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}
/* The Lenis-style wheel hijack is gone (see motion.js §1) — the page scrolls
   natively, so its helper classes are no longer applied to <html>. */

body {
  background: var(--ivory);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--step-0);
  font-weight: 400;
  line-height: 1.75;
  letter-spacing: 0.01em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
  text-rendering: optimizeLegibility;
}
body.is-locked { overflow: hidden; }

img, video, svg { display: block; max-width: 100%; height: auto; }
button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
ul, ol { list-style: none; padding: 0; }

:focus-visible {
  outline: 1.5px solid var(--gold-deep);
  outline-offset: 4px;
}

::selection { background: var(--gold-lift); color: var(--ink); }

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

.skip-link {
  position: fixed; top: 0; left: 50%;
  transform: translate(-50%, -110%);
  z-index: 200;
  padding: 0.75rem 1.5rem;
  background: var(--ink); color: var(--ivory);
  font-size: var(--step--1); letter-spacing: 0.18em; text-transform: uppercase;
  transition: transform var(--dur-fast) var(--ease-out);
}
.skip-link:focus { transform: translate(-50%, 0); }

/* --------------------------------------------------------------------------
   3. Typography primitives
   -------------------------------------------------------------------------- */
.display {
  font-family: var(--font-display);
  font-weight: 300;
  line-height: 0.95;
  letter-spacing: -0.015em;
  font-feature-settings: "liga" 1, "dlig" 1;
}
.display em {
  font-style: italic;
  font-weight: 300;
}

/* --- Italic headings ------------------------------------------------------
   Every display-face heading is set in Cormorant Garamond italic, matching
   the reference hero's treatment. Deliberately NOT included: numerals
   (countdown, schedule times, the hero date) and navigation, where the
   upright forms stay more legible and the roman/italic contrast is the point.
   -------------------------------------------------------------------------- */
.section__title,
.dress__what,
.info__v,
.rsvp__thanks h3,
.count__done {
  font-style: italic;
}

.eyebrow {
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--ink-mute);
  display: inline-flex;
  align-items: center;
  gap: 0.9em;
}
.eyebrow::before,
.eyebrow::after {
  content: "";
  width: clamp(1.5rem, 5vw, 3.25rem);
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--line-gold), transparent);
}

.lede {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 300;
  line-height: 1.5;
  color: var(--ink-soft);
  max-width: 44ch;
}

.prose p + p { margin-top: 1.1em; }
.prose { color: var(--ink-soft); max-width: var(--measure); }

/* Cormorant defaults to old-style figures; clocks and counters need the
   digits to share one height, so force lining + tabular everywhere numeric. */
.tnum {
  font-variant-numeric: lining-nums tabular-nums;
  font-feature-settings: "lnum" 1, "tnum" 1;
}

/* --------------------------------------------------------------------------
   4. Layout shell
   -------------------------------------------------------------------------- */
.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.shell--narrow { max-width: 900px; }

.section {
  position: relative;
  padding-block: var(--section);
}
.section--tint { background: var(--ivory-sink); }
.section--ink {
  background: var(--wine-deep);
  color: var(--ivory);
}

/* Clay section with a floral photograph beneath the colour. Three background
   layers, topmost first:
     1. a soft dark wash, normal blend — buys contrast for the ivory type
     2. flat clay, MULTIPLY — this is what tints the photograph: white petals
        come through as clay, shadowed folds as deep brown
     3. the photograph itself (set by app.js from settings.json, so it is
        swappable from /admin; the fallback keeps the section working alone)
   background-color sits behind all of it, so a missing image degrades to a
   plain clay band rather than to nothing. */
.section--clay {
  color: var(--ivory);
  background-color: var(--clay);
  background-image:
    linear-gradient(178deg, rgba(70, 32, 24, 0.40), rgba(70, 32, 24, 0.16) 52%, rgba(70, 32, 24, 0.46)),
    linear-gradient(0deg, var(--clay), var(--clay)),
    /* Fallback is the shipped file, so the band still looks right if the
       content fetch fails; app.js overrides it from settings.json. Note the
       path is relative to THIS stylesheet, not to the page. */
    var(--section-bg, url("../img/floral-clay.jpg"));
  background-size: cover, cover, cover;
  background-position: center, center, center 42%;
  background-repeat: no-repeat;
  background-blend-mode: normal, multiply, normal;
}

/* The invitation and the countdown are both clay and sit close together, with
   only the ribbon between them. Showing the identical crop twice reads as a
   mistake, so this variant frames a lower part of the same bloom — same
   treatment, different framing. */
.section--clay-alt { background-position: center, center, center 82%; }

.section--ink .eyebrow,
.section--clay .eyebrow { color: var(--gold-lift); }
.section--ink .prose,
.section--ink .lede,
.section--clay .prose,
.section--clay .lede { color: rgba(246, 241, 232, 0.82); }
/* The gold hairline and ornament read as dirt against clay; lift them to ivory. */
.section--clay .rule { background: rgba(246, 241, 232, 0.5); }
.section--clay .ornament { color: rgba(246, 241, 232, 0.66); }
.section--clay .invite__quote { color: var(--ivory); }
.section--clay .invite__sign { color: rgba(246, 241, 232, 0.78); }

.section__head {
  display: grid;
  gap: clamp(1rem, 2vw, 1.5rem);
  justify-items: center;
  text-align: center;
  margin-bottom: clamp(3rem, 6vw, 5.5rem);
}
.section__title { font-size: var(--step-4); }

/* Paper grain — sits above everything, ignores pointer events */
.grain {
  position: fixed;
  /* Oversized on purpose: grainDrift translates up to ±2%, and at inset:0
     that would expose an unmultiplied strip along one edge. */
  inset: -6%;
  z-index: 90;
  pointer-events: none;
  opacity: 0.32;
  mix-blend-mode: multiply;
  background-image: var(--grain-url);
  background-size: 220px 220px;
}
@media (prefers-reduced-motion: no-preference) {
  .grain { animation: grainDrift 8s steps(6) infinite; }
}
@keyframes grainDrift {
  0%, 100% { transform: translate(0, 0); }
  20% { transform: translate(-2%, 1%); }
  40% { transform: translate(1%, -2%); }
  60% { transform: translate(-1%, -1%); }
  80% { transform: translate(2%, 2%); }
}

/* --------------------------------------------------------------------------
   Custom cursor — ivory/gold ring with a gold dot at its centre.
   Desktop-only: touch and stylus keep their native behaviour entirely.
   -------------------------------------------------------------------------- */
.cursor__ring,
.cursor__dot {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 999;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  will-change: transform;
  display: none;
}

@media (hover: hover) and (pointer: fine) {
  .cursor__ring, .cursor__dot { display: block; }

  /* Hide the native arrow everywhere it would otherwise reappear. */
  body, a, button, label, summary, select,
  [role="button"], .choice__opt, .field select { cursor: none; }

  /* The JS writes `transform` on these two every animation frame, so NEITHER may
     transition transform — a 0.25s tween restarted 60 times a second is what
     made the dot smear along behind the pointer instead of tracking it.
     Size changes live on ::before as a scale() so they stay on the compositor;
     animating width/height/margin re-ran layout on every frame of every hover. */
  .cursor__ring {
    width: 34px;
    height: 34px;
    margin: -17px 0 0 -17px;
    transition: opacity 0.3s var(--ease-soft);
  }
  .cursor__ring::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: 1.5px solid var(--gold-deep);
    background: rgba(150, 121, 74, 0.07);
    transition: transform 0.38s var(--ease-out),
                border-color 0.3s var(--ease-soft),
                background-color 0.3s var(--ease-soft);
  }
  .cursor__dot {
    width: 6px;
    height: 6px;
    margin: -3px 0 0 -3px;
    background: var(--gold-deep);
    transition: opacity 0.2s var(--ease-soft),
                background-color 0.3s var(--ease-soft);
  }

  /* Visible once the pointer has actually moved into the page. */
  .has-cursor .cursor__ring { opacity: 1; }
  .has-cursor .cursor__dot  { opacity: 1; }

  /* Over the dark sections the ring switches to ivory, as in the reference. */
  .cursor-dark .cursor__ring::before {
    border-color: rgba(246, 241, 232, 0.9);
    background: rgba(246, 241, 232, 0.1);
  }
  .cursor-dark .cursor__dot { background: var(--gold-lift); }

  /* Over anything clickable the ring opens up and the dot recedes. */
  .cursor-hover .cursor__ring::before {
    transform: scale(1.7);
    background: rgba(150, 121, 74, 0.13);
  }
  .cursor-dark.cursor-hover .cursor__ring::before { background: rgba(246, 241, 232, 0.16); }
  .cursor-hover .cursor__dot { opacity: 0.35; }

  /* Press feedback. */
  .cursor-down .cursor__ring::before { transform: scale(0.76); }
  .cursor-down.cursor-hover .cursor__ring::before { transform: scale(1.45); }

  /* Free-text fields keep the native I-beam — losing the caret hurts the
     RSVP. Radios/checkboxes are clickable, not typable, so they keep none. */
  input[type="text"], input[type="email"], input[type="tel"],
  input:not([type]), textarea { cursor: auto; }
  input[type="radio"], input[type="checkbox"] { cursor: none; }
  .cursor-text .cursor__ring,
  .cursor-text .cursor__dot { opacity: 0; }
}

/* Hairline rule that draws itself in */
.rule {
  height: 1px;
  width: min(320px, 60%);
  background: linear-gradient(90deg, transparent, var(--line-gold) 20%, var(--line-gold) 80%, transparent);
  transform: scaleX(0);
  transform-origin: center;
  transition: transform var(--dur-slow) var(--ease-out) var(--rv-delay, 0s);
}
.is-in .rule, .rule.is-in { transform: scaleX(1); }

/* Diamond ornament */
.ornament {
  display: grid;
  place-items: center;
  gap: 0;
  color: var(--gold);
}
.ornament svg { width: clamp(28px, 4vw, 44px); height: auto; }

/* --------------------------------------------------------------------------
   5. Buttons
   -------------------------------------------------------------------------- */
.btn {
  --btn-fg: var(--ivory);
  --btn-bg: var(--ink);
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.7em;
  padding: 1.05em 2.4em;
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--btn-fg);
  background: var(--btn-bg);
  border-radius: var(--radius);
  overflow: hidden;
  isolation: isolate;
  transition: color var(--dur-fast) var(--ease-soft),
              transform var(--dur-fast) var(--ease-out),
              /* .btn--ghost animates its inset outline through box-shadow, so the
                 transition stays even though no button casts a shadow. */
              box-shadow var(--dur-fast) var(--ease-out);
}
.btn::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--gold);
  transform: translateY(101%);
  transition: transform 0.55s var(--ease-out);
}
/* Lift only — no cast shadow. */
.btn:hover, .btn:focus-visible {
  color: var(--ink);
  transform: translateY(-2px);
}
.btn:hover::after, .btn:focus-visible::after { transform: translateY(0); }
.btn:active { transform: translateY(0); }

.btn--ghost {
  --btn-fg: var(--ink);
  --btn-bg: transparent;
  box-shadow: inset 0 0 0 1px var(--line);
}
.btn--ghost:hover, .btn--ghost:focus-visible { box-shadow: inset 0 0 0 1px transparent; }

.btn--light {
  --btn-fg: var(--wine-deep);
  --btn-bg: var(--ivory);
}

.link-underline {
  position: relative;
  display: inline-block;
  padding-bottom: 2px;
  font-size: var(--step--1);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.link-underline::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.5s var(--ease-out);
}
.link-underline:hover::after, .link-underline:focus-visible::after { transform: scaleX(1); }

/* --------------------------------------------------------------------------
   6. Preloader
   -------------------------------------------------------------------------- */
.veil {
  position: fixed;
  inset: 0;
  z-index: 120;
  display: grid;
  place-items: center;
  background: var(--ivory);
  transition: opacity 0.9s var(--ease-inout), visibility 0.9s;
}
.veil__inner {
  display: grid;
  justify-items: center;
  gap: 1.5rem;
  text-align: center;
}
/* The preloader's wordmark is .brand--veil now; see the lockup block. */
.veil__bar {
  width: clamp(120px, 26vw, 220px);
  height: 1px;
  background: var(--line);
  overflow: hidden;
}
.veil__bar i {
  display: block;
  height: 100%;
  width: 100%;
  background: var(--gold);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 1.1s var(--ease-out);
}
@keyframes veilIn {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}
body.is-loaded .veil {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
body.is-loaded .veil__bar i { transform: scaleX(1); }

/* --------------------------------------------------------------------------
   7. Scroll progress + nav
   -------------------------------------------------------------------------- */
.progress {
  position: fixed;
  top: 0; left: 0;
  /* Above the announcement bar (112), or it would be hidden behind it. */
  z-index: 113;
  height: 2px;
  width: 100%;
  transform: scaleX(0);
  transform-origin: left;
  background: linear-gradient(90deg, var(--gold-deep), var(--gold-lift));
  will-change: transform;
}

/* Thin announcement strip. In normal flow, so it scrolls away and the header
   below it takes over the top of the screen. */
.topbar {
  position: relative;
  z-index: 112;
  height: var(--bar-h);
  display: grid;
  place-items: center;
  background: var(--clay-band);
  color: rgba(246, 241, 232, 0.92);
}
.topbar p {
  font-size: 0.58rem;
  font-weight: 600;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  padding-inline: var(--gutter);
  text-align: center;
}

/* Sticky, not fixed. A sticky element keeps its space in the flow, so the hero
   begins exactly where the header ends — there is no height to guess and no gap
   to open up. The previous version was fixed with the hero pushed down by a
   hardcoded --nav-h, which left a visible strip whenever that guess was off by
   a pixel, and drifted again the moment the sticky padding tightened. */
.nav {
  position: sticky;
  top: 0;
  z-index: 110;
  background: var(--paper);
  box-shadow: 0 1px 0 var(--line-soft);
  transition: background var(--dur) var(--ease-soft),
              box-shadow var(--dur) var(--ease-soft),
              transform 0.55s var(--ease-out);
}
/* Three tracks so the names stay optically centred no matter how wide the two
   menus are — space-between would drift the moment one side gets longer. */
.nav__inner {
  display: grid;
  /* minmax(0,…) lets the side columns shrink instead of forcing the row wider
     than the viewport — a plain 1fr was pushing the language pill off-screen. */
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: clamp(0.75rem, 2vw, 1.5rem);
  max-width: var(--shell);
  margin-inline: auto;
  padding: clamp(0.6rem, 1.1vw, 0.95rem) var(--gutter);
  transition: padding var(--dur) var(--ease-soft);
}

/* ---- Wordmark lockup ----------------------------------------------------
   Built on the reference's logo: the first word on its own line, the second
   line opening with a small italic beat before the second word, then a
   letterspaced line beneath. Set in the display serif, uppercase, tracked
   tight so the two words read as one block.

   --brand-size drives everything, so the header, preloader and footer differ
   by one declaration each (see the modifiers at the end of this block). */
.brand {
  --brand-size: clamp(1.3rem, 2.4vw, 2rem);
  display: grid;
  justify-items: center;
  gap: 0.34em;
  text-align: center;
  white-space: nowrap;
  font-family: var(--font-display);
  font-weight: 400;
  text-transform: uppercase;
  line-height: 0.9;
  color: var(--ink);
}
.brand__a,
.brand__b {
  display: block;
  /* Tight, near-touching tracking is what makes it read as a wordmark rather
     than as a line of type. */
  letter-spacing: 0.012em;
}
.brand__a { font-size: var(--brand-size); }
/* The second name is shorter, so it is set larger — with the ampersand filling
   the left gap, the two lines end up close to the same width, which is what
   gives the lockup its block shape. */
.brand__b {
  font-size: calc(var(--brand-size) * 1.34);
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 0.12em;
  margin-top: 0.06em;
}
/* The italic beat: small, gold, sitting low against the big capitals. */
.brand__amp {
  font-size: 0.4em;
  font-style: italic;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--gold-deep);
  transform: translateY(-0.12em);
}
.brand__date {
  font-family: var(--font-body);
  font-size: calc(var(--brand-size) * 0.26);
  font-weight: 600;
  letter-spacing: 0.34em;
  /* The tracking is applied to the right of the last glyph too, which throws a
     centred line off; half of it back the other way re-centres it. */
  text-indent: 0.34em;
  color: var(--ink-mute);
  line-height: 1;
}

/* The same lockup, larger, on the preloader and in the footer. The footer sits
   on wine, so it inverts to ivory. */
.brand--veil {
  --brand-size: clamp(1.9rem, 6vw, 3.1rem);
  opacity: 0;
  animation: veilIn 1.1s var(--ease-out) 0.15s forwards;
}
.brand--foot {
  --brand-size: clamp(1.6rem, 4.6vw, 2.6rem);
  color: var(--ivory);
}
.brand--foot .brand__amp { color: var(--gold-lift); }
.brand--foot .brand__date { color: rgba(246, 241, 232, 0.6); }
/* The bar is opaque from the start now, so "stuck" only deepens the shadow and
   tightens the padding rather than fading a background in. */
.nav.is-stuck {
  box-shadow: 0 1px 0 var(--line-soft), 0 12px 28px -22px rgba(36, 30, 27, 0.45);
}
/* Stuck must be *tighter* than resting, not looser — the resting padding is
   already small because the stacked monogram carries the header's height. */
.nav.is-stuck .nav__inner { padding-block: clamp(0.35rem, 0.7vw, 0.6rem); }
.nav.is-hidden { transform: translateY(-100%); }

/* Still used by the preloader and the footer. */
.mono {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 400;
  letter-spacing: 0.14em;
  white-space: nowrap;
}
.mono span { color: var(--gold-deep); font-style: italic; padding-inline: 0.1em; }

.nav__links {
  display: flex;
  align-items: center;
  gap: clamp(1.2rem, 2.6vw, 2.4rem);
}
.nav__links--l { justify-content: flex-start; }
.nav__links--r { justify-content: flex-end; }
.nav__link {
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-soft);
  position: relative;
  padding-block: 0.3rem;
  white-space: nowrap;          /* "ДРЕС КОД" must not wrap to two lines */
  transition: color var(--dur-fast) var(--ease-soft);
}
.nav__link::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  height: 1px; width: 100%;
  background: var(--gold);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.45s var(--ease-out);
}
.nav__link:hover, .nav__link:focus-visible { color: var(--ink); }
.nav__link:hover::after,
.nav__link:focus-visible::after,
.nav__link.is-active::after { transform: scaleX(1); }

.nav__side {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: clamp(0.75rem, 1.6vw, 1.25rem);
}

/* MK / EN switch. flex: none so it can never be squeezed by a crowded right
   column — it was previously being pushed off the right edge of the viewport by
   the (invisible but still space-taking) RSVP button that used to sit beside it.
   The header is opaque paper now, so no translucency or blur is needed. */
/* --- Music toggle -----------------------------------------------------------
   Sits beside the language switch. Four bars that stand still when paused and
   step through a staggered bounce while playing, so the state is legible
   without a label. Hidden with [hidden] when no track is configured — an author
   display would beat the UA rule, so the guard is explicit. */
/* Attribution for a CC BY track. Deliberately the quietest thing in the
   footer — the licence asks for a credit that is reasonable to the medium, not
   for a second headline. */
.foot__credit {
  margin: 1.6rem 0 0;
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  line-height: 1.6;
  color: rgba(246, 241, 232, 0.42);
}
.foot__credit[hidden] { display: none; }

.nav__end { display: flex; align-items: center; gap: 0.45rem; flex: none; }

.music {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--ivory);
  color: var(--gold-deep);
  cursor: pointer;
  transition: border-color 0.3s var(--ease-soft), color 0.3s var(--ease-soft),
              background 0.3s var(--ease-soft);
}
.music[hidden] { display: none; }
.music:hover { border-color: var(--line-gold); color: var(--ink); }
.music:focus-visible { outline: 2px solid var(--gold-deep); outline-offset: 2px; }
.music[aria-pressed="true"] { background: var(--gold-deep); border-color: var(--gold-deep); color: var(--ivory); }

.music__eq { display: flex; align-items: flex-end; gap: 2px; height: 13px; }
.music__eq i {
  display: block;
  width: 2px;
  height: 4px;
  border-radius: 1px;
  background: currentColor;
  transform-origin: bottom;
}
/* Paused: a static, slightly uneven bar chart rather than a flat line, so the
   control still reads as "sound" when it is off. */
.music__eq i:nth-child(1) { height: 5px; }
.music__eq i:nth-child(2) { height: 11px; }
.music__eq i:nth-child(3) { height: 7px; }
.music__eq i:nth-child(4) { height: 13px; }

.music[aria-pressed="true"] .music__eq i { animation: eq 1s var(--ease-inout) infinite; }
.music[aria-pressed="true"] .music__eq i:nth-child(2) { animation-delay: 0.18s; }
.music[aria-pressed="true"] .music__eq i:nth-child(3) { animation-delay: 0.36s; }
.music[aria-pressed="true"] .music__eq i:nth-child(4) { animation-delay: 0.54s; }

@keyframes eq {
  0%, 100% { transform: scaleY(0.35); }
  50%      { transform: scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
  .music[aria-pressed="true"] .music__eq i { animation: none; }
}

.lang {
  display: inline-flex;
  align-items: center;
  flex: none;
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.12em;
  border: 1px solid var(--line);
  border-radius: 999px;
  overflow: hidden;
  background: var(--ivory);
}
.lang button {
  padding: 0.45em 0.95em;
  line-height: 1;
  color: var(--ink-mute);
  transition: background var(--dur-fast) var(--ease-soft), color var(--dur-fast) var(--ease-soft);
}
.lang button:hover { color: var(--ink); }
.lang button[aria-pressed="true"] { background: var(--ink); color: var(--ivory); }

/* Burger */
.burger {
  display: none;
  width: 46px; height: 46px;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 999px;
}
.burger i {
  display: block;
  width: 18px; height: 1px;
  background: var(--ink);
  position: relative;
  transition: transform 0.4s var(--ease-out), opacity 0.2s;
}
.burger i::before, .burger i::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%; height: 1px;
  background: var(--ink);
  transition: transform 0.4s var(--ease-out);
}
.burger i::before { top: -6px; }
.burger i::after  { top: 6px; }
body.menu-open .burger i { transform: rotate(45deg); }
body.menu-open .burger i::before { transform: translateY(6px) rotate(-90deg); }
body.menu-open .burger i::after  { opacity: 0; }

/* Mobile drawer */
.drawer {
  position: fixed;
  inset: 0;
  z-index: 105;
  display: grid;
  place-items: center;
  background: var(--ivory);
  clip-path: inset(0 0 100% 0);
  transition: clip-path 0.8s var(--ease-inout);
  visibility: hidden;
}
body.menu-open .drawer { clip-path: inset(0 0 0 0); visibility: visible; }
.drawer__list {
  display: grid;
  gap: clamp(1rem, 4vh, 2rem);
  text-align: center;
}
.drawer__list a {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 300;
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out);
}
body.menu-open .drawer__list a { opacity: 1; transform: none; }
body.menu-open .drawer__list a:nth-child(1) { transition-delay: 0.18s; }
body.menu-open .drawer__list a:nth-child(2) { transition-delay: 0.24s; }
body.menu-open .drawer__list a:nth-child(3) { transition-delay: 0.30s; }
body.menu-open .drawer__list a:nth-child(4) { transition-delay: 0.36s; }
body.menu-open .drawer__list a:nth-child(5) { transition-delay: 0.42s; }
body.menu-open .drawer__list a:nth-child(6) { transition-delay: 0.48s; }
.drawer__foot {
  position: absolute;
  bottom: clamp(2rem, 6vh, 4rem);
  left: 0; right: 0;
  text-align: center;
  font-size: var(--step--1);
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* --------------------------------------------------------------------------
   8. Hero — one photograph, edge to edge, with a torn bottom edge
   -------------------------------------------------------------------------- */
.hero {
  position: relative;
  /* No top offset: the header above is sticky and still occupies its space in
     the flow, so the photograph butts straight up against it. */
  background: var(--ivory-sink);
  line-height: 0;                 /* kill the inline-image descender gap */
}
.hero__img {
  display: block;
  width: 100%;
  /* The photograph is shown WHOLE on desktop: its own 16:9 with height derived
     from the width, and deliberately no max-height. A cap is what forced `cover`
     to throw frame away — first the heads, then the foreground. Nothing is
     cropped here, so there is no object-position to argue about. */
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

/* Phones are the one place a 16:9 band is too short to have any presence
   (219px tall at 390px wide), so there we do crop — from the bottom, keeping
   the faces and as much of the lower frame as the height allows. */
@media (max-width: 700px) {
  .hero__img {
    aspect-ratio: auto;
    height: clamp(300px, 58svh, 560px);
    object-position: center 24%;
  }
}

/* Torn edge. Filled with --paper, the same cream as the header and the intro
   section, so all three read as one sheet. Sits 1px low to kill sub-pixel
   seams, and the intro's arch (z-index 2) deliberately rides over it. */
.hero__tear {
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  /* Tall enough for the displacement to have somewhere to go — a short band
     flattens the rip back into a line no matter how high the noise scale. */
  height: clamp(58px, 8vw, 150px);
  pointer-events: none;
}
.hero__tear svg { display: block; width: 100%; height: 100%; overflow: visible; }
.hero__tear path { fill: var(--paper); }

/* --------------------------------------------------------------------------
   8b. Intro — the section that overlaps the hero
   -------------------------------------------------------------------------- */
/* Exactly the header's cream, so the torn edge above reads as one sheet of paper
   continuing down the page. The site-wide .grain overlay supplies the stock
   texture, which is why there is no gradient stack here. */
.intro {
  padding-top: clamp(1rem, 2vw, 2rem);      /* the arch supplies the top space */
  /* Far less than the global --section (up to 176px). The photo strip already
     extends well below the copy, so a full bottom rhythm on top of that left a
     large empty band before the next section. */
  padding-bottom: clamp(1.5rem, 3vw, 3rem);
  background: var(--paper);
  /* Above the following section, so the photo strip that hangs past this
     section's bottom edge is not painted over by it (later siblings win at the
     same z-index, and .section is already position: relative). */
  z-index: 2;
  /* No overflow clipping here: the arch deliberately hangs above this section's
     top edge, and `overflow: hidden` would cut the overlap clean off. */
}
/* The drawn floral texture (see index.html) sits in the lower half of the sheet.
   It replaced a plain radial wash, which gave a warm tint but no texture. */
.intro__texture {
  position: absolute;
  inset: auto 0 0 0;
  height: 66%;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}
.intro__texture svg { display: block; width: 100%; height: 100%; }

/* Only the content column is lifted above the texture — a blanket
   `.intro > *` would raise the texture itself out of the background. */
.intro > .shell { position: relative; z-index: 1; }

.intro__top {
  display: grid;
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: center;
  justify-items: center;
}

/* The arch straddles the hero boundary: pulled up by its own negative margin,
   which is why .intro cannot use the global --section padding at the top. */
.intro__arch {
  position: relative;
  z-index: 2;
  margin: 0;
  /* Rides high up into the photograph — the overlap is the whole point of this
     element, so the negative pull is large and scales with the viewport. */
  margin-top: clamp(-230px, -17vw, -90px);
  width: clamp(150px, 21vw, 250px);
  aspect-ratio: 3 / 4;
  /* No ring, no fill, and deliberately NO shadow. The frame is one hairline
     traced just outside the oval (::after below). overflow must stay visible or
     that trace gets clipped away — the video rounds itself off instead. */
}
.intro__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 38%;
  border-radius: 999px;          /* clips the footage into the oval */
  background: var(--ivory-sink); /* placeholder while the clip loads */
  opacity: 0;
  transition: opacity 0.9s var(--ease-soft);
}
.intro__video.is-ready { opacity: 1; }

/* The hairline. 1px, pale, and offset by unequal amounts so it reads as a line
   traced alongside the oval rather than a border welded to it. Most of it falls
   over the photograph above, which is where it shows most clearly. */
.intro__arch::after {
  content: "";
  position: absolute;
  inset: -14px -7px -5px -13px;
  border: 1px solid rgba(220, 199, 155, 0.72);
  border-radius: 999px;
  pointer-events: none;
}

.intro__head { text-align: center; display: grid; gap: clamp(0.7rem, 1.6vw, 1.1rem); justify-items: center; }
.intro__title { display: grid; gap: 0.1em; justify-items: center; }
.intro__script {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
  font-size: clamp(2.2rem, 6.4vw, 4.6rem);
  line-height: 1.02;
  letter-spacing: -0.01em;
  color: var(--gold-deep);
}
.intro__date {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.4rem, 3.4vw, 2.6rem);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink);
}

.intro__row {
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
  /* Deliberately small: the heading and its paragraph should sit close under the
     seal, not float in the middle of the section. */
  margin-top: clamp(0.5rem, 1.5vw, 1.25rem);
}
.intro__copy { display: grid; gap: clamp(1rem, 2.2vw, 1.5rem); justify-items: start; max-width: 46ch; }
.intro__h {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 400;
  line-height: 1.16;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  color: var(--gold-deep);
}
.intro__copy p { color: var(--ink-soft); max-width: 44ch; }
.intro__cta { justify-self: start; }

/* Circular seal — the reference's EST. badge. The text is set on an SVG
   textPath so it curves around the ring; a stack of straight lines inside a
   round border was the previous version and read as a label, not a stamp. */
.seal {
  width: clamp(104px, 11vw, 138px);
  margin: clamp(1.1rem, 2.6vw, 2rem) auto 0;    /* centred in the section */
  color: var(--gold-deep);
}
.seal svg { display: block; width: 100%; height: auto; overflow: visible; }
.seal__text {
  fill: currentColor;
  font-family: var(--font-body);
  font-size: 8.2px;                              /* in viewBox units */
  font-weight: 600;
  letter-spacing: 1.5px;
  text-transform: uppercase;
}
.seal__star { fill: var(--gold); font-size: 19px; }

/* --- Photo strip -------------------------------------------------------- */
/* A single composed image (white frames included), sitting off to one side and
   tilted a couple of degrees so it reads as laid down by hand.

   The tilt is a plain rotate(), NOT a perspective rotateY. A perspective turn
   makes a trapezoid — one edge genuinely shorter than the other — which warps
   the photographs. In the reference every frame is an undistorted rectangle;
   what makes that strip look off-kilter is the horizontal stagger between the
   three frames, not any distortion of them. */
.stack {
  margin: 0;
  width: min(228px, 70vw);
  justify-self: end;
  /* Declared as --rv-base, not `transform`: this element also carries
     data-reveal, and the reveal rules compose with --rv-base while a plain
     transform here would just be overwritten by them. */
  --rv-base: rotate(2.2deg);
  transform-origin: center;
}
/* No shadow — the white frames are already part of the image, and a cast shadow
   under them read as a sticker sitting on the page. */
.stack img {
  display: block;
  width: 100%;
  height: auto;
}
/* app.js hides this figure when its path is cleared in /admin, and the mobile
   block below sets `display: none` on .stack — an author display beats the UA's
   [hidden] rule, so make the guard explicit. */
.stack[hidden] { display: none; }
/* The tilt is decorative; flatten it when motion is unwelcome. */
@media (prefers-reduced-motion: reduce) {
  .stack { transform: none; }
}

/* --- Mobile carousel ------------------------------------------------------
   The same three photographs as the desktop strip, swipeable. Native
   scroll-snap rather than a slider library: momentum, keyboard scrolling and
   assistive tech all keep working, and it costs no JavaScript to operate —
   app.js only lights the matching dot. */
.slides { display: none; }              /* desktop uses .stack instead */

.slides__track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 78%;
  gap: clamp(0.6rem, 3vw, 1rem);
  overflow-x: auto;
  overscroll-behavior-x: contain;       /* swiping the strip must not page-back */
  scroll-snap-type: x mandatory;
  /* Inset so the first and last slide can still centre. */
  padding-inline: 11%;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.slides__track::-webkit-scrollbar { display: none; }
.slides__track:focus-visible { outline: 1.5px solid var(--gold-deep); outline-offset: 4px; }

.slides__item {
  margin: 0;
  scroll-snap-align: center;
  padding: 6px;
  background: var(--paper);
  border: 1px solid rgba(191, 160, 106, 0.28);
}
.slides__item img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
}
.slides__item[hidden] { display: none; }

.slides__dots {
  display: flex;
  justify-content: center;
  gap: 0.45rem;
  margin-top: clamp(0.8rem, 3vw, 1.1rem);
}
.slides__dots i {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: rgba(150, 121, 74, 0.3);
  transition: background 0.3s var(--ease-soft), transform 0.3s var(--ease-out);
}
.slides__dots i.is-on { background: var(--gold-deep); transform: scale(1.35); }

@media (min-width: 861px) {
  /* Arch left, heading filling the rest — the reference's arrangement. */
  .intro__top {
    grid-template-columns: auto minmax(0, 1fr);
    justify-items: start;
    gap: clamp(2rem, 5vw, 4.5rem);
  }
  /* Inset from the shell's left edge rather than sitting on it, so it lands
     inboard the way the reference's oval does. align-self: start pins it to the
     top of the row, so the overlap is exactly the negative margin — with the
     row's default centring it shifted around as the heading's height changed. */
  .intro__arch {
    margin-left: clamp(1rem, 7vw, 7rem);
    align-self: start;
  }
  .intro__head { justify-self: center; }
  .intro__row {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
  }
  /* Pulled up so its top sits alongside the seal, and pulled up again at the
     bottom so it does not reserve its full height in the flow. It still ends
     lower than the copy beside it — that hang is the look — but the section now
     closes just under it instead of adding a full rhythm of empty space. */
  .stack {
    margin-top: clamp(-200px, -11vw, -60px);
    margin-bottom: clamp(-170px, -9vw, -50px);
    /* Right margin is what shifts it inboard: it is centred in its grid column,
       so growing this pulls the strip left, away from the shell's edge. */
    margin-right: clamp(1.5rem, 6vw, 7rem);
  }
}
@media (max-width: 860px) {
  .intro__row { justify-items: center; text-align: center; }
  .intro__copy { justify-items: center; }
  .intro__cta { justify-self: center; }

  /* The arch is the hinge between the photograph and this section, so it needs
     to read as overlapping on a phone too. The base rule's -17vw collapses to
     its -90px floor at these widths (only ~45% of the arch), so both the size
     and the pull are restated in viewport units here: ~54% of the arch ends up
     over the photo, matching the desktop proportion. */
  .intro__arch {
    width: min(280px, 74vw);
    /* The pull grows with the arch: it is a share of the element's own height,
       so a bigger arch that kept the old margin would sit lower and lose the
       overlap with the photograph that the whole composition depends on. */
    margin-top: clamp(-215px, -52vw, -140px);
  }

  /* Swap the tall composed strip for the swipeable carousel. The carousel bleeds
     its slides past the shell's gutters, so it cancels exactly the padding the
     shell applied — NOT `width: 100vw; margin-inline: calc(50% - 50vw)`.
     `vw` counts the classic scrollbar that `clientWidth` does not, so wherever a
     scrollbar is reserved (a desktop browser at this width, an embedded frame,
     some Android configurations) that version came out ~8px too wide, pushed the
     document past the viewport, and every centred element in the section landed
     off-centre with its right edge clipped. Negative gutters have no such gap. */
  .stack { display: none; }
  .slides {
    display: block;
    width: auto;
    margin-inline: calc(var(--gutter) * -1);
    margin-top: clamp(2rem, 7vw, 3rem);
  }
}

/* --- Intro typography on phones --------------------------------------------
   The desktop scale collapses badly here. At 390px the names came out 35px and
   the heading 29.5px — a ratio of 1.19, so nothing led and the block read as
   three competing shouts. The names are the emotional centre, so they take the
   space and everything else steps down behind them: the ratio is 2.1 now.

   Measures are set in `ch` rather than left full-bleed, because a 47-character
   all-caps heading running edge to edge is what made it feel like a notice
   board instead of an invitation. */
@media (max-width: 700px) {
  .intro__head { gap: 0.5rem; }

  /* Up, and tightened — display type at this size wants negative leading. */
  .intro__script {
    font-size: clamp(2.35rem, 10.5vw, 3.2rem);
    line-height: 0.94;
    letter-spacing: -0.015em;
  }
  /* Down and widely tracked, so it reads as a caption to the names rather than
     as a second headline. */
  .intro__date {
    font-size: clamp(0.92rem, 3.9vw, 1.15rem);
    letter-spacing: 0.22em;
    text-indent: 0.22em;          /* re-centres against the trailing track */
  }

  .seal { margin-top: clamp(1.5rem, 5.5vw, 2.25rem); }
  .intro__row { margin-top: clamp(1.4rem, 5vw, 2rem); }

  .intro__h {
    font-size: clamp(1.12rem, 5vw, 1.45rem);
    line-height: 1.38;
    letter-spacing: 0.075em;      /* small caps need air to stay legible */
    max-width: 17ch;
    margin-inline: auto;
    text-wrap: balance;           /* even line lengths instead of a short widow */
  }

  .intro__copy { gap: 1.1rem; }
  .intro__copy p {
    font-size: 0.9rem;
    line-height: 1.8;
    max-width: 34ch;              /* ~9 words a line, the comfortable range */
    text-wrap: pretty;
  }
  .intro__cta { margin-top: 0.3rem; }
}

/* --- Invitation typography on phones ---------------------------------------
   Same failure as the intro section: at 390px the title was 38.5px against a
   29.5px quote — 1.31, too close for either to lead, and both ran the full width
   of the screen. The title keeps the lead, the quote steps back to a pull-quote
   rather than a second headline, and every text block gets a measure.

   Scoped by #invite so the other sections' shared .section__title and .prose
   are untouched. */
@media (max-width: 700px) {
  #invite .invite__body { gap: 1.3rem; }

  #invite .section__title {
    font-size: clamp(1.85rem, 8.4vw, 2.5rem);
    line-height: 1.06;
    text-wrap: balance;
  }

  /* 24ch keeps it to four shapely lines instead of four ragged full-width ones. */
  #invite .invite__quote {
    font-size: clamp(1.15rem, 5.2vw, 1.5rem);
    line-height: 1.5;
    max-width: 24ch;
    text-wrap: balance;
  }

  #invite .prose { gap: 1rem; }
  #invite .prose p {
    font-size: 0.9rem;
    line-height: 1.85;
    /* 38ch, not narrower: 36ch tipped the first paragraph from four lines to
       five, while the uncapped width was running to 48 characters. */
    max-width: 38ch;
    margin-inline: auto;
    text-wrap: pretty;
  }

  #invite .invite__sign {
    font-size: 0.64rem;
    letter-spacing: 0.26em;
    text-indent: 0.26em;          /* re-centres against the trailing track */
  }
  #invite .ornament svg { width: 26px; }
  #invite .rule { width: min(140px, 40vw); }
}
/* --------------------------------------------------------------------------
   9. Marquee ribbon
   -------------------------------------------------------------------------- */
.ribbon {
  border-block: 1px solid var(--line-soft);
  background: var(--ivory-lift);
  padding-block: clamp(0.9rem, 2vw, 1.4rem);
  overflow: hidden;
  --mask: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  -webkit-mask-image: var(--mask);
  mask-image: var(--mask);
}
.ribbon__track {
  display: flex;
  width: max-content;
  gap: 0;
}
@media (prefers-reduced-motion: no-preference) {
  .ribbon__track { animation: marquee 34s linear infinite; }
}
.ribbon:hover .ribbon__track { animation-play-state: paused; }
@keyframes marquee {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
.ribbon__set { display: flex; align-items: center; }
.ribbon__item {
  display: inline-flex;
  align-items: center;
  gap: clamp(1.4rem, 4vw, 3rem);
  padding-inline: clamp(0.7rem, 2vw, 1.5rem);
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 300;
  letter-spacing: 0.1em;
  white-space: nowrap;
  color: var(--ink-soft);
  font-variant-numeric: lining-nums;
  font-feature-settings: "lnum" 1;
}
.ribbon__item::after {
  content: "◆";
  font-size: 0.34em;
  color: var(--gold);
  transform: translateY(-0.2em);
}

/* --------------------------------------------------------------------------
   10. Invitation
   -------------------------------------------------------------------------- */
.invite__grid {
  display: grid;
  gap: clamp(2.5rem, 6vw, 5rem);
  align-items: center;
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  text-align: center;
}
.invite__body { display: grid; gap: clamp(1.5rem, 3vw, 2.25rem); justify-items: center; }

/* Optional photo, uploaded from /admin. A fixed 4:5 crop means any picture the
   couple picks lands in the same shape and the layout never jumps. */
.invite__photo {
  margin: 0;
  width: min(320px, 72vw);
  padding: 0.5rem;
  background: var(--ivory-lift);
  border: 1px solid var(--line-gold);
}
.invite__photo[hidden] { display: none; }
.invite__photo img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
}
.invite__quote {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 300;
  font-style: italic;
  line-height: 1.32;
  max-width: 26ch;
  color: var(--ink);
}
.invite__sign {
  font-size: var(--step--1);
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* --------------------------------------------------------------------------
   11. Countdown
   -------------------------------------------------------------------------- */
.count {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(0.5rem, 2vw, 1.5rem);
  width: min(760px, 100%);
  margin-inline: auto;
}
.count__cell {
  position: relative;
  display: grid;
  justify-items: center;
  gap: clamp(0.7rem, 1.4vw, 1rem);
  padding: clamp(1.1rem, 3vw, 2rem) clamp(0.35rem, 1.5vw, 1rem);
  background: var(--paper);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  box-shadow: var(--shadow-soft);
  transition: transform var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out);
}
/* Lift only. A cast shadow on the clay ground just muddies it. */
.count__cell:hover { transform: translateY(-4px); }
/* Countdown.init() hides this block when the date will not parse, and .count
   declares `display: grid` — which beats the UA's [hidden] rule, so without this
   guard a broken date would leave four empty frames on the page. */
.count[hidden] { display: none; }
.count__num {
  font-family: var(--font-display);
  font-size: var(--step-4);
  font-weight: 300;
  /* Old-style figures, matching the schedule times — several of them descend
     below the baseline, so the line box needs more than 1 or the tails crowd
     the label underneath. */
  line-height: 1.12;
  /* onum for the look; tnum kept so the digits stay one width and the seconds
     do not jiggle sideways on every tick. */
  font-variant-numeric: oldstyle-nums tabular-nums;
  font-feature-settings: "onum" 1, "tnum" 1;
}
.count__lab {
  font-size: 0.64rem;
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--ink-mute);
  text-align: center;
}
/* Countdown cells on either dark ground. On clay they need a touch more tint and
   a slightly stronger edge, or the frames disappear into the floral. */
.section--ink .count__cell {
  background: rgba(246, 241, 232, 0.05);
  border-color: rgba(220, 199, 155, 0.2);
  box-shadow: none;
}
.section--clay .count__cell {
  background: rgba(58, 26, 20, 0.16);
  border-color: rgba(246, 241, 232, 0.26);
  box-shadow: none;
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}
.section--ink .count__num,
.section--clay .count__num { color: var(--ivory); }
.section--ink .count__lab,
.section--clay .count__lab { color: var(--gold-lift); }
.section--clay .count__done { color: var(--ivory); }
.count__done {
  grid-column: 1 / -1;
  text-align: center;
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-style: italic;
}

/* --------------------------------------------------------------------------
/* --------------------------------------------------------------------------
   12. Schedule — two cards, ceremony and reception
   -------------------------------------------------------------------------- */
.plan {
  position: relative;
  display: grid;
  gap: clamp(1.25rem, 3vw, 2rem);
  max-width: 980px;
  margin-inline: auto;
  list-style: none;
}
.plan__card {
  display: grid;
  justify-items: start;
  gap: 0;
  padding: clamp(1.75rem, 3.6vw, 2.75rem);
  background: var(--ivory-lift, rgba(255, 252, 246, 0.6));
  border: 1px solid rgba(220, 199, 155, 0.34);
  border-radius: 2px;
}
.plan__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  width: 100%;
}
.plan__label { margin: 0; }
.plan__icon {
  width: clamp(22px, 2.4vw, 26px);
  height: auto;
  flex: none;
  color: var(--gold);
}
.plan__time {
  font-family: var(--font-display);
  font-size: clamp(2.9rem, 6vw, 4.6rem);
  font-weight: 300;
  line-height: 1;
  letter-spacing: 0.01em;
  color: var(--ink);
  margin-top: clamp(1.5rem, 3.4vw, 2.6rem);
  /* Old-style figures here — the lining/tabular set the rest of the site uses
     would flatten the 16:30 that makes this card read as an invitation. */
  font-variant-numeric: oldstyle-nums proportional-nums;
  font-feature-settings: "onum" 1, "pnum" 1;
}
.plan__rule {
  display: block;
  width: 46px;
  height: 1px;
  background: var(--gold);
  margin-top: clamp(0.9rem, 2vw, 1.35rem);
}
.plan__venue {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 400;
  font-style: normal;          /* the section's other h3s are italic */
  line-height: 1.15;
  margin-top: clamp(1.1rem, 2.4vw, 1.7rem);
}
.plan__addr {
  display: flex;
  align-items: center;
  gap: 0.5em;
  margin-top: 0.55rem;
  font-size: var(--step--1);
  color: var(--ink-mute);
  letter-spacing: 0.03em;
}
.plan__addr svg { width: 1.05em; height: 1.05em; flex: none; color: var(--gold-deep); }
.plan__map { margin-top: clamp(1.4rem, 3vw, 2.1rem); }

/* Side by side, with the connector node centred in the gap. */
@media (min-width: 861px) {
  .plan { grid-template-columns: 1fr 1fr; }
  .plan__node {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 27px;
    height: 27px;
    border-radius: 50%;
    border: 1px solid var(--line-gold);
    background: var(--ivory);
    display: grid;
    place-items: center;
    z-index: 1;
  }
  .plan__node::after {
    content: "";
    width: 7px; height: 7px;
    border-radius: 50%;
    background: var(--gold);
  }
  .section--tint .plan__node { background: var(--ivory-sink); }
}
@media (max-width: 860px) {
  .plan__node { display: none; }
}

/* --------------------------------------------------------------------------
   14. Dress code
   -------------------------------------------------------------------------- */
.dress {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: clamp(1.5rem, 3.5vw, 3rem);
  max-width: 940px;
  margin-inline: auto;
}
.dress__card {
  display: grid;
  justify-items: center;
  gap: 1rem;
  padding: clamp(2rem, 4vw, 3rem) clamp(1.25rem, 3vw, 2rem);
  text-align: center;
  border: 1px solid rgba(220, 199, 155, 0.24);
  border-radius: var(--radius);
  background: rgba(246, 241, 232, 0.04);
  transition: transform var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out);
}
.dress__card:hover { transform: translateY(-6px); border-color: rgba(220, 199, 155, 0.5); }
/* Gold medallions: a hairline ring with a fine glyph inside, in the same
   language as the circular seal in the intro section. All three are the same
   square SVG, so they need no per-icon sizing — that is what keeps the three
   card labels starting at the same height. */
.dress__ico {
  display: block;
  width: clamp(76px, 9.5vw, 108px);
  height: auto;
  margin-bottom: 0.45rem;
  overflow: visible;
  transition: transform 0.6s var(--ease-out);
}
.dress__ico-ring {
  fill: none;
  stroke: rgba(220, 199, 155, 0.38);
  stroke-width: 1;
  transition: stroke 0.4s var(--ease-soft);
}
.dress__ico-mark {
  fill: var(--gold-lift);
  transition: fill 0.4s var(--ease-soft);
}
/* "No white": the medallion doubles as the prohibition circle, so this is the
   bar across it — a second ring would just read as clutter. */
.dress__ico-slash {
  stroke: var(--gold-lift);
  stroke-width: 2;
  stroke-linecap: round;
  transition: stroke 0.4s var(--ease-soft);
}
.dress__card:hover .dress__ico { transform: translateY(-3px) scale(1.04); }
.dress__card:hover .dress__ico-ring { stroke: rgba(220, 199, 155, 0.7); }
.dress__card:hover .dress__ico-mark,
.dress__card:hover .dress__ico-slash { fill: var(--gold); stroke: var(--gold); }
.dress__who {
  font-size: var(--step--1);
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--gold-lift);
}
.dress__what { font-family: var(--font-display); font-size: var(--step-2); font-weight: 300; }
.dress__hint { font-size: var(--step--1); color: rgba(246, 241, 232, 0.62); max-width: 26ch; }

.palette {
  display: flex;
  justify-content: center;
  gap: clamp(0.6rem, 2vw, 1.1rem);
  margin-top: clamp(2.5rem, 5vw, 4rem);
  flex-wrap: wrap;
}
.swatch {
  display: grid;
  justify-items: center;
  gap: 0.6rem;
  font-size: 0.6rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: rgba(246, 241, 232, 0.55);
}
.swatch em { font-style: normal; }
.swatch i {
  width: clamp(38px, 8vw, 52px);
  aspect-ratio: 1;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgba(246, 241, 232, 0.22);
}

/* If JS never runs, nothing may stay invisible. */
.no-js [data-reveal],
.no-js .split__w {
  opacity: 1 !important;
  transform: none !important;
  clip-path: none !important;
}
.no-js .rule { transform: none !important; }
.no-js .veil { display: none; }

/* --------------------------------------------------------------------------
   15. Details / info cards
   -------------------------------------------------------------------------- */
.info {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(clamp(220px, 30vw, 280px), 1fr));
  gap: clamp(1.25rem, 3vw, 2.25rem);
}
/* No drop shadow. Definition comes from a gold-tinted hairline plus a 1px inset
   highlight along the top edge, which reads as pressed card stock rather than a
   panel floating over the page.

   The stock itself is four layers, topmost first:
     1. the paper grain, multiplied — the fibre
     2. a soft top-left sheen, as if light falls across it
     3. a faint warm settling toward the bottom-right
     4. a barely-there diagonal weave, giving it a linen direction
   All of it is generated, so the cards cost no extra request. */
.info__card {
  position: relative;
  display: grid;
  gap: 0.7rem;
  padding: clamp(1.6rem, 3vw, 2.25rem);
  border: 1px solid rgba(191, 160, 106, 0.26);
  border-radius: var(--radius);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75);
  background-color: var(--paper);
  background-image:
    var(--grain-url),
    radial-gradient(120% 100% at 8% 0%, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0) 58%),
    radial-gradient(90% 90% at 100% 100%, rgba(214, 195, 168, 0.20), rgba(214, 195, 168, 0) 62%),
    repeating-linear-gradient(58deg,
      rgba(160, 132, 96, 0.035) 0 1px,
      rgba(160, 132, 96, 0) 1px 7px);
  background-size: 220px 220px, cover, cover, auto;
  /* soft-light, not multiply, for the grain: the page already has a global grain
     overlay, and multiplying a second layer on top of it turns the cards grubby
     rather than textured. */
  background-blend-mode: soft-light, normal, normal, normal;
  transition: transform var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out);
}
/* Hover lifts and warms the hairline instead of casting a shadow. */
.info__card:hover {
  transform: translateY(-5px);
  border-color: rgba(191, 160, 106, 0.5);
}
.info__k {
  font-size: 0.64rem;
  font-weight: 600;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--gold-deep);
}
.info__v { font-family: var(--font-display); font-size: var(--step-2); font-weight: 400; line-height: 1.25; }
.info__d { font-size: var(--step--1); color: var(--ink-mute); }

/* --------------------------------------------------------------------------
   16. RSVP
   -------------------------------------------------------------------------- */
/* The card carries its own generous padding, so the section around it does not
   need the full --section rhythm on top. */
#rsvp { padding-block: clamp(3.5rem, 7.5vw, 7rem); }

/* Dusty rose field instead of the ivory tint, so the cream card reads as a
   card. The gradient keeps it from looking like a flat colour block. */
.section--rose {
  background: linear-gradient(168deg, var(--rose) 0%, var(--rose-deep) 100%);
}

/* The whole RSVP block sits on a raised card. Against the tinted section this
   bounds it, so the eye reads one panel rather than an endlessly tall column. */
.rsvp__panel {
  max-width: 860px;
  margin-inline: auto;
  padding: clamp(2rem, 5vw, 4rem) clamp(1.4rem, 4vw, 3.5rem);
  background: var(--ivory-lift);
  /* No cast shadow. Against the rose the card is already clearly lighter, so a
     warm hairline plus a 1px inset highlight is enough to seat it — the same
     treatment as the details cards. */
  border: 1px solid rgba(120, 76, 62, 0.16);
  border-radius: 3px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
/* The heading sits on the rose, outside the card. Rose is a mid-tone, so the
   ink-mute eyebrow (1.8:1) and ink-soft lede (3.7:1) both failed against it —
   they drop to the near-black wine instead, which clears AA at both ends of the
   section's gradient. */
#rsvp .section__head {
  gap: clamp(0.7rem, 1.6vw, 1.1rem);
  margin-bottom: clamp(1.6rem, 3.5vw, 2.6rem);
}
#rsvp .section__title { font-size: var(--step-3); color: var(--wine-deep); }
#rsvp .eyebrow { color: var(--wine-deep); }
/* Fully opaque, not 88%: the section is a gradient, and at its darker end a
   translucent wine only reached 3.95:1. Solid gets 4.81:1 there. */
#rsvp .lede { max-width: 42ch; color: var(--wine-deep); }

.rsvp {
  max-width: 720px;
  margin-inline: auto;
}

.form { display: grid; gap: clamp(1.1rem, 2.4vw, 1.6rem); }
.form__row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: clamp(1.1rem, 2.4vw, 1.6rem);
}
.field { display: grid; gap: 0.45rem; position: relative; }
.field > label {
  font-size: 0.64rem;
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.field input,
.field select,
.field textarea {
  width: 100%;
  padding: 0.9em 0.2em;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  font-size: var(--step-0);
  color: var(--ink);
  font-family: inherit;
  transition: border-color var(--dur-fast) var(--ease-soft),
              background-color var(--dur-fast) var(--ease-soft);
  appearance: none;
}
.field textarea { resize: vertical; min-height: 5.5em; line-height: 1.7; }

/* Dropdowns get a boxed treatment rather than a bare underline: a select is a
   button that opens a menu, and it should look pressable. */
.field select {
  padding: 0.85em 2.4em 0.85em 0.95em;
  background-color: var(--paper);
  border: 1px solid rgba(150, 121, 74, 0.28);
  border-bottom-color: rgba(150, 121, 74, 0.28);
  border-radius: 2px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.4l5 5 5-5' fill='none' stroke='%2396794A' stroke-width='1.3' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.95em center;
  background-size: 11px;
  cursor: pointer;
}
.field select:hover { border-color: rgba(150, 121, 74, 0.5); }
/* Once app.js has drawn the themed listbox, the native control is hidden. It
   stays in the DOM and still carries its value on submit — and it is what shows
   if the script never runs, so the form degrades to a working native select. */
.field select.is-enhanced { display: none; }

/* --- Themed dropdown ------------------------------------------------------
   Replaces the operating system's popup, which cannot be styled and rendered as
   a grey panel breaking out of the page. Built from a button plus a listbox
   (see Selects in app.js). */
.sel { position: relative; }
.sel.is-open { z-index: 30; }        /* over the fields below it */

.sel__btn {
  width: 100%;
  min-height: 48px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75em;
  padding: 0.8em 1em;
  background: var(--paper);
  border: 1px solid rgba(150, 121, 74, 0.28);
  border-radius: 2px;
  font-family: inherit;
  font-size: var(--step-0);
  color: var(--ink);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-soft),
              box-shadow var(--dur-fast) var(--ease-soft);
}
.sel__btn:hover { border-color: rgba(150, 121, 74, 0.5); }
.sel__btn:focus-visible,
.sel.is-open .sel__btn {
  outline: none;
  border-color: var(--gold-deep);
  box-shadow: 0 0 0 3px rgba(191, 160, 106, 0.18);
}
/* The selected label. min-width:0 lets it shrink inside the flex row so a long
   option ellipsises instead of pushing the chevron off the control. */
.sel__value {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sel__chev {
  width: 11px;
  height: 8px;
  flex: none;
  color: var(--gold-deep);
  transition: transform 0.3s var(--ease-out);
}
.sel.is-open .sel__chev { transform: rotate(180deg); }

.sel__list {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;                          /* matches the field — never wider */
  margin: 0;
  padding: 0.3rem;
  list-style: none;
  background: var(--paper);
  border: 1px solid rgba(150, 121, 74, 0.34);
  border-radius: 2px;
  /* No cast shadow, in keeping with the rest of the page — the hairline and the
     paper fill are enough to lift it. */
  max-height: min(15rem, 42vh);      /* long lists scroll rather than overflow */
  overflow-y: auto;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4px);
  transition: opacity 0.22s var(--ease-out),
              transform 0.22s var(--ease-out),
              visibility 0s linear 0.22s;
}
.sel.is-open .sel__list {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition-delay: 0s;
}

.sel__opt {
  padding: 0.68em 0.8em;
  border-radius: 2px;
  font-size: var(--step-0);
  color: var(--ink-soft);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-soft), color var(--dur-fast) var(--ease-soft);
}
.sel__opt:hover,
.sel__opt.is-active { background: rgba(191, 160, 106, 0.16); color: var(--ink); }
.sel__opt[aria-selected="true"] {
  color: var(--ink);
  font-weight: 500;
  background: rgba(191, 160, 106, 0.1);
}
@media (prefers-reduced-motion: reduce) {
  .sel__list { transition-duration: 0.01ms; transform: none; }
  .sel__chev { transition-duration: 0.01ms; }
}

.field input:focus,
.field textarea:focus {
  outline: none;
  border-color: var(--gold);
}
.field select:focus-visible {
  outline: none;
  border-color: var(--gold-deep);
  box-shadow: 0 0 0 3px rgba(191, 160, 106, 0.18);
}
.field input::placeholder, .field textarea::placeholder { color: var(--ink-faint); }
.field__err {
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  color: #A33B3B;
  min-height: 1em;
  opacity: 0;
  transform: translateY(-3px);
  transition: opacity var(--dur-fast), transform var(--dur-fast);
}
.field.has-error input,
.field.has-error select { border-color: #A33B3B; }

/* The reveal must not be scoped to .field. The yes/no group is a
   <fieldset class="choice">, so `.field.has-error .field__err` never matched
   it: pressing Send without choosing set the message, scrolled the page up to
   the group, and displayed nothing at all — the form looked like it had simply
   ignored the click. */
.field.has-error .field__err,
.choice.has-error .field__err { opacity: 1; transform: none; }

/* And say it visually, not only in words. */
.choice.has-error .choice__opt span { border-color: #A33B3B; }

/* Yes / No choice */
.choice { display: grid; gap: 0.6rem; }
.choice__set {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(0.6rem, 1.6vw, 1rem);
}
/* Side by side, each option gets only ~110px of usable width on a phone, and the
   longer subheading ("WE CANNOT MAKE IT" / "НЕ МОЖЕМЕ ДА ДОЈДЕМЕ") needs ~125px.
   Stack them rather than let it wrap mid-phrase. */
@media (max-width: 560px) {
  .choice__set { grid-template-columns: 1fr; }
  .choice__sub { letter-spacing: 0.16em; text-indent: 0.16em; }
}

/* --- RSVP form on phones --------------------------------------------------- */
@media (max-width: 700px) {
  /* 16px is a hard minimum, not a style choice: iOS Safari zooms the whole page
     when you focus a control smaller than that, which throws the layout sideways
     mid-form. --step-0 computes to 14.5px here, so it has to be overridden. */
  .field input,
  .field select,
  .field textarea { font-size: 16px; }

  /* Comfortable thumb target — the padding alone lands a couple of pixels short. */
  .field select { min-height: 48px; }

  /* The themed dropdown, sized for thumbs. */
  .sel__btn { font-size: 16px; }
  .sel__opt { font-size: 16px; padding: 0.8em 0.8em; }
  /* Shorter than the desktop cap: on a phone the panel must not swallow the
     screen, and 38vh still shows four options at a time. */
  .sel__list { max-height: min(13rem, 38vh); }

  /* The card is the form now, so it needs less padding than when it also held
     the heading. */
  .rsvp__panel { padding: clamp(1.5rem, 5.5vw, 2.25rem) clamp(1.1rem, 4.5vw, 1.75rem); }

  #rsvp .section__title {
    font-size: clamp(1.85rem, 8.4vw, 2.5rem);
    line-height: 1.06;
    text-wrap: balance;
  }
  #rsvp .lede {
    font-size: clamp(1rem, 4.4vw, 1.2rem);
    line-height: 1.65;
    max-width: 30ch;
    text-wrap: pretty;
  }
}
.choice__opt { position: relative; }
.choice__opt input {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
  width: 100%; height: 100%;
  margin: 0;
}
.choice__opt span {
  display: grid;
  place-items: center;
  gap: 0.4rem;
  padding: clamp(1rem, 2.4vw, 1.5rem) 1rem;
  text-align: center;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-family: var(--font-display);
  font-size: var(--step-1);
  transition: border-color var(--dur-fast) var(--ease-soft),
              background var(--dur-fast) var(--ease-soft),
              transform var(--dur-fast) var(--ease-out);
  pointer-events: none;
}
/* Two lines per option: the answer in display italic, then a plain-language
   subheading in small letterspaced caps. */
.choice__lead {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  font-size: var(--step-2);
  line-height: 1.05;
}
.choice__sub {
  font-family: var(--font-body);
  font-style: normal;
  font-size: 0.6rem;
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-mute);
  /* Tracking is added after the last letter too, which throws a centred line
     off; half of it back re-centres it. */
  text-indent: 0.24em;
  transition: color var(--dur-fast) var(--ease-soft);
}
.choice__opt input:checked + span {
  border-color: var(--ink);
  background: var(--ink);
  color: var(--ivory);
}
.choice__opt input:checked + span .choice__sub { color: rgba(246, 241, 232, 0.72); }
.choice__opt input:focus-visible + span { outline: 1.5px solid var(--gold-deep); outline-offset: 3px; }
.choice__opt:hover span { transform: translateY(-2px); border-color: var(--gold); }

/* Conditional block */
.reveal-block {
  display: grid;
  gap: clamp(1.1rem, 2.4vw, 1.6rem);
  overflow: hidden;
  max-height: 0;
  opacity: 0;
  transition: max-height 0.6s var(--ease-inout), opacity 0.4s var(--ease-soft), margin 0.6s var(--ease-inout);
}
.reveal-block.is-open { max-height: 46rem; opacity: 1; }

.form__status {
  min-height: 1.5em;
  font-size: var(--step--1);
  letter-spacing: 0.06em;
}
.form__status.is-ok { color: var(--olive-deep); }
.form__status.is-bad { color: #A33B3B; }

.rsvp__thanks {
  display: grid;
  justify-items: center;
  gap: 1.25rem;
  text-align: center;
  padding: clamp(2.5rem, 6vw, 4rem) 1rem;
}
/* An author `display` beats the UA's `[hidden] { display: none }`, so without
   this the block still rendered while marked hidden — an empty heading, an empty
   note and its ornament, plus up to 8rem of padding, sitting under the form as a
   large blank gap with a stray sparkle in it. */
.rsvp__thanks[hidden] { display: none; }
.rsvp__thanks h3 { font-family: var(--font-display); font-size: var(--step-3); font-weight: 300; }

/* --------------------------------------------------------------------------
   17. Footer
   -------------------------------------------------------------------------- */
/* Garden roses under the wine. Three layers, topmost first:
     1. a wine wash, normal — buys contrast for the type
     2. the photograph, SCREEN
     3. flat --wine-deep

   Screen, not multiply, because the photograph is almost black (it averages
   rgb(8,2,2)): multiplying it over wine would bury the flowers completely.
   Screening it means the black falls away and only the blooms lift out of the
   wine, which is the glow this needs.

   The wash never drops below 0.44 alpha — that is the figure the contrast was
   measured at. On the single brightest pixel of the image the body text lands at
   4.63:1, the wordmark 7.17:1 and the gold 4.87:1, all past WCAG AA. Lighten the
   wash and that stops being true. */
.foot {
  position: relative;
  padding-block: clamp(4rem, 9vw, 7rem) clamp(2rem, 4vw, 3rem);
  /* Same warm brown as the announcement strip at the top of the page. */
  background: var(--clay-band);
  color: rgba(246, 241, 232, 0.88);
  text-align: center;
  overflow: hidden;
  /* Contains the .foot__bg blend to the footer, so `screen` mixes with the clay
     underneath it and nothing further up the page. */
  isolation: isolate;
}

/* The roses. SCREEN, not multiply: the photograph is almost entirely black (it
   averages rgb(8,2,2)), so multiplying would bury the flowers — screening makes
   the black fall away and lifts only the blooms out of the clay. */
.foot__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 46%;
  opacity: 0.85;
  mix-blend-mode: screen;
  pointer-events: none;
}
.foot__bg[hidden] { display: none; }

/* Readability wash. It is a DARKENED clay, not clay itself: screening a photo
   over clay only ever lightens it, and a clay-on-clay wash cannot pull the
   result back down — at every opacity tried, ivory text on plain clay failed AA
   (best was 3.3:1). This darker shade of the same hue keeps the footer reading
   brown while restoring contrast.

   0.55 alpha is the measured figure. Over the brightest pixel of the photograph
   the wordmark lands at 7.33:1, body text 5.56:1 and the gold 4.98:1 — all past
   WCAG AA. Lighten it, or use a brighter photo, and that stops holding. */
.foot::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: rgba(50, 27, 18, 0.55);
  pointer-events: none;
}

.foot .shell { position: relative; z-index: 2; }
.foot__inner { display: grid; justify-items: center; gap: clamp(1.25rem, 3vw, 2rem); }
/* The footer's wordmark is .brand--foot now; see the lockup block. */
.foot__thanks {
  font-size: var(--step--1);
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--gold-lift);
}
.foot__date { font-size: var(--step--1); letter-spacing: 0.3em; text-transform: uppercase; }
.foot__meta {
  margin-top: clamp(2rem, 5vw, 3.5rem);
  padding-top: clamp(1.25rem, 3vw, 2rem);
  border-top: 1px solid rgba(220, 199, 155, 0.16);
  width: 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem 1.5rem;
  font-size: 0.66rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(246, 241, 232, 0.45);
}
.foot__top {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  font-size: 0.66rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--gold-lift);
}
.foot__top svg { width: 12px; transition: transform var(--dur-fast) var(--ease-out); }
.foot__top:hover svg { transform: translateY(-4px); }

/* --- Footer on phones ------------------------------------------------------
   Everything in one centred column. The wide desktop tracking is the main thing
   to pull back: 0.42em on a 10px line eats most of a narrow screen and forces
   ugly breaks. Each tracked line gets a matching text-indent so it stays
   optically centred despite the space added after its last letter. */
@media (max-width: 700px) {
  .foot { padding-block: clamp(3rem, 11vw, 4.5rem) clamp(1.5rem, 6vw, 2.25rem); }

  .foot__inner { gap: clamp(1rem, 4vw, 1.5rem); }
  /* Defensive: nothing in this column may be pulled into a second one. */
  .foot__inner > * { grid-column: 1; max-width: 100%; }

  .foot__thanks { font-size: 0.62rem; letter-spacing: 0.3em; text-indent: 0.3em; }
  .foot__date {
    font-size: 0.68rem;
    letter-spacing: 0.16em;
    text-indent: 0.16em;
    line-height: 1.7;
    max-width: 22ch;
    text-wrap: balance;
  }
  .foot__top { font-size: 0.62rem; letter-spacing: 0.18em; }

  .foot__meta {
    margin-top: clamp(1.5rem, 6vw, 2.25rem);
    padding-top: clamp(1rem, 4vw, 1.5rem);
    flex-direction: column;
    gap: 0.35rem;
    font-size: 0.56rem;
    letter-spacing: 0.12em;
    line-height: 1.7;
  }
  /* The interpunct only makes sense between items on one line. */
  .foot__meta > span:nth-child(2) { display: none; }
}

/* --------------------------------------------------------------------------
   18. Motion primitives — scroll reveal + split text + parallax
   -------------------------------------------------------------------------- */
/* --rv-base carries any transform the element needs to KEEP through the reveal.
   Without it, this rule and `.is-in` below simply overwrite the element's own
   transform — they sit later in the sheet at equal or higher specificity. That
   is what silently flattened the photo strip's rotate(2.2deg): the tilt was
   declared, but the reveal replaced it and then locked it to none. Anything with
   both `data-reveal` and a transform of its own must set --rv-base instead. */
[data-reveal] {
  --rv-base: rotate(0deg);
  opacity: 0;
  transform: translate3d(0, var(--rv-y, 28px), 0) scale(var(--rv-s, 1)) var(--rv-base);
  transition: opacity var(--rv-dur, 1s) var(--ease-out) var(--rv-delay, 0s),
              transform var(--rv-dur, 1s) var(--ease-out) var(--rv-delay, 0s);
  will-change: opacity, transform;
}
[data-reveal="fade"]  { --rv-y: 0px; }
[data-reveal="scale"] { --rv-y: 0px; --rv-s: 1.06; }
[data-reveal="left"]  { --rv-y: 0px; transform: translate3d(-36px, 0, 0); }
[data-reveal="right"] { --rv-y: 0px; transform: translate3d(36px, 0, 0); }
[data-reveal="rise"]  { --rv-y: 56px; }
[data-reveal].is-in {
  opacity: 1;
  transform: translate3d(0, 0, 0) scale(1) var(--rv-base);
}

/* Split text — each word rides up out of its own mask */
.split__m {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  /* room for descenders so g/j/у/ј are never clipped */
  padding-bottom: 0.16em;
  margin-bottom: -0.16em;
}
.split__w {
  display: inline-block;
  transform: translate3d(0, 108%, 0) rotate(1.6deg);
  opacity: 0;
  transition: transform 1.05s var(--ease-out), opacity 0.85s var(--ease-out);
  will-change: transform;
}
.split.is-in .split__w { transform: translate3d(0, 0, 0) rotate(0); opacity: 1; }

[data-parallax] { will-change: transform; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  html { scroll-behavior: auto; }
  [data-reveal], .split__w {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
    transition-delay: 0s !important;
  }
  .rule { transform: none !important; }
  .ribbon__track { animation: none !important; }
}

/* --------------------------------------------------------------------------
   19. Responsive nav
   -------------------------------------------------------------------------- */
@media (max-width: 1000px) {
  .nav__links { display: none; }
  .burger { display: grid; }

  /* Mobile header: language left · wordmark centre · burger right.
     `.lang` and `.burger` live inside `.nav__side`, so they cannot be placed in
     the header grid directly. `display: contents` drops that wrapper's box and
     promotes its children to grid items, which is what makes the three-way split
     possible without duplicating them in the markup.
     The placement must be explicit: `.nav__links--l` is display:none here, so it
     occupies no cell, and auto-placement would slide everything one column left. */
  .nav__side { display: contents; }

  /* grid-ROW 1 is required on all three, not just grid-column. With the row left
     automatic, placement follows DOM order — brand (column 2) lands first and
     moves the cursor past column 1, so .lang, needing a column *behind* the
     cursor, is pushed down to row 2 and .burger follows it. The result is the
     wordmark on one line and the pill and burger on another.

     Every selector here MUST be scoped to .nav. The same `.brand` lockup is
     reused in the preloader and the footer, and both of those are grids too — an
     unscoped `grid-column: 2` pinned the footer wordmark into a second column
     and scattered the rest of the footer around it. */
  .nav .lang   { grid-row: 1; grid-column: 1; justify-self: start; align-self: center; }
  .nav .brand  { grid-row: 1; grid-column: 2; align-self: center; }
  .nav .nav__end { grid-row: 1; grid-column: 3; justify-self: end; align-self: center; }
}
@media (max-width: 860px) {
  .section { padding-block: clamp(4rem, 12vw, 6.5rem); }
}

/* The header is opaque paper now and no longer sits over the photograph, so the
   old ivory-on-dark nav treatment is gone — it would be invisible on white. */

/* Tighter chrome on phones: smaller monogram, no date line. Nothing else has to
   change — the sticky header carries its own height. */
@media (max-width: 640px) {
  :root { --bar-h: 28px; }
  .topbar p { font-size: 0.52rem; letter-spacing: 0.2em; }
  /* One token shrinks the whole lockup. The date line goes on phones — three
     lines of wordmark plus a date makes the fixed header too tall. */
  .nav .brand { --brand-size: 1.05rem; }
  .nav__end { gap: 0.35rem; }
  .music { width: 30px; height: 30px; }
  .music__eq { height: 11px; }
  .nav .brand__date { display: none; }
}
