@font-face {
  font-family: "Moon Invite";
  src: url("/assets/font/Moon2.0-Regular.otf") format("opentype");
  font-display: swap;
}

@font-face {
  font-family: "Fraunces Invite";
  src: url("/assets/font/Fraunces-VariableFont_SOFT,WONK,opsz,wght.ttf") format("truetype");
  font-display: swap;
}

@font-face {
  font-family: "CL Handwriting";
  src: url("/assets/font/asset-f3c91.otf") format("opentype");
  font-display: swap;
}
/* Bold weight of Moon so `font-weight: 700` on "Moon Invite" uses the bold file. */
@font-face {
  font-family: "Moon Invite";
  src: url("/assets/font/Moon2.0-Bold.otf") format("opentype");
  font-weight: 700;
  font-display: swap;
}
/* Display + icon faces used on the invite (files already in the repo). */
@font-face {
  font-family: "Envelove Caps";
  src: url("/assets/font/envelovecaps.otf") format("opentype");
  font-display: swap;
}
@font-face {
  font-family: "Envelove Icons";
  src: url("/assets/font/enveloveicons.otf") format("opentype");
  font-display: swap;
}

:root {
  color-scheme: dark;
  --navy: #101f42;
  --midnight: #08142d;
  --paper: #fffaf4;
  --paper-soft: #f8ecdf;
  --pink: #e6a7c2;
  --pink-line: #d88aac;
  --ink: #172452;
  --muted: #6f6878;
  --gold: #d8b771;
  --shadow: rgba(3, 8, 20, 0.45);
  --nettuno-paper: url("/assets/image/nettuno-paper.png");
  /* Single source of truth for how far the envelope/invite area sits from the
     top. Lowered from the old ~70-150px so the experience sits toward the upper
     part of the viewport now that the controls live in a bottom footer.
     Used by envelope, pocket, invite-wrap and the intro/stage padding so every
     layer stays vertically aligned (see desktop override below). */
  --stage-top: clamp(40px, 11vh, 104px);
  --envelope-ratio: 713.06 / 493;
  --envelope-width: min(92%, 430px);
  --envelope-mouth-y: 48.33%;
  --envelope-card-width: 94.42%;
  /* Approx height of the fixed action footer (button row + vertical padding),
     excluding safe-area inset. Drives bottom padding for scrollable content and
     the offset for the scroll-shortcut buttons so nothing hides behind it. */
  --footer-h: 66px;
  font-family: "Fraunces Invite", Georgia, serif;
}

* { box-sizing: border-box; }

.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;
}

html, body {
  min-height: 100%;
  margin: 0;
  background:
    radial-gradient(circle at 50% 10%, rgba(54, 92, 156, 0.75), transparent 42%),
    linear-gradient(180deg, #102a5b, #08142d 50%, #050b19);
  color: var(--paper);
}

html {
  overflow-y: auto;
  scrollbar-color: rgba(255, 250, 244, 0.28) transparent;
  scrollbar-gutter: stable;
}

::-webkit-scrollbar {
  width: 8px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: rgba(255, 250, 244, 0.22);
  border-radius: 999px;
}

body {
  position: relative;
  min-height: 100%;
  padding: max(14px, env(safe-area-inset-top)) 12px max(16px, env(safe-area-inset-bottom));
  overflow-x: hidden;
}

body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(circle at 9% 13%, rgba(255, 255, 255, 0.9) 0 1px, transparent 1.8px),
    radial-gradient(circle at 24% 31%, rgba(255, 250, 244, 0.74) 0 1px, transparent 1.7px),
    radial-gradient(circle at 74% 18%, rgba(239, 189, 208, 0.72) 0 1px, transparent 1.8px),
    radial-gradient(circle at 88% 41%, rgba(255, 255, 255, 0.82) 0 1px, transparent 1.8px),
    radial-gradient(circle at 42% 72%, rgba(255, 250, 244, 0.88) 0 1.2px, transparent 2px),
    radial-gradient(circle at 63% 88%, rgba(255, 255, 255, 0.68) 0 1px, transparent 1.7px);
  background-size: 180px 220px, 230px 260px, 260px 300px, 310px 340px, 190px 280px, 280px 210px;
  animation: twinkle 5.8s ease-in-out infinite alternate;
}

.firefly-layer {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  mix-blend-mode: screen;
}

.firefly {
  position: absolute;
  left: var(--firefly-x);
  top: var(--firefly-y);
  width: var(--firefly-size);
  height: var(--firefly-size);
  border-radius: 999px;
  opacity: 0;
  background: rgba(255, 222, 132, 0.86);
  box-shadow:
    0 0 8px 3px rgba(255, 213, 111, 0.34),
    0 0 20px 10px rgba(230, 167, 194, 0.1);
  filter: blur(0.1px);
  animation:
    fireflyDrift var(--firefly-drift-duration) ease-in-out var(--firefly-drift-delay) infinite alternate,
    fireflyGlow var(--firefly-glow-duration) ease-in-out var(--firefly-glow-delay) infinite;
  will-change: transform, opacity;
}


.stage {
  position: relative;
  z-index: 1;
  width: min(100%, 460px);
  min-height: calc(100svh - 28px);
  margin: 0 auto;
  overflow: hidden;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}

.stage.is-open {
  min-height: auto;
  overflow: visible;
  padding-top: var(--stage-top);
  /* Clear the fixed bottom footer, then leave the same visual breathing room
     below the invite as --stage-top gives it above, so the card sits with
     equal gaps between page top and footer. */
  padding-bottom: calc(var(--footer-h) + env(safe-area-inset-bottom) + var(--stage-top));
}

.stage::before {
  display: none;
}

.animate-envelope-scene {
  position: absolute;
  left: 50%;
  top: var(--stage-top);
  z-index: 32;
  /* Sized so the canvas card's END state (721px of the 954px stage) equals
     the DOM invite width (min(94vw, 560px)) — the handoff crossfade is then
     size-exact instead of jumping: 94vw*954/721 = 124.4vw, 560*954/721 = 741. */
  width: min(124.4vw, 741px);
  aspect-ratio: 954 / 960;
  transform: translate(-50%, -48%);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 220ms ease, visibility 220ms ease;
}

/* While the open plays, drift the scene so the card's final resting spot
   coincides with where the DOM invite hero appears (card centre sits at
   57.7% of the canvas; the hero centre lands at stage-top + 35.45% of the
   invite width — solving gives -31.1% translateY). Slow ease over the open
   animation so it reads as the card settling into place. */
.animate-envelope-scene.is-converging {
  transition: opacity 220ms ease, visibility 220ms ease,
    transform 3600ms cubic-bezier(0.25, 0.6, 0.3, 1);
  /* Low enough that the flap opening into the canvas's upper half stays on
     screen; the invite measures the card's final rect at reveal and settles
     itself up to reading position. */
  transform: translate(-50%, -8%);
}

.animate-envelope-scene[hidden] {
  display: none;
}

.animate-envelope-scene.is-active {
  opacity: 1;
  visibility: visible;
}

.animate-envelope-shell,
.animate-envelope-canvas {
  display: block;
  width: 100%;
  height: 100%;
}

.animate-envelope-canvas {
  background: transparent;
}

/* The shell wraps the canvas AND the personalised name overlay, so the CSS
   idle wiggle below shakes both together (the name can't live in the canvas as
   it's per-guest). Also the anchor for the absolutely-positioned name layer. */
.animate-envelope-shell {
  position: relative;
}

@keyframes shellWiggle {
  0%, 100% { transform: rotate(0deg) translateY(0); }
  18% { transform: rotate(-1.3deg) translateY(-1px); }
  36% { transform: rotate(1.1deg) translateY(1px); }
  54% { transform: rotate(-0.8deg) translateY(0); }
  72% { transform: rotate(0.5deg) translateY(-1px); }
}
.animate-envelope-shell.is-wiggling {
  animation: shellWiggle 850ms 3s ease-in-out infinite;
  transform-origin: 50% 60%;
}

/* When the canvas hero is the idle intro, hide the legacy SVG envelope but keep
   its layout box (visibility, not display) so the "Tap" cue stays in place. */
.stage.is-canvas-intro .intro-envelope {
  visibility: hidden;
  animation: none;
}

/* Personalised name sitting on the canvas envelope's address area. Positioned
   as a % of the shell so it scales with the canvas at any size; font-size is
   driven by fitCanvasName() so long names shrink to fit (no clip/overflow). */
.canvas-name-layer {
  position: absolute;
  left: 50%;
  /* The envelope art sits in the LOWER half of the 954x960 canvas (bbox y ~48%
     to ~99%). The box is anchored by its CENTRE (flex-centred content, so
     multi-line names grow symmetrically): centre = top + height/2 = 78% of
     the canvas ≈ 59% down the envelope — a single-line name reads just below
     the envelope's centre line, in the lower third. */
  top: 68%;
  width: 60%;
  height: 20%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  color: var(--ink);
  pointer-events: none;
  transition: opacity 160ms ease;
}
.canvas-name-layer .intro-name {
  font-size: 30px;
  transform-origin: 50% 50%;
}

.stage.is-animate-envelope-playing .invite-wrap,
.stage.is-animate-envelope-playing .envelope,
.stage.is-animate-envelope-playing .envelope-pocket {
  opacity: 0 !important;
  visibility: hidden !important;
  pointer-events: none !important;
}

/* Once the canvas hero has opened, retire the legacy SVG envelope: the canvas
   already showed the envelope, and its open flap renders mirrored. Only set on
   the canvas-success path, so the SVG fallback envelope is left untouched. */
.stage.is-canvas-opened .envelope,
.stage.is-canvas-opened .envelope-pocket {
  opacity: 0 !important;
  visibility: hidden !important;
  pointer-events: none !important;
}

.stage.is-js-opening-panels .panel,
.stage.is-js-opening-panels .side.front .panel:first-child,
.stage.is-js-opening-panels .panel:not(:first-child) {
  animation: none !important;
}

.invite-wrap {
  position: absolute;
  left: 50%;
  top: var(--stage-top);
  width: min(94vw, 560px);
  z-index: 8;
  opacity: 0;
  transform: translateX(-50%) translateY(96px) scale(0.64);
  transform-origin: 50% 0%;
  perspective: 1800px;
  pointer-events: none;
}

.stage.is-open .invite-wrap {
  position: relative;
  left: auto;
  top: auto;
  margin: 0 auto;
  opacity: 1;
  transform: none;
  animation: none;
  pointer-events: auto;
}

.stage.is-resetting .invite-wrap {
  position: absolute !important;
  left: 50% !important;
  top: var(--stage-top) !important;
  margin: 0 !important;
  opacity: 0 !important;
  transform: translateX(-50%) translateY(96px) scale(0.64) !important;
  pointer-events: none !important;
}

.stage.is-opening .invite {
  pointer-events: none;
}

.stage.is-lifted .invite-wrap {
  z-index: 14;
}

.invite {
  position: relative;
  display: block;
  transform-origin: 50% 0%;
  opacity: 1;
  transform: none;
  cursor: pointer;
}

.invite.is-turning {
  pointer-events: none;
}

.stage.is-open .invite {
  animation: none;
}

.side {
  display: block;
  transition: none;
  backface-visibility: hidden;
  /* Give the panel rotateX folds real depth: without a perspective on the
     panels' container the fold renders as a flat vertical squash, which reads
     as janky. A perspective here makes each panel foreshorten as it hinges
     from its top edge, like paper in a concertina. */
  perspective: 1400px;
  transform-style: flat;
  visibility: visible;
}

.side.back {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: none;
  visibility: hidden;
}

.invite.show-back .side.front {
  opacity: 0;
  transform: none;
  visibility: hidden;
}

.invite.show-back .side.back {
  opacity: 1;
  transform: none;
  visibility: visible;
}

.panel {
  position: relative;
  width: 100%;
  aspect-ratio: 148 / 105;
  min-height: 0;
  padding: clamp(16px, 4.8vw, 28px);
  background: var(--paper);
  color: var(--ink);
  border: 0;
  clip-path: none;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.18);
  transform-origin: top center;
  opacity: 0;
  transform: rotateX(-88deg);
  /* Promote panels to their own compositor layer so the fold animates on the
     GPU and stays smooth. */
  will-change: transform, opacity;
  overflow: hidden;
}

.panel + .panel {
  margin-top: -1.5px;
}

.panel:not(.hero) {
  background:
    linear-gradient(rgba(255, 250, 244, 0.9), rgba(255, 250, 244, 0.9)),
    var(--nettuno-paper) center / 230px auto repeat,
    var(--paper);
  background-blend-mode: normal, multiply, normal;
}

.stage.is-open .side.front .panel:first-child {
  opacity: 1;
  transform: rotateX(0deg);
  animation: coverSettle 1250ms 900ms cubic-bezier(.18, .78, .2, 1) both;
}

.stage.is-open .side.back .panel:first-child {
  opacity: 1;
  transform: rotateX(0deg);
}

.stage.is-open .panel:not(:first-child) {
  animation: unfold 900ms cubic-bezier(.18, .74, .22, 1) both;
}

.stage.is-open .panel:nth-child(2) { animation-delay: 2320ms; }
.stage.is-open .panel:nth-child(3) { animation-delay: 2820ms; }
.stage.is-open .panel:nth-child(4) { animation-delay: 3320ms; }

.panel::before,
.panel::after {
  content: "";
  position: absolute;
  width: 112px;
  height: 74px;
  background:
    radial-gradient(circle at 24px 32px, rgba(230, 167, 194, 0.55) 0 16px, transparent 17px),
    radial-gradient(circle at 58px 26px, rgba(246, 210, 221, 0.75) 0 18px, transparent 19px),
    radial-gradient(circle at 78px 44px, rgba(255, 244, 248, 0.9) 0 16px, transparent 17px);
  opacity: 0.82;
  pointer-events: none;
}

.panel::before { left: -24px; top: -20px; transform: rotate(-12deg); }
.panel::after { right: -28px; bottom: -18px; transform: rotate(14deg); }

.hero {
  padding: 0;
  background:
    url("/assets/image/cover-panel.png") center / cover no-repeat;
  color: var(--paper);
}

.hero::before,
.hero::after {
  display: none;
}

h1, h2, p { margin: 0; }

body.invite-locked {
  overflow: hidden;
}

body.invite-locked .stage,
body.invite-locked .actions,
body.invite-locked .hint,
body.invite-locked .scroll-shortcuts {
  display: none;
}

.password-gate {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  padding: 24px;
  background:
    radial-gradient(circle at 50% 10%, rgba(54, 92, 156, 0.82), transparent 42%),
    linear-gradient(180deg, #102a5b, #08142d 54%, #050b19);
}

/* Starfield for the gate — same dots as the invite page but on larger, more
   varied tiles so the repeat never reads as a grid on big empty screens. */
.password-gate::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(circle at 11% 17%, rgba(255, 255, 255, 0.9) 0 1px, transparent 1.8px),
    radial-gradient(circle at 27% 64%, rgba(255, 250, 244, 0.74) 0 1px, transparent 1.7px),
    radial-gradient(circle at 71% 23%, rgba(239, 189, 208, 0.72) 0 1px, transparent 1.8px),
    radial-gradient(circle at 87% 79%, rgba(255, 255, 255, 0.82) 0 1px, transparent 1.8px),
    radial-gradient(circle at 44% 41%, rgba(255, 250, 244, 0.88) 0 1.2px, transparent 2px),
    radial-gradient(circle at 61% 88%, rgba(255, 255, 255, 0.68) 0 1px, transparent 1.7px);
  background-size: 337px 411px, 433px 337px, 521px 479px, 613px 547px, 379px 587px, 467px 353px;
  animation: twinkle 5.8s ease-in-out infinite alternate;
}

.password-card {
  position: relative;
  z-index: 1;
}

.password-gate[hidden] {
  display: none;
}

.password-card {
  width: min(92vw, 380px);
  padding: 24px 26px 18px;
  border: 1px solid rgba(255, 250, 244, 0.2);
  border-radius: 8px;
  background:
    linear-gradient(rgba(255, 250, 244, 0.92), rgba(255, 250, 244, 0.92)),
    var(--nettuno-paper) center / 240px auto repeat,
    var(--paper);
  color: var(--ink);
  text-align: center;
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.36);
}

.password-card h1 {
  margin: 0 0 8px;
  font: 700 20px/1.15 "Moon Invite", "Trebuchet MS", sans-serif;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.password-card p {
  margin: 0 0 10px;
  font: 14px/1.35 "Fraunces Invite", Georgia, serif;
  color: var(--muted);
}

.password-card .password-hint {
  margin-top: -4px;
  font-size: 12px;
  color: rgba(23, 36, 82, 0.62);
}

.password-form {
  display: grid;
  gap: 10px;
}

.password-field {
  position: relative;
  width: 100%;
}

/* Show/hide password eye, inline at the right of the box. The selector
   carries .password-form to beat the generic .password-form button rule
   (which paints the navy submit pill). */
.password-form .password-toggle {
  position: absolute;
  top: 50%;
  right: 8px;
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: #767c96;
  cursor: pointer;
}
.password-form .password-toggle {
  width: 34px;
  border: 0;
  border-radius: 50%;
  padding: 0;
  background: transparent;
  color: #767c96;
  box-shadow: none;
}
.password-form .password-toggle:hover { color: var(--ink); }
.password-form .password-toggle .eye { width: 20px; height: 20px; }
.password-form .password-toggle .eye[hidden] { display: none; }

/* Edge/IE draw their own reveal eye — ours replaces it. */
#passwordInput::-ms-reveal,
#passwordInput::-ms-clear { display: none; }

.password-form input {
  width: 100%;
  border: 1px solid rgba(23, 36, 82, 0.24);
  border-radius: 999px;
  padding: 13px 44px;
  color: var(--ink);
  background: rgba(255, 255, 255, 0.76);
  font: 600 16px/1 system-ui, sans-serif;
  text-align: center;
}

.password-form input:focus {
  outline: 3px solid rgba(230, 167, 194, 0.58);
  outline-offset: 2px;
}

.password-form button {
  border: 0;
  border-radius: 999px;
  padding: 13px 16px;
  color: var(--paper);
  background: var(--ink);
  font: 800 14px/1.1 system-ui, sans-serif;
  cursor: pointer;
}

.password-form button:disabled,
.password-form input:disabled {
  cursor: not-allowed;
  opacity: 0.58;
}

.password-error:empty { display: none; }

.password-error {
  min-height: 18px;
  margin: 0;
  color: #b84d73;
  font: 700 13px/1.35 system-ui, sans-serif;
  white-space: pre-line;
}

.password-error:empty {
  display: none;
}

.guest-confirmation[hidden],
.password-entry[hidden] {
  display: none;
}

.guest-confirmation h1 {
  margin-bottom: 18px;
  letter-spacing: 0;
  text-transform: none;
}

.confirmation-actions {
  display: grid;
  gap: 10px;
}

.confirmation-actions button,
.confirmation-actions a {
  border: 0;
  border-radius: 999px;
  padding: 13px 16px;
  font: 800 14px/1.1 system-ui, sans-serif;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}

.confirmation-actions button {
  color: var(--paper);
  background: var(--ink);
}

.confirmation-actions a {
  color: var(--ink);
  background: rgba(23, 36, 82, 0.08);
  border: 1px solid rgba(23, 36, 82, 0.18);
}

.panel-title {
  font: 700 clamp(15px, 4.2vw, 22px)/1.05 "Moon Invite", "Trebuchet MS", sans-serif;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink);
  text-align: center;
}

.main-invite {
  display: grid;
  align-content: center;
  gap: clamp(7px, 2vw, 14px);
  text-align: center;
}

.small {
  font: 600 clamp(9px, 2.4vw, 13px)/1.35 "Moon Invite", "Trebuchet MS", sans-serif;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

.invite-copy {
  font: clamp(9px, 2.45vw, 15px)/1.35 "Fraunces Invite", Georgia, serif;
  color: var(--muted);
}

.invite-copy strong {
  color: var(--ink);
  font-weight: 700;
}

.names {
  font: 600 clamp(16px, 4.7vw, 32px)/1.12 "Moon Invite", "Trebuchet MS", sans-serif;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
}

.name-svg {
  display: block;
  width: min(92%, 260px);
  max-height: 118px;
  margin: 0 auto;
  object-fit: contain;
}

.date {
  font: 700 clamp(10px, 2.6vw, 15px)/1.25 "Fraunces Invite", Georgia, serif;
  color: var(--ink);
}

.rule {
  width: 64%;
  height: 2px;
  margin: 0 auto;
  background: linear-gradient(90deg, transparent, var(--pink-line), transparent);
}

.two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(8px, 2.2vw, 14px);
  font: clamp(8px, 2.25vw, 13px)/1.35 "Fraunces Invite", Georgia, serif;
  color: var(--muted);
}

.rsvp {
  display: grid;
  align-content: center;
  gap: clamp(8px, 2vw, 14px);
}

.qr {
  float: left;
  width: 62px;
  height: 62px;
  margin: 2px 14px 8px 0;
  background:
    linear-gradient(90deg, #b84d73 50%, transparent 0) 0 0/12px 12px,
    linear-gradient(#b84d73 50%, transparent 0) 0 0/12px 12px,
    #fff;
  border: 4px solid #fff;
  outline: 1px solid #e5a8bf;
}

.copy {
  font: clamp(8px, 2.15vw, 13px)/1.32 "Fraunces Invite", Georgia, serif;
  color: var(--muted);
}

.qr-image {
  width: clamp(76px, 22vw, 116px);
  height: clamp(76px, 22vw, 116px);
  object-fit: contain;
  justify-self: center;
}

.details {
  display: grid;
  align-content: center;
  gap: clamp(5px, 1.5vw, 11px);
  text-align: center;
}

.details p {
  font: clamp(8px, 2.08vw, 13px)/1.3 "Fraunces Invite", Georgia, serif;
  color: var(--muted);
}

.parking-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
  margin-top: 8px;
  text-align: left;
}

.parking-grid p {
  font-size: clamp(7.5px, 2vw, 12px);
}

/* ===== Full reverse side as real, selectable text =====
   Each panel carries one .bcard — a flex column pinned to the artwork's white
   card (bounds measured from reverse-side.webp) with a consistent inner
   margin, so every section shares the same breathing room. Font sizes are vw
   clamps derived from the 420px design width (px/4.2 = vw, cap = px*460/420,
   matching the stage's 460px width cap). #parkingLine is filled dynamically
   by invite-data.js. Icon + divider artwork are background-image slots that
   render nothing until the files are exported (see .bicon-* below).

   NB: selectors carry `.panel` to out-specify the shared artwork rule
   `.side[data-figma-ready] .panel > :not(.figma-art)` (0,4,0, defined later)
   which forces position:relative + opacity:0 on panel children. */
.side.back .bcard { display: none; }

.side.back[data-figma-ready="true"] .panel .bcard {
  --bgap: clamp(4px, 1.5vw, 7px);
  position: absolute;
  /* White card bounds for back-2/3/4 (back-1's card is taller, below). Top/
     bottom insets trimmed from the original 10.3%/11.4% — real guest copy
     runs longer than the design draft, and the extra room here (plus the
     tighter gap/line-height below) is what keeps it inside its own card
     instead of bleeding into the panel above/below on mobile. */
  inset: 5% 7% 6% 7%;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* flex-start (not center): if content still slightly overruns its box on
     a narrow phone, it overflows downward only — never upward into the
     previous panel. */
  justify-content: flex-start;
  gap: var(--bgap);
  padding: 4% 6%;
  margin: 0;
  text-align: center;
  pointer-events: none;
  opacity: 1;
}

.side.back[data-figma-ready="true"] .panel[data-figma-panel="back-1"] .bcard {
  inset: 4% 7% 8% 7%;
}

.side.back .bcard p { margin: 0; }

.bcard .bhead {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.45em;
  font-size: clamp(24px, 9.13vw, 42px);
}

/* Icon slots — hand-drawn icons from Claire's InDesign export, rasterised to
   /assets/figma/icons/. Hidden (zero width) until invite-data.js probes the
   file and tags the span .is-ready, so a missing icon never leaves an empty
   box pushing the title off-centre. Each keeps its artwork's own aspect
   ratio, sized against the title's cap height. */
.bicon {
  flex: 0 0 auto;
  display: none;
  height: 1.05em;
  width: auto;
  background: center / contain no-repeat;
}
.bicon.is-ready { display: block; }
.bicon-church { aspect-ratio: 226 / 160; background-image: url("/assets/figma/icons/icon-church.png"); }
.bicon-reception { aspect-ratio: 172 / 160; background-image: url("/assets/figma/icons/icon-reception.png"); }
.bicon-attire { aspect-ratio: 148 / 176; background-image: url("/assets/figma/icons/icon-attire.png"); }
.bicon-parking { aspect-ratio: 164 / 160; background-image: url("/assets/figma/icons/icon-parking.png"); }

.bcard .btitle {
  font-family: "Envelove Caps", "Moon Invite", serif;
  font-size: 1em;
  line-height: 1;
  color: #303880;
  letter-spacing: 0.07em;
  line-height: 1.2;
  /* Hairline same-colour stroke recreates the design's faux-bold titles. */
  -webkit-text-stroke: 0.55px currentColor;
  /* The design uses the font's plain capitals; ligatures/contextual
     alternates were swapping in alternate glyphs (wrong K in PARKING). */
  font-feature-settings: "liga" 0, "clig" 0, "dlig" 0, "calt" 0;
  text-wrap: balance;
}

.bcard .bsub {
  font-family: "Moon Invite", sans-serif;
  font-weight: 700;
  font-size: clamp(10px, 3.1vw, 14.3px);
  line-height: 1.2;
  color: #db94b6;
  letter-spacing: 0.16em;
  -webkit-text-stroke: 0.3px currentColor;
  text-wrap: balance;
}

.bcard .bp {
  font-family: "Fraunces Invite", Georgia, serif;
  font-size: clamp(8.6px, 2.7vw, 13.4px);
  line-height: 1.16;
  color: #303880;
  letter-spacing: 0.02em;
  max-width: 100%;
  overflow-wrap: break-word;
  /* `pretty` avoids widows where supported; a JS guard glues the last two
     words with a no-break space everywhere else. */
  text-wrap: pretty;
}
.bcard .bp strong,
.bcard .bp-strong { font-weight: 600; }
.bcard .bp-balance { text-wrap: balance; }
.bcard .bp-date {
  font-size: clamp(12px, 4.05vw, 18.6px);
  line-height: 1.2;
  color: #cc5080;
}
.bcard .bp-pink-moon {
  font-family: "Moon Invite", sans-serif;
  font-weight: 400;
  font-size: clamp(7.5px, 2.2vw, 10.9px);
  line-height: 1.22;
  color: #cc5080;
  letter-spacing: 0.1em;
}

/* Two-column attire block — each column centre-aligned within itself. */
.bcard .bcols {
  display: flex;
  gap: 6%;
  width: 100%;
  align-items: flex-start;
}
.bcard .bcol {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--bgap);
}

/* Hearts divider slot — same probe-gated reveal as the icons. */
.bcard .bdivider {
  display: none;
  width: 56%;
  height: clamp(10px, 3.6vw, 16.6px);
  background: center / contain no-repeat url("/assets/figma/icons/divider-hearts.png");
}
.bcard .bdivider.is-ready { display: block; }

.envelope,
.envelope-pocket {
  position: absolute;
  left: 50%;
  top: var(--stage-top);
  width: var(--envelope-width);
  aspect-ratio: var(--envelope-ratio);
  height: auto;
  opacity: 0;
  transform: translateX(-50%);
  transform-style: preserve-3d;
  perspective: 900px;
  filter: drop-shadow(0 20px 30px rgba(0, 0, 0, 0.26));
  pointer-events: none;
}

.envelope {
  z-index: 6;
}

.envelope-pocket {
  z-index: 12;
  filter: none;
}

.stage.is-open .envelope,
.stage.is-open .envelope-pocket {
  opacity: 1;
  animation: none;
}

.env-art {
  position: absolute;
  left: 50%;
  display: block;
  height: auto;
  pointer-events: none;
  transform: translateX(-50%);
}

.env-back {
  z-index: 1;
  top: 0;
  width: 99.69%;
}

.env-flap {
  z-index: 2;
  width: 99.69%;
}

.env-flap-closed {
  top: 0;
  opacity: 1;
  transform-origin: 50% 0%;
}

.env-flap-open {
  top: 0;
  opacity: 0;
  transform-origin: 50% 100%;
  transform: translateX(-50%) translateY(18%) scaleY(0.72);
}

.stage.is-open .env-flap-closed {
  animation: flapClosedOut 520ms 120ms cubic-bezier(.2, .72, .25, 1) both;
}

.stage.is-open .env-flap-open {
  animation: flapOpenIn 620ms 180ms steps(4, end) both;
}

.env-front {
  z-index: 4;
  top: 0;
  width: 100%;
}

.seal {
  display: none;
}

/* Fixed bottom action footer. Spans the full width, sits above all content,
   and is always visible once the invitation experience is unlocked (it is
   hidden together with the rest while body.invite-locked covers the password /
   guest-confirmation modal; see the body.invite-locked rule). */
.actions {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 10px 16px;
  /* Respect the mobile home-indicator / safe area. */
  padding-bottom: calc(10px + env(safe-area-inset-bottom));
  /* Semi-opaque navy so the invitation stays faintly visible underneath. */
  background: rgba(8, 20, 45, 0.72);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  border-top: 1px solid rgba(255, 250, 244, 0.12);
  transition: opacity 360ms ease, visibility 360ms ease;
}

/* The footer belongs to the reading view only — keep it hidden through the
   envelope intro/opening (it was popping in as soon as the invite unlocked). */
.stage:not(.is-open) ~ .actions {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.action-row {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 10px;
  width: 100%;
}

.button,
.replay {
  border: 1px solid rgba(239, 189, 208, 0.72);
  box-shadow: none;
}

.button {
  flex: 0 1 360px;
  border-radius: 999px;
  padding: 13px 16px;
  color: var(--midnight);
  background: #fff6ec;
  font: 800 14px/1.1 system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-align: center;
  text-decoration: none;
}

button.button {
  cursor: pointer;
}

.replay {
  flex: none;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  color: var(--paper);
  background: var(--midnight);
  cursor: pointer;
  font: 700 18px/1 system-ui, sans-serif;
}

.hint {
  margin: 0;
  width: min(88vw, 410px);
  color: rgba(255, 250, 244, 0.92);
  font: 700 clamp(15px, 4.2vw, 19px)/1.3 system-ui, sans-serif;
  letter-spacing: 0.05em;
  text-align: center;
  opacity: 0;
  transition: opacity 360ms ease;
}

.stage.is-ready ~ .actions .hint {
  opacity: 1;
}

.scroll-shortcuts {
  position: absolute;
  right: max(14px, env(safe-area-inset-right));
  bottom: calc(100% + 12px);
  z-index: 41;
  display: grid;
  gap: 10px;
}

.scroll-jump {
  display: none;
  width: 46px;
  height: 46px;
  border: 1px solid rgba(239, 189, 208, 0.72);
  border-radius: 50%;
  color: var(--paper);
  background: var(--midnight);
  cursor: pointer;
  font: 800 20px/1 system-ui, sans-serif;
}

body.show-scroll-top .scroll-top,
body.show-scroll-bottom .scroll-bottom {
  display: block;
}

.intro {
  position: absolute;
  inset: 0;
  z-index: 30;
  display: grid;
  justify-items: center;
  align-content: start;
  padding: 28px;
  padding-top: var(--stage-top);
  background: transparent;
  cursor: pointer;
  transition: opacity 420ms ease, transform 420ms ease, visibility 420ms ease;
}

/* .intro is a full-bleed (inset:0) tap target that gets programmatic focus
   after the password/guest step. Its default focus ring therefore frames the
   whole stage as an intermittent white box. Suppress it — the visible "Tap to
   turn your invite over" CTA is the affordance, and Enter/Space activation is
   handled in JS, so keyboard operability is preserved. */
.intro:focus,
.intro:focus-visible {
  outline: none;
}

.stage.is-open .intro {
  opacity: 0;
  transform: translateY(-18px) scale(0.98);
  visibility: hidden;
  pointer-events: none;
}

.intro-envelope {
  position: relative;
  width: var(--envelope-width);
  aspect-ratio: var(--envelope-ratio);
  filter: drop-shadow(0 24px 42px rgba(0, 0, 0, 0.34));
  animation: waitingShake 850ms 5s ease-in-out infinite;
  transform-style: preserve-3d;
  perspective: 1200px;
}

.intro-envelope.is-flipping {
  animation: none;
}

.intro-envelope.is-flipped {
  animation: none;
}

.intro-side {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  transform-style: preserve-3d;
}

.intro-side.front-side {
  opacity: 1;
  transform: rotateY(0deg);
}

.intro-side.back-side {
  opacity: 0;
  transform: rotateY(86deg);
}

.intro-envelope.is-flipping .front-side {
  animation: envelopeFrontOut 360ms cubic-bezier(.36, 0, .24, 1) both;
}

.intro-envelope.is-flipping .back-side {
  animation: envelopeBackIn 420ms 300ms cubic-bezier(.2, .72, .25, 1) both;
}

.intro-envelope.is-flipped .front-side {
  opacity: 0;
  transform: rotateY(-86deg);
}

.intro-envelope.is-flipped .back-side {
  opacity: 1;
  transform: rotateY(0deg);
}

.intro-art {
  position: absolute;
  left: 50%;
  display: block;
  height: auto;
  pointer-events: none;
  transform: translateX(-50%);
}

.front-side .intro-front,
.back-side .intro-front {
  top: 0;
  width: 100%;
}

.back-side .intro-back {
  top: 0;
  z-index: 1;
  width: 99.69%;
}

.back-side .intro-flap {
  top: 0;
  z-index: 2;
  width: 99.69%;
}

.back-side .intro-front {
  z-index: 3;
}

.intro-stamp,
.intro-return {
  display: none;
}

.intro-address {
  position: absolute;
  left: 50%;
  top: 43%;
  z-index: 4;
  width: min(78%, 280px);
  transform: translateX(-50%);
  display: grid;
  gap: 10px;
  justify-items: center;
  color: var(--ink);
  text-align: center;
}

.intro-side.back-side .intro-address,
.intro-side.back-side .intro-stamp,
.intro-side.back-side .intro-return {
  display: none;
}

.intro-label {
  font: 700 11px/1 "Moon Invite", "Trebuchet MS", sans-serif;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(23, 36, 82, 0.62);
}

.intro-name {
  width: 100%;
  max-width: 100%;
  margin: 0;
  font: 400 clamp(30px, 7.8vw, 46px)/1.08 "CL Handwriting", "Moon Invite", "Trebuchet MS", sans-serif;
  letter-spacing: 0;
  text-transform: none;
  white-space: normal;
  overflow-wrap: normal;
  word-break: normal;
  text-wrap: balance;
  font-variant-ligatures: common-ligatures discretionary-ligatures;
  font-feature-settings: "liga" 1, "dlig" 1, "calt" 1;
}

.intro-name-svg {
  display: block;
  width: 100%;
  max-height: 82px;
  object-fit: contain;
}

.intro-cta {
  margin-top: 24px;
  width: min(82%, 330px);
  color: rgba(255, 250, 244, 0.88);
  font: 600 14px/1.35 system-ui, sans-serif;
  text-align: center;
}

/* Figma artwork handoff scaffold.
   These anchors remain hidden until the final front and reverse exports exist. */
.figma-art {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

.figma-art[hidden] {
  display: none;
}

.figma-art::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 400%;
  background-image: var(--figma-side-art, none);
  background-position: center;
  background-repeat: no-repeat;
  background-size: 100% 100%;
}

.panel:nth-child(1) .figma-art::before { top: 0; }
.panel:nth-child(2) .figma-art::before { top: -100%; }
.panel:nth-child(3) .figma-art::before { top: -200%; }
.panel:nth-child(4) .figma-art::before { top: -300%; }

.side[data-figma-ready="true"] .panel > :not(.figma-art) {
  position: relative;
  z-index: 1;
}

/* Finished Figma artwork activation. */
.side.front[data-figma-ready="true"] {
  --figma-side-art: url("/assets/figma/front-side.svg");
}

.side.back[data-figma-ready="true"] {
  --figma-side-art: url("/assets/figma/reverse-sideLIVE.webp");
}

/* TEMPORARY: bake the canvas Invite_Placeholder art (cropped from the D2 atlas)
   onto the hero panel so the DOM invite's first section matches the card the
   canvas hands out — until front-side.svg's hero section is re-exported. */
.side.front[data-figma-ready="true"] .panel.hero .figma-art::before {
  top: 0;
  height: 100%;
  background-image: url("/assets/figma/hero-placeholder.png");
  background-size: cover;
  background-position: center top;
}

.side[data-figma-ready="true"] .figma-art {
  display: block;
}

.side[data-figma-ready="true"] .panel {
  background: transparent;
  box-shadow: none;
}

.side[data-figma-ready="true"] .panel + .panel {
  margin-top: 0;
}

.side[data-figma-ready="true"] .panel::before,
.side[data-figma-ready="true"] .panel::after {
  display: none;
}

.side[data-figma-ready="true"] .panel > :not(.figma-art) {
  opacity: 0;
  pointer-events: none;
}

/* Fixed height (not min-height) so the name-fit measures a layout-constant
   box — with min-height the slot grew with the name's own font-size, feeding
   the fitter its previous result and making sizes drift run-to-run. The name
   row (1fr) and the celebrate line (auto) are separate grid rows so the
   celebrate copy no longer eats the name's fitting budget. */
.side.front[data-figma-ready="true"] .main-invite > .invite-copy:first-of-type {
  position: absolute;
  left: 8%;
  right: 8%;
  top: 39.7%;
  z-index: 2;
  display: grid;
  grid-template-rows: 1fr auto;
  height: 20%;
  align-items: center;
  justify-items: center;
  opacity: 1;
  padding: 1.5% 3% 2%;
  background: #fff;
  color: #e25b8a;
  text-align: center;
  pointer-events: none;
}

.side.front[data-figma-ready="true"] #invitationGuestName {
  display: block;
  color: #e25b8a;
  font: 400 clamp(12px, 3.1vw, 17px)/1.08 "CL Handwriting", "Moon Invite", sans-serif;
  width: 100%;
  max-width: 100%;
  letter-spacing: 0;
  text-transform: none;
  white-space: normal;
  overflow-wrap: normal;
  word-break: normal;
  text-wrap: balance;
}

/* Real-text line missing from the baked front-side.svg: "to celebrate the
   marriage of their daughter," — sits inside the household box, directly below
   the name so it always stays with it (regardless of name length). Fraunces
   navy, centred. */
/* Sized to visually match the baked "request the pleasure of" line above the
   name (same family/weight, ~13px at the 420 design width), per Claire. */
.side.front[data-figma-ready="true"] .main-invite .invite-celebrate {
  display: block;
  margin-top: 0.2em;
  color: #2f3780;
  font: 400 clamp(10px, 3.1vw, 14.3px)/1.3 "Fraunces Invite", Georgia, serif;
  letter-spacing: 0.02em;
}

@keyframes rise {
  0%, 100% { opacity: 1; transform: none; }
}

@keyframes coverSettle {
  0%, 44% {
    opacity: 1;
    transform: rotateX(0deg);
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
  }
  100% {
    opacity: 1;
    transform: rotateX(0deg);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.18);
  }
}

@keyframes unfold {
  0% { opacity: 0; transform: rotateX(-88deg); }
  18% { opacity: 1; }
  100% { opacity: 1; transform: rotateX(0deg); }
}

@keyframes flapClosedOut {
  0% { opacity: 1; transform: translateX(-50%) translateY(0) scaleY(1); }
  100% { opacity: 0; transform: translateX(-50%) translateY(-4%) scaleY(0.92); }
}

@keyframes flapOpenIn {
  0% { opacity: 0; transform: translateX(-50%) translateY(18%) scaleY(0.72); }
  25% { opacity: 1; transform: translateX(-50%) translateY(12%) scaleY(0.82); }
  50% { opacity: 1; transform: translateX(-50%) translateY(7%) scaleY(0.9); }
  75% { opacity: 1; transform: translateX(-50%) translateY(3%) scaleY(0.96); }
  100% { opacity: 1; transform: translateX(-50%) translateY(0) scaleY(1); }
}

@keyframes envelopeFade {
  0% { opacity: 1; transform: translateX(-50%) translateY(0); }
  100% { opacity: 0; transform: translateX(-50%) translateY(54px); }
}

@keyframes actionsIn {
  to { opacity: 1; transform: translateX(-50%) translateY(0); pointer-events: auto; }
}

@keyframes waitingShake {
  0%, 100% { transform: rotate(0deg) translateY(0); }
  18% { transform: rotate(-1.3deg) translateY(-1px); }
  36% { transform: rotate(1.1deg) translateY(1px); }
  54% { transform: rotate(-0.8deg) translateY(0); }
  72% { transform: rotate(0.5deg) translateY(-1px); }
}

@keyframes twinkle {
  0% { opacity: 0.42; transform: translateY(0); }
  48% { opacity: 0.72; }
  100% { opacity: 0.52; transform: translateY(-8px); }
}

@keyframes fireflyDrift {
  0% {
    transform: translate3d(0, 0, 0) scale(0.78);
  }
  52% {
    transform: translate3d(var(--firefly-mid-x), var(--firefly-mid-y), 0) scale(1);
  }
  100% {
    transform: translate3d(var(--firefly-drift-x), var(--firefly-drift-y), 0) scale(0.84);
  }
}

@keyframes fireflyGlow {
  0%, 100% {
    opacity: 0.08;
  }
  18% {
    opacity: 0.68;
  }
  42% {
    opacity: 0.2;
  }
  64% {
    opacity: 0.86;
  }
  82% {
    opacity: 0.14;
  }
}


@keyframes envelopeFrontOut {
  0% { opacity: 1; transform: rotateY(0deg); }
  80% { opacity: 1; }
  100% { opacity: 0; transform: rotateY(-86deg); }
}

@keyframes envelopeBackIn {
  0% { opacity: 0; transform: rotateY(86deg); }
  18% { opacity: 1; }
  100% { opacity: 1; transform: rotateY(0deg); }
}

@media (min-width: 850px) {
  :root {
    /* Desktop variant of the shared top offset; keeps every layer aligned. */
    --stage-top: clamp(44px, 10vh, 100px);
  }

  body {
    padding: 20px;
  }

  .stage {
    width: min(96vw, 720px);
    min-height: calc(100svh - 40px);
    border-radius: 0;
  }

  .invite-wrap {
    width: min(72vw, 620px);
  }

  /* Keep the canvas end-card matched to the wider desktop invite
     (72vw*954/721 = 95.3vw, 620*954/721 = 820px). */
  .animate-envelope-scene {
    width: min(95.3vw, 820px);
  }

  .invite {
    transform-origin: 50% 100%;
  }

  .panel {
    padding: 28px;
  }

  .names {
    font-size: 24px;
  }

  /* Keep both envelope halves on identical width at >=850px (their shared top
     comes from --stage-top, so the back+flap and front pocket stay aligned). */
  .envelope,
  .envelope-pocket,
  .intro-envelope {
    width: 430px;
  }

  .action-row .scroll-shortcuts {
    position: static;
    grid-auto-flow: column;
    flex: none;
  }

  @keyframes rise {
    0%, 100% { opacity: 1; transform: none; }
  }
}

@media (prefers-reduced-motion: reduce) {
  body::before,
  body::after,
  .firefly {
    animation: none;
  }

  .firefly-layer {
    display: none;
  }
}

/* Best-effort save/drag deterrent for invite artwork — not real protection,
   just raises the bar past a plain right-click. */
img,
svg,
canvas {
  -webkit-user-drag: none;
  user-drag: none;
  -webkit-touch-callout: none;
}
