/* =========================================================================
   K + A · CDMX 2027 — Slideshow stylesheet
   See design-system.md. One color. One size. One weight. One font.
   ========================================================================= */

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

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: "latienne-pro", "Cormorant Garamond", "EB Garamond", Georgia, "Times New Roman", serif;
  font-size: var(--fs);
  font-weight: 400;
  line-height: var(--lh);
  letter-spacing: var(--tracking);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

html, body { height: 100%; overflow: hidden; }

/* Hide scrollbars globally. This is a single-screen, full-bleed
   experience; scenes may use `overflow-y: auto` as a safety net so
   content doesn't get clipped on tiny viewports (s6 form, s7 thanks
   on 844px-tall phones), but the scrollbar itself should never be
   visible — the controls bar is the navigation, not a scrollbar.
   Scroll behavior (touch/wheel/keyboard) is preserved; only the
   visible chrome is hidden. */
* {
  scrollbar-width: none;          /* Firefox */
  -ms-overflow-style: none;       /* legacy Edge / IE */
}
*::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;                  /* WebKit (Safari, Chrome, modern Edge) */
}

img { max-width: 100%; display: block; }
button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  letter-spacing: inherit;
}
p { margin: 0; }
em { font-style: italic; font-weight: 400; }

::selection { background: var(--fg); color: var(--bg); }

/* -----------------------------------------------------------------------
   Audio toggle (now lives inside .controls)
----------------------------------------------------------------------- */

.audio-toggle {
  opacity: 0.45;
  transition: opacity 320ms var(--ease);
}

.audio-toggle:hover,
.audio-toggle:focus-visible { opacity: 0.95; outline: none; }

.audio-toggle .icon {
  position: absolute;
  width: 20px;
  height: 20px;
  transition: opacity 220ms var(--ease);
}

.audio-toggle[data-state="on"]  .icon-on  { opacity: 1; }
.audio-toggle[data-state="on"]  .icon-off { opacity: 0; }
.audio-toggle[data-state="off"] .icon-on  { opacity: 0; }
.audio-toggle[data-state="off"] .icon-off { opacity: 1; }

/* -----------------------------------------------------------------------
   Audio hint (only shown when audible autoplay is blocked)
----------------------------------------------------------------------- */

.audio-hint {
  position: fixed;
  left: clamp(16px, 2.4vw, 28px);
  bottom: clamp(16px, 2.4vw, 28px);
  z-index: 99;
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--fg);
  opacity: 0;
  pointer-events: none;
  transition: opacity 800ms var(--ease);
  font-size: 13px;
  letter-spacing: 0.04em;
}

.audio-hint.is-in {
  opacity: 0.5;
}

.audio-hint__glyph {
  display: inline-block;
  font-size: 18px;
  line-height: 1;
  animation: audio-hint-pulse 2400ms ease-in-out infinite;
}

.audio-hint__label {
  font-style: italic;
  font-weight: 400;
}

@keyframes audio-hint-pulse {
  0%, 100% { opacity: 0.55; transform: translateY(0); }
  50%      { opacity: 1;    transform: translateY(-1px); }
}

@media (prefers-reduced-motion: reduce) {
  .audio-hint__glyph { animation: none; }
}

/* -----------------------------------------------------------------------
   Controls (prev / pause-play / next)
----------------------------------------------------------------------- */

.controls {
  position: fixed;
  left: 50%;
  bottom: clamp(16px, 2.4vw, 28px);
  transform: translateX(-50%);
  z-index: 100;
  display: flex;
  align-items: center;
  gap: 20px;
}

.ctrl {
  position: relative;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  color: var(--fg);
  opacity: 0.32;
  transition: opacity 320ms var(--ease);
}

.ctrl:hover,
.ctrl:focus-visible { opacity: 0.9; outline: none; }

.ctrl[disabled] {
  opacity: 0.12;
  cursor: default;
  pointer-events: none;
}

.ctrl .icon {
  position: absolute;
  width: 18px;
  height: 18px;
  fill: currentColor;
  transition: opacity 220ms var(--ease);
}

#control[data-state="playing"] .icon-play  { opacity: 0; }
#control[data-state="playing"] .icon-pause { opacity: 1; }
#control[data-state="paused"]  .icon-play  { opacity: 1; }
#control[data-state="paused"]  .icon-pause { opacity: 0; }

/* -----------------------------------------------------------------------
   Film overlay — vignette + grain above scenes, below controls (z 100).
----------------------------------------------------------------------- */

.film-overlay {
  position: fixed;
  inset: 0;
  z-index: 90;
  pointer-events: none;
  background: radial-gradient(
    ellipse 90% 86% at 50% 42%,
    transparent 0%,
    transparent 48%,
    rgba(0, 0, 0, 0.22) 72%,
    rgba(0, 0, 0, 0.58) 100%
  );
}

.film-overlay::before {
  content: "";
  position: absolute;
  inset: -30%;
  opacity: 0.11;
  mix-blend-mode: soft-light;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='256' height='256'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.72' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 200px 200px;
}

body.is-cover .film-overlay {
  visibility: hidden;
}

/* -----------------------------------------------------------------------
   Stage / scenes
----------------------------------------------------------------------- */

.stage {
  position: fixed;
  inset: 0;
}

.scene {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 700ms var(--ease);
  display: grid;
  align-items: center;
  padding-top: var(--desk-pad-top);
  padding-bottom: var(--desk-pad-bottom);
  padding-left: clamp(28px, 6vw, 96px);
  padding-right: clamp(28px, 6vw, 96px);
  overflow: hidden;
}

.scene.is-active {
  opacity: 1;
  pointer-events: auto;
}

.scene-grid {
  display: grid;
  width: 100%;
  height: 100%;
  align-items: center;
  gap: clamp(40px, 6vw, 96px);
}

.scene-grid--right-photo,
.scene-grid--left-photo {
  grid-template-columns: 1fr 1fr;
}

.scene-grid--left-photo .photo--portrait {
  justify-self: start;
}

.scene-grid--left-photo .copy--left-mid {
  justify-self: start;
  padding-left: clamp(16px, 2vw, 32px);
}

/* -----------------------------------------------------------------------
   Copy regions
----------------------------------------------------------------------- */

.copy { display: block; }

.copy--left-mid {
  align-self: center;
  justify-self: start;
  max-width: 720px;
}

.copy--left-top {
  align-self: start;
  justify-self: start;
  max-width: 720px;
  padding-top: 4vh;
}

.copy--center {
  width: 100%;
  max-width: 1100px;
  margin: 0 auto;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(28px, 4vh, 56px);
}

.copy--bottom-right {
  position: absolute;
  right: clamp(28px, 6vw, 96px);
  bottom: clamp(40px, 8vh, 96px);
  z-index: 2;
  text-align: right;
  max-width: 80%;
}

.stack {
  display: flex;
  flex-direction: column;
  gap: 0.4em;
}

.stack--tight { gap: 0.15em; }

.line-gap { height: 0.25em; }
.line-gap--lg { height: 1em; }

/* -----------------------------------------------------------------------
   Lines (typewriter targets)
----------------------------------------------------------------------- */

.line {
  min-height: 1.35em;
  position: relative;
  display: inline-block;
  white-space: pre-wrap;
}

.line--center { display: block; }

/* Cursor — only on the element currently typing. */
.line.is-typing::after,
.line-lead.is-typing::after {
  content: "";
  display: inline-block;
  width: 2px;
  height: 0.95em;
  margin-left: 6px;
  vertical-align: -0.08em;
  background: currentColor;
  animation: blink 900ms steps(1) infinite;
}

@keyframes blink {
  0%, 49%   { opacity: 1; }
  50%, 100% { opacity: 0; }
}

/* Fade-out for line/lead/decoration spans (replaces backspacing). */
.line.is-out,
.line-lead.is-out,
.amor-fade.is-out,
.city-fade.is-out {
  opacity: 0;
  transition: opacity 1400ms var(--ease);
}

/* -----------------------------------------------------------------------
   Photos
----------------------------------------------------------------------- */

.photo {
  margin: 0;
  overflow: hidden;
  border-radius: 4px;
  opacity: 0;
  transition: opacity var(--photo-fade) var(--ease), transform var(--photo-fade) var(--ease);
}

.photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1);
  /* Exit speed — when .is-in drops, the zoom eases back over the same
     window as the photo opacity fade (no animation snap). */
  transition: transform var(--photo-fade) var(--ease);
}

.photo.is-in { opacity: 1; transform: translate3d(0, 0, 0); }

/* Hero (s1): the marquee sign sits at the very top of the source frame.
   Anchor the crop to the top so it's never clipped, regardless of the
   frame's aspect ratio. The top-origin zoom override below keeps it in
   frame during the Ken Burns drift too. */
#s1 .photo--portrait img { object-position: center top; }

.photo--portrait {
  aspect-ratio: 3 / 4;
  max-width: 100%;
  width: 100%;
  /* Cap to the desktop content box (asymmetric scene padding + controls)
     so the photo stays inside the stage and clears the bottom bar. */
  max-height: min(
    84vh,
    calc(100vh - var(--desk-pad-top) - var(--desk-pad-bottom) - 16px)
  );
  justify-self: end;
}

.photo--wide {
  aspect-ratio: 16 / 9;
  width: 100%;
  max-width: 1100px;
}

.photo--bleed {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  transform: translate3d(-40px, 0, 0);
}

/* Pull the visible crop window toward the top of the image so the upper
   architecture (medallion, carved arch) stays in frame. */
.photo--bleed img {
  object-position: center 28%;
}

/* Full-bleed top and sides; leaves a black band at the bottom for the
   "For a weekend celebrating amor ♥" copy. Absolute-positioned to escape
   the scene's outer padding. */
.photo--full {
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: 82vh;
  margin: 0;
}

.photo--full img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
}

.scene--amor .copy--bottom-right {
  z-index: 2;
  /* Photo above runs 0 → 82vh; black band 82vh → 100vh. Center the line
     at the band's midpoint (91vh) so it sits halfway between the bottom of
     the image and the bottom of the screen. */
  top: 89vh;
  bottom: auto;
  right: clamp(24px, 4vw, 72px);
  transform: translateY(-50%);
  text-shadow: none;
}

/* Scene 2 — "amor ♥" fade-in (replaces the typed "<3") */
.amor-fade,
.city-fade {
  opacity: 0;
  transition: opacity 2000ms var(--ease);
  white-space: nowrap;
}

.amor-fade.is-in,
.city-fade.is-in { opacity: 1; }

.heart {
  font-style: normal;
  display: inline-block;
}

/* Scene 4 — tighter stack; the magical-place line shrinks just enough
   to stay on a single line at desktop widths. */
#s4 .stack { gap: 0.25em; }

/* Scene 7 — thank-you copy sits a bit above center so the first line
   isn't pushed to the middle. */
#s7 .copy--left-mid {
  align-self: start;
  padding-top: clamp(48px, 14vh, 160px);
}

/* -----------------------------------------------------------------------
   Form (scene 6)
----------------------------------------------------------------------- */

.rsvp {
  display: flex;
  flex-direction: column;
  gap: 1.4em;
  width: 100%;
  max-width: 480px;
  margin-top: 1.8em;
  margin-bottom: 1.4em;
  font-size: 0.78em;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 0;
  opacity: 0;
  transition: opacity 800ms var(--ease);
}

.field.is-in { opacity: 1; }

.field span {
  font-size: 0.7em;
  line-height: 1.2;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  opacity: 0.5;
  margin-bottom: 4px;
}

.field input {
  width: 100%;
  padding: 0.05em 0 0.15em;
  background: transparent;
  color: var(--fg);
  border: 0;
  border-bottom: 1px solid currentColor;
  border-radius: 0;
  font: inherit;
  font-size: 1em;
  letter-spacing: var(--tracking);
  line-height: 1.3;
  outline: none;
}

.field input:focus { border-bottom-width: 2px; padding-bottom: calc(0.15em - 1px); }

/* Mailing address is two stacked inputs (line1 + line2) under one label so
   browser autofill — which splits a saved address into address-line1 /
   address-line2 — populates both at once. The second input gets a little
   top margin so the two underlines read as separate rows, not one block. */

.field--address input + input { margin-top: 0.55em; }

.field--address input::placeholder {
  color: currentColor;
  opacity: 0.32;
  font-style: italic;
  letter-spacing: var(--tracking);
}

.field--check {
  flex-direction: row;
  align-items: center;
  gap: 12px;
  margin-top: 0.6em;
}

.field--check input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  flex: 0 0 16px;
  margin: 0;
  border: 1px solid var(--fg);
  border-radius: 2px;
  background: transparent;
  cursor: pointer;
  position: relative;
  opacity: 0.4;
  transition: opacity 220ms var(--ease), background-color 220ms var(--ease);
}

.field--check input[type="checkbox"]:checked {
  background-color: var(--fg);
  opacity: 1;
}

.field--check input[type="checkbox"]:checked::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 5px;
  width: 4px;
  height: 8px;
  border: 1.5px solid var(--bg);
  border-top: none;
  border-left: none;
  transform: rotate(45deg);
}

.field--check input[type="checkbox"]:focus-visible {
  outline: 1px solid var(--fg);
  outline-offset: 3px;
  opacity: 0.7;
}

.field--check span {
  font-size: 0.75em;
  line-height: 1.35;
  letter-spacing: 0;
  text-transform: none;
  opacity: 0.85;
  margin: 0;
}

/* Scene 6 — match other portrait columns (cover fills the 3:4 frame). */
.scene--form .photo--portrait img {
  object-fit: cover;
  object-position: center 40%;
}

/* Allow the prompt to wrap inside its column and reserve two lines so the
   form below doesn't shift down as the typewriter advances onto line two.
   (At >900px viewports the heading is ~631px wide and the form column is
   <500px, so a single-line nowrap heading clipped behind the photo.) */
.scene--form [data-line="form-q"] {
  white-space: normal;
  max-width: 100%;
  min-height: calc(2 * 1.35em);
}

/* minmax(0,1fr) keeps 1fr 1fr equal even with long heading content. */
#s6 .scene-grid--right-photo {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}

#s6 .copy--left-top {
  min-width: 0;
  max-width: 100%;
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 1.6em;
  margin-top: 1.4em;
  opacity: 0;
  transition: opacity 600ms var(--ease);
  pointer-events: none;
}

.actions--single { justify-content: flex-start; }

.actions.is-in { opacity: 1; pointer-events: auto; }
.actions.is-locked.is-in { opacity: 0; pointer-events: none; }

.actions button {
  position: relative;
  padding: 0;
  text-align: left;
}

.actions button::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -4px;
  height: 1px;
  background: var(--fg);
  transform-origin: left;
  transform: scaleX(1);
  transition: transform 600ms var(--ease);
}

.actions button:hover::after,
.actions button:focus-visible::after {
  transform-origin: right;
  transform: scaleX(0);
}

.actions button[disabled] { opacity: 0.4; cursor: default; }

.rsvp__error {
  margin: 0.8em 0 0;
  min-height: 1.2em;
  font-size: 0.78em;
  color: #b3261e;
  opacity: 0;
  transition: opacity 280ms var(--ease);
}
.rsvp__error:not(:empty) { opacity: 1; }

/* -----------------------------------------------------------------------
   Scene-specific
----------------------------------------------------------------------- */

#s5 .photo--wide { max-height: 64vh; }

/* -----------------------------------------------------------------------
   Responsive
----------------------------------------------------------------------- */

@media (max-width: 900px) {
  * { -webkit-tap-highlight-color: rgba(239, 231, 210, 0.12); }

  .stage { height: 100dvh; }

  /* Mobile layout shell — every scene is a vertically-centered flex column
     so the photo + copy stack reads as a composed slide, not a top-pinned
     blog post. Padding is identical on every scene; gap between photo and
     copy is identical on every scene. */
  .scene {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: center;
    gap: var(--m-stack-gap);
    min-height: 100dvh;
    overflow-y: auto;
    padding-top: max(env(safe-area-inset-top, 0px), var(--m-pad-top));
    padding-bottom: calc(var(--m-controls-clear) + env(safe-area-inset-bottom, 0px));
    padding-left: max(env(safe-area-inset-left, 0px), var(--m-inset));
    padding-right: max(env(safe-area-inset-right, 0px), var(--m-inset));
  }

  /* Keep the logo finale grid-centered (single child, no stack rhythm). */
  .scene--logo {
    display: grid;
    place-items: center;
  }

  /* Form scene (mobile): plain form on the scene background — no photo,
     no panel container. The photo figure is hidden on mobile but kept in
     the DOM for the desktop two-column layout. */
  #s6 .photo {
    display: none;
  }

  /* Vertically center the form within the scene; `safe` keeps the top
     reachable when content exceeds the viewport (iPhone SE).
     Extra padding-top bumps the prompt line down off the safe-area edge —
     without it, `safe center` falls back to flex-start (because the form
     is taller than the available area) and the heading hugs the top of
     the viewport with no visual breathing room. The bottom is tightened
     by the `.rsvp` overrides below so the submit doesn't overlap the
     fixed controls. */
  .scene--form {
    justify-content: safe center;
    padding-top: max(
      env(safe-area-inset-top, 0px),
      clamp(56px, 8.5vh, 88px)
    );
    /* Extra bottom clearance (beyond the default --m-controls-clear) so the
       centered form's Submit row never collides with the fixed controls. */
    padding-bottom: calc(
      var(--m-controls-clear) + clamp(36px, 7vh, 72px) +
      env(safe-area-inset-bottom, 0px)
    );
  }

  /* Tighten the form's outer + trailing margins on mobile so the
     content height shrinks enough to absorb the extra padding-top
     above without overlapping the fixed controls at the bottom. */
  .scene--form .rsvp {
    margin-top: 1em;
    margin-bottom: 0.4em;
  }

  /* Don't reserve a min-height row for the (usually empty) error message
     below the submit on mobile — it adds ~28px of dead space directly
     below the submit on every viewport. The reserved row still applies
     on desktop. */
  .scene--form .rsvp__error {
    margin-top: 0.4em;
    min-height: 0;
  }

  /* The internal scene-grid (desktop two-column) collapses to a single flex
     column on mobile, sharing the scene's stack gap. */
  .scene-grid,
  .scene-grid--right-photo,
  .scene-grid--left-photo {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: auto;
    align-items: stretch;
    justify-content: center;
    gap: var(--m-stack-gap);
  }

  /* Photo always leads the stack on mobile (cinematic). */
  .scene-grid--right-photo .photo,
  .scene-grid--left-photo .photo {
    order: -1;
  }

  /* Portrait photos: stretched to full inset width (matches text edges),
     tall and impactful, cropped on faces via object-position. */
  .photo--portrait {
    aspect-ratio: auto;
    align-self: stretch;
    justify-self: stretch;
    width: 100%;
    max-width: 100%;
    height: var(--m-photo-cap);
    max-height: var(--m-photo-cap);
    margin: 0;
  }

  .photo--portrait img {
    display: block;
    width: 100%;
    height: 100%;
    max-width: 100%;
    max-height: 100%;
    object-fit: cover;
    object-position: center 30%;
    margin: 0;
  }

  /* Per-photo object-position tuned so faces stay in frame. */
  #s1 .photo--portrait img { object-position: center 36%; }
  #s3 .photo--portrait img { object-position: 38% center; }
  #s4 .photo--portrait img { object-position: center 70%; }
  #s6 .photo--portrait img { object-position: center 40%; }
  #s7 .photo--portrait img { object-position: center 38%; }

  /* Scene 1 — match the frame to the source photo's natural 2:3 aspect on
     mobile so the full image (marquee, couple, full bodies) shows uncropped
     at the inset width. Falls back to a 70dvh ceiling on unusually short
     viewports so the line below + controls stay reachable. */
  #s1 .photo--portrait {
    aspect-ratio: 2 / 3;
    height: auto;
    max-height: 70dvh;
  }
  #s1 .photo--portrait img { object-position: center; }

  /* Full-bleed amor sits in flow on mobile. Portrait 4/5 aspect scales the
     photo up and (with object-fit: cover on a 3/2 landscape source) crops to
     the middle ~53% horizontally — empty side chairs drop away and the couple
     + "CHURROS CALIENTITOS" sign fill the frame. */
  .photo--full {
    position: relative;
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 5;
    margin: 0;
    overflow: hidden;
  }

  .photo--full img {
    object-fit: cover;
    /* X = 43% (vs default 50%) shifts the source window left so the
       "CHURROS CALIENTITOS" sign — which sits left-of-center in the
       source frame — reads more centered in the mobile portrait crop.
       43% is the sweet spot: it nudges the text closer to frame center
       (~47% vs original 38%) while keeping the trailing "S" intact
       (right edge of visible window lands a few source pixels past it). */
    object-position: 43% 42%;
  }

  /* Copy regions — full inset width, left-aligned per design system.
     The compound selectors `.scene-grid--left-photo .copy--left-mid` and
     `.scene-grid--right-photo .copy--left-mid` add a desktop padding-left
     at higher specificity (0,2,0), so we have to match that specificity on
     mobile to zero them out. Otherwise scene 4 (the only left-photo scene)
     ends up indented past its photo's left edge. */
  .copy,
  .copy--left-mid,
  .copy--left-top,
  .scene-grid--left-photo .copy--left-mid,
  .scene-grid--right-photo .copy--left-mid {
    align-self: stretch;
    justify-self: stretch;
    text-align: left;
    width: 100%;
    max-width: 100%;
    padding: 0;
    margin: 0;
  }

  /* Bottom-right (amor) becomes inline on mobile. */
  .copy--bottom-right {
    position: static;
    align-self: stretch;
    width: 100%;
    max-width: 100%;
    top: auto;
    bottom: auto;
    right: auto;
    left: auto;
    transform: none;
    text-align: left;
    padding: 0;
  }

  /* Controls bar — safe-area lifted, larger gap, brighter on photos. */
  .controls {
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    bottom: max(calc(env(safe-area-inset-bottom, 0px) + 16px), 24px);
    gap: 28px;
  }

  .ctrl {
    width: 44px;
    height: 44px;
    opacity: 0.42;
  }

  /* Form (scene 6) — bigger taps, keyboard-aware, generous breathing room
     so the form fills the viewport on phones instead of bunching at the top. */
  .rsvp {
    font-size: 1em;
    gap: clamp(1.65em, 4vh, 2.1em);
  }

  .field input,
  .cover__pw-input { font-size: 16px; }

  /* Tighten the label→underline gap: smaller top padding pulls each input
     line up closer to its label, and a shorter min-height keeps the whole
     form compact enough that Submit clears the fixed controls. */
  .field span { margin-bottom: 2px; }

  .field input:not([type="checkbox"]) {
    padding: 6px 0;
    min-height: 34px;
  }

  .field--address input + input { margin-top: 0.4em; }

  .field--check { gap: 14px; }

  .field--check input[type="checkbox"] {
    width: 22px;
    height: 22px;
    min-height: 22px;
    flex: 0 0 22px;
    padding: 0;
  }

  .field--check input[type="checkbox"]:checked::after {
    top: 4px;
    left: 7px;
    width: 6px;
    height: 11px;
  }

  .actions button {
    padding: 14px 4px;
    min-height: 44px;
  }

  /* On mobile, .actions stays hidden via the universal `.actions { opacity: 0 }`
     default until the JS adds .is-in after the field-stagger loop. The Submit
     button greys out via its [disabled] attribute (handled by the universal
     `.actions button[disabled] { opacity: 0.4 }` rule above). We deliberately
     do NOT make `.is-locked` alone visible here — that caused Submit to flash
     in during the heading typewriter on first entry. */
  .scene--form .actions.is-locked.is-in {
    opacity: 1;
    pointer-events: auto;
  }

  .cover__pw-error { font-size: 0.72em; }

  /* Cover — courtyard bg (no baked K&A). Center the gate, use dvh + safe
     area so the submit control isn't clipped below the fold on iOS. */
  .cover__inner {
    justify-content: center;
    padding-top: max(env(safe-area-inset-top, 0px), 20px);
    padding-bottom: max(env(safe-area-inset-bottom, 0px), 28px);
  }

  .cover__password {
    gap: 10px;
    width: min(320px, 88vw);
  }

  /* Plain white arrow stacked below the password line — no circle. */
  .cover__pw-submit {
    opacity: 1;
    min-width: 44px;
    min-height: 44px;
    display: grid;
    place-items: center;
    padding: 4px 8px;
  }

  .cover__play-group.is-hidden {
    display: none;
  }

  /* Scene 7 — same 2:3 portrait frame as s4, but s7 carries 6 lines of
     signoff copy vs s4's 5, so an identical photo height makes the scene
     ~13px taller than the viewport on 844px phones. That overflow defeats
     `justify-content: center` and pins the photo to the very top with no
     breathing room. Cap the photo slightly shorter (54dvh) so the whole
     scene fits and re-centers, giving the same top margin as every other
     screen. object-fit: cover crops the few extra rows off top + bottom. */
  #s7 .photo--portrait {
    aspect-ratio: 2 / 3;
    height: auto;
    max-height: 54dvh;
  }
  #s7 .photo--portrait img { object-position: center; }

  /* Tighter scene-grid gap on s7 only — the 6 signoff lines need room
     below the now-taller photo and the default --m-stack-gap (~36px)
     would push the last line into the fixed controls. */
  #s7 .scene-grid--right-photo {
    gap: clamp(14px, 2.5vh, 22px);
  }

  #s7 .copy--left-mid {
    padding-top: 0;
    align-self: stretch;
  }

  #s7 .stack { gap: 0.18em; }
  #s7 .line-gap--lg { height: 0.4em; }

  /* Scene 4 — match the frame to the source photo's natural 2:3 aspect on
     mobile so the full image (tree canopy, ornate medallion + oval window,
     iron gate, beetle, couple in motion blur, street) shows uncropped.
     Capped at 62dvh so the 5 lines of copy below + controls stay reachable
     on shorter viewports. */
  #s4 .photo--portrait {
    aspect-ratio: 2 / 3;
    height: auto;
    max-height: 62dvh;
  }
  #s4 .photo--portrait img { object-position: center; }
}

/* The heading wrap + 2-line min-height now applies at every viewport
   (see .scene--form [data-line="form-q"] above), so this 720px-scoped
   override is no longer needed. */

@media (max-width: 480px) {
  .rsvp { max-width: 100%; }

  .field--check span { font-size: 0.85em; }

  .cover__pw-submit {
    font-size: 1.35em;
  }
}

/* Instant restore: disable all transitions/animations on a scene's
   DESCENDANTS when its snapshot is being applied (so lines/photo/fades
   don't replay their entrance transitions). The scene element itself
   keeps its own opacity transition so back-nav still cross-fades in.
   Class removed on next frame. */
.scene.is-restoring *,
.scene.is-restoring *::before,
.scene.is-restoring *::after {
  transition: none !important;
  animation: none !important;
}

/* Motion effects live in no-preference so the global reduce block below
   cannot zero them out with animation-duration: 0ms on *. */
@media (prefers-reduced-motion: no-preference) {
  @keyframes film-grain {
    0%   { transform: translate(0, 0); }
    25%  { transform: translate(-3%, 2%); }
    50%  { transform: translate(2.5%, -2.5%); }
    75%  { transform: translate(-1.5%, -2%); }
    100% { transform: translate(0, 0); }
  }

  /* Ken Burns via transition (not keyframes) so exit doesn't snap when
     .is-in is removed — enter is slow, exit matches the opacity fade. */
  .photo.is-in > img {
    transform: scale(1.05) translate3d(-0.8%, -0.5%, 0);
    transform-origin: center center;
    transition: transform 12s ease-out;
    backface-visibility: hidden;
  }

  /* Hero (s1): zoom from the top edge so the marquee stays pinned. */
  #s1 .photo.is-in > img {
    transform: scale(1.05);
    transform-origin: center top;
    transition: transform 12s ease-out;
  }

  .film-overlay::before {
    animation: film-grain 0.5s steps(8) infinite;
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0ms !important;
    transition-duration: 200ms !important;
  }
  .line.is-typing::after { animation: none; }
  .film-overlay::before { opacity: 0.05; animation: none; }
}

/* -----------------------------------------------------------------------
   Cover (gates music + slideshow on a user gesture)
   Photo is full-bleed (object-fit: cover). The IMG sits as a direct
   sibling of .cover__inner so .cover__inner's z-index wins and the
   form is clickable.
----------------------------------------------------------------------- */

.cover {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: var(--bg);
  opacity: 1;
  transition: opacity 700ms var(--ease);
  overflow: hidden;
}

.cover.is-out {
  opacity: 0;
  pointer-events: none;
}

/* Translucent black overlay over the courtyard image: darkens the
   background while the photo still reads through. Sits above the image
   (z-index 0) and below the form (z-index 2). */
.cover::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: rgba(0, 0, 0, 0.5);
  pointer-events: none;
}

/* Cover ink: white form elements over the darkened image. */
:root {
  --cover-ink: #ffffff;
}

.cover__inner {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding-left: clamp(20px, 5vw, 48px);
  padding-right: clamp(20px, 5vw, 48px);
  padding-top: var(--gutter);
  padding-bottom: clamp(40px, 8vh, 96px);
  /* Space between the white K&A mark and the password gate below it. */
  gap: clamp(28px, 5vh, 56px);
}

/* White K&A mark on the cover (same artwork as the finale, trimmed of its
   surrounding whitespace). Sits above the password gate. */
.cover__brand {
  flex: 0 0 auto;
  width: clamp(200px, 34vw, 360px);
  aspect-ratio: 1497 / 779;
  background-color: #ffffff;
  -webkit-mask: url('assets/ka-logo-alpha-trim.png?v=1') no-repeat center / contain;
          mask: url('assets/ka-logo-alpha-trim.png?v=1') no-repeat center / contain;
}

.cover__mark {
  font-size: clamp(28px, 3.2vw, 38px);
  letter-spacing: 0.04em;
  color: var(--fg);
  opacity: 0.85;
}

.cover__play {
  width: clamp(54px, 5.6vw, 64px);
  height: clamp(54px, 5.6vw, 64px);
  border-radius: 50%;
  border: 1px solid var(--cover-ink);
  color: var(--cover-ink);
  display: grid;
  place-items: center;
  opacity: 0.55;
  transition: opacity 320ms var(--ease), transform 320ms var(--ease), border-color 320ms var(--ease);
  background: transparent;
}

.cover__play:hover,
.cover__play:focus-visible {
  opacity: 0.9;
  outline: none;
  transform: scale(1.04);
}

.cover__play svg {
  width: 28%;
  height: 28%;
  transform: translateX(1.5px);
}

.cover__hint {
  font-size: 13px;
  letter-spacing: 0.08em;
  font-style: italic;
  color: var(--fg);
  opacity: 0.5;
}

/* While the cover is up, hide scene controls (audio toggle is inside). */
body.is-cover .controls {
  opacity: 0;
  pointer-events: none;
}

/* -----------------------------------------------------------------------
   K & A logo mark (cover + final scene)
   Source PNG is black artwork on white (no alpha); use luminance masking
   so the white background is treated as transparent.
----------------------------------------------------------------------- */

.logo-mark {
  background-color: #ffffff;
  /* Alpha mask (opaque letters / transparent bg) so it works on iOS Safari,
     which doesn't support mask-mode: luminance. */
  -webkit-mask: url('assets/ka-logo-alpha.png?v=1') no-repeat center / contain;
          mask: url('assets/ka-logo-alpha.png?v=1') no-repeat center / contain;
}

.cover__logo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
  z-index: 0;
}

.scene--logo {
  display: grid;
  place-items: center;
  padding: clamp(24px, 6vw, 96px);
}

.logo-mark {
  width: clamp(180px, 30vmin, 380px);
  aspect-ratio: 1 / 1;
  opacity: 0;
  transition: opacity 1400ms var(--ease);
  background-color: #ffffff;
}

.logo-mark.is-in { opacity: 1; }

/* -----------------------------------------------------------------------
   Password gate
----------------------------------------------------------------------- */

.cover__slot {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 100%;
}

.cover__slot > * {
  grid-area: 1 / 1;
}

.cover__password {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(16px, 2.4vh, 24px);
  transition: opacity 400ms var(--ease);
}

/* Input on top, submit arrow stacked below it (all viewports). */
.cover__pw-row {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(14px, 2.2vh, 22px);
  width: 100%;
}

.cover__password.is-out {
  opacity: 0;
  pointer-events: none;
}

.cover__pw-input {
  background: transparent;
  border: 0;
  border-bottom: 1.5px solid var(--cover-ink);
  color: var(--cover-ink);
  font: inherit;
  font-size: 1em;
  letter-spacing: 0.14em;
  text-align: center;
  width: clamp(200px, 32vw, 260px);
  padding: 0.45em 0.4em 0.55em;
  outline: none;
  opacity: 1;
  transition: border-bottom-width 200ms ease, padding-bottom 200ms ease;
}

.cover__pw-input::placeholder {
  color: var(--cover-ink);
  opacity: 0.5;
  letter-spacing: 0.18em;
  font-style: italic;
  text-transform: lowercase;
}

.cover__pw-input:focus {
  border-bottom-width: 2px;
  padding-bottom: calc(0.55em - 0.5px);
}

/* Phones: bigger tap target on the password input (>= 44px). */
@media (max-width: 900px) {
  .cover__pw-input {
    width: min(280px, 78vw);
    padding: 14px 8px;
    min-height: 48px;
    font-size: 16px; /* prevent iOS zoom on focus */
  }
}

.cover__pw-error {
  font-size: 0.72em;
  letter-spacing: 0.06em;
  font-style: italic;
  color: var(--cover-ink);
  opacity: 0.75;
  min-height: 1.4em;
  text-align: center;
  margin: 0;
}

.cover__play-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Anchor to the top of the shared cover__slot grid cell so the play
     button lands in the same position the password input occupied —
     i.e. tucked right under the K&A — instead of floating in the
     middle of the slot's natural cell. */
  align-self: start;
  gap: clamp(14px, 2vh, 20px);
}

.cover__play-group.is-hidden {
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
}

.cover__play-label {
  font-family: inherit;
  font-style: italic;
  font-size: 0.78em;
  letter-spacing: 0.16em;
  text-transform: lowercase;
  color: var(--cover-ink);
  opacity: 0.55;
  margin: 0;
}

.cover__pw-submit {
  color: var(--cover-ink);
  opacity: 0.9;
  font-size: 1.25em;
  letter-spacing: 0;
  padding: 0.4em 0.8em;
  transition: opacity 200ms ease, transform 200ms var(--ease);
  line-height: 1;
  min-height: 44px;
  min-width: 44px;
}

.cover__pw-submit:hover,
.cover__pw-submit:focus-visible {
  opacity: 1;
  outline: none;
  transform: translateX(2px);
}

