/* ============================================================
   Carla & Siobhan — shared styles
   ============================================================ */

:root {
  /* Brand gradient — the "flame" ramp */
  --flame-pink:   #fd297b;
  --flame-coral:  #ff5864;
  --flame-orange: #ff7854;
  --flame: linear-gradient(100deg, var(--flame-pink) 0%, var(--flame-coral) 55%, var(--flame-orange) 100%);

  /* Surfaces */
  --ink:        #12100e;
  --ink-soft:   #1c1917;
  --paper:      #ffffff;
  --paper-dim:  rgba(255, 255, 255, 0.72);
  --paper-faint:rgba(255, 255, 255, 0.45);
  --hairline:   rgba(255, 255, 255, 0.18);
  --danger:     #ffb3b3;

  /* Type */
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;

  /* Rhythm */
  --gutter: 24px;
  --radius-pill: 999px;
}

*, *::before, *::after { box-sizing: border-box; }

/* Visible to screen readers only. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

::selection {
  background: rgba(253, 41, 123, 0.55);
  color: #fff;
}

html, body {
  height: 100%;
}

body {
  margin: 0;
  font-family: var(--font);
  background-color: var(--ink);
  color: var(--paper);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ============================================================
   Background collage
   ------------------------------------------------------------
   A grid of tiles behind everything. Each tile layers an
   optional photo (--photo) on top of a warm gradient, so if no
   photo is supplied the gradient shows through untouched.
   ============================================================ */

.collage {
  position: fixed;
  inset: -6%;                 /* overscan so the drift never reveals an edge */
  z-index: 0;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: 1fr;
  gap: 6px;
  filter: saturate(0.85);
}

@media (min-width: 640px)  { .collage { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 960px)  { .collage { grid-template-columns: repeat(4, 1fr); } }

/* A tile with no background-image renders nothing at all, so the
   default state is a clean dark field rather than a grid of grey
   boxes. The image is set inline in the HTML (see note below). */
.collage__tile {
  background-size: cover;
  background-position: center;
  border-radius: 10px;
  animation: drift 22s ease-in-out infinite alternate;
}

/* Stagger the drift so the grid never pulses in unison. */
.collage__tile:nth-child(4n+1) { animation-delay: -0s;  }
.collage__tile:nth-child(4n+2) { animation-delay: -5s;  }
.collage__tile:nth-child(4n+3) { animation-delay: -11s; }
.collage__tile:nth-child(4n+4) { animation-delay: -16s; }

@keyframes drift {
  from { transform: scale(1)    translate3d(0, 0, 0); }
  to   { transform: scale(1.06) translate3d(0, -1.5%, 0); }
}

/* The wash over the collage. Two warm blooms (top and bottom)
   give the empty state some depth, then a dark linear gradient
   guarantees white text stays readable once photos are added. */
.veil {
  position: fixed;
  inset: 0;
  z-index: 1;
  background:
    radial-gradient(110% 62% at 50% 6%,   rgba(253, 41, 123, 0.22) 0%, transparent 62%),
    radial-gradient(100% 55% at 50% 104%, rgba(255, 120, 84, 0.16) 0%, transparent 62%),
    linear-gradient(180deg, rgba(18, 16, 14, 0.58) 0%, rgba(18, 16, 14, 0.80) 45%, rgba(18, 16, 14, 0.93) 100%);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}

/* ============================================================
   Login layout
   ============================================================ */

.login {
  position: relative;
  z-index: 2;
  min-height: 100svh;         /* svh keeps mobile browser chrome from clipping */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  gap: 32px;
  padding:
    calc(48px + env(safe-area-inset-top)) var(--gutter)
    calc(32px + env(safe-area-inset-bottom));
  max-width: 460px;
  margin-inline: auto;
  text-align: center;
}

/* --- Brand block --- */

.brand {
  margin-top: auto;
  margin-bottom: auto;
  animation: rise 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

/* On wider screens there's no thumb to reach for, so centre the
   whole composition instead of leaving a void in the middle.
   This must come AFTER the .brand rule above — a media query adds
   no specificity, so it only wins by being later in the file. */
@media (min-width: 640px) {
  .login { justify-content: center; gap: 56px; }
  .brand { margin-block: 0; }
}

/* The logo file carries symmetric padding inside its viewBox, so
   size by height and let width follow — the visible flame lands
   at roughly two-thirds of the box height. */
.brand__flame {
  height: 104px;
  width: auto;
  display: block;
  margin: 0 auto 14px;
  filter: drop-shadow(0 8px 26px rgba(0, 0, 0, 0.45));
}

.brand__wordmark {
  margin: 0;
  /* Capitals are wider than lowercase and want air rather than the
     tight negative tracking a lowercase wordmark needs, so the size
     and the letter-spacing both change with the case. */
  font-size: clamp(1.75rem, 7.8vw, 2.7rem);
  font-weight: 800;
  letter-spacing: 0.005em;
  line-height: 1.05;
  text-transform: uppercase;
}

.brand__amp {
  background: var(--flame);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  font-style: italic;
}

.brand__tagline {
  margin: 14px 0 0;
  font-size: 1rem;
  line-height: 1.5;
  color: var(--paper-dim);
}

/* --- Gate (the form block at the bottom) --- */

.gate {
  width: 100%;
  animation: rise 700ms 120ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

.gate__legal {
  margin: 0 0 18px;
  font-size: 0.78rem;
  line-height: 1.55;
  color: var(--paper-faint);
  text-wrap: balance;
}

.gate__legal em { color: var(--paper-dim); font-style: italic; }

.gate__form {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* --- Field --- */

.field__label {
  /* Visually hidden but still read out by screen readers. */
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.field__input {
  width: 100%;
  height: 54px;
  padding: 0 22px;
  font: inherit;
  font-size: 1rem;
  color: var(--paper);
  text-align: center;
  background: rgba(255, 255, 255, 0.07);
  border: 1.5px solid var(--hairline);
  border-radius: var(--radius-pill);
  outline: none;
  transition: border-color 160ms ease, background-color 160ms ease;
}

.field__input::placeholder { color: rgba(255, 255, 255, 0.38); }

.field__input:hover { background: rgba(255, 255, 255, 0.10); }

.field__input:focus-visible {
  border-color: var(--flame-coral);
  background: rgba(255, 255, 255, 0.12);
  box-shadow: 0 0 0 4px rgba(255, 88, 100, 0.22);
}

/* --- Button --- */

.btn {
  position: relative;
  height: 54px;
  border: 0;
  border-radius: var(--radius-pill);
  font: inherit;
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--paper);
  cursor: pointer;
  transition: transform 140ms ease, box-shadow 200ms ease, filter 160ms ease;
}

/* .btn is used on both <button> and <a>; anchors need the underline
   removed and their label centred explicitly. */
a.btn {
  display: grid;
  place-items: center;
  text-decoration: none;
}

.btn--flame {
  background-image: var(--flame);
  box-shadow: 0 8px 26px rgba(253, 41, 123, 0.38);
}

.btn--flame:hover:not(:disabled) {
  filter: brightness(1.07);
  box-shadow: 0 10px 32px rgba(253, 41, 123, 0.5);
}

.btn:active:not(:disabled) { transform: scale(0.975); }

.btn:focus-visible {
  outline: 2px solid var(--paper);
  outline-offset: 3px;
}

.btn:disabled { cursor: default; filter: saturate(0.7) brightness(0.9); }

/* Spinner swaps in for the label while we're checking the list. */
.btn__spinner {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 20px;
  height: 20px;
  border: 2.5px solid rgba(255, 255, 255, 0.35);
  border-top-color: var(--paper);
  border-radius: 50%;
  opacity: 0;
  animation: spin 700ms linear infinite;
}

.btn.is-loading .btn__label  { opacity: 0; }
.btn.is-loading .btn__spinner { opacity: 1; }

/* --- Error + help text --- */

.gate__error {
  min-height: 1.2em;
  margin: 2px 0 0;
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--danger);
  opacity: 0;
  transition: opacity 180ms ease;
}

.gate__error.is-visible { opacity: 1; }

.gate__help {
  max-width: 32ch;
  margin: 22px auto 0;
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--paper-faint);
  text-wrap: balance;
}

/* Shake the whole form on a rejected email. */
.gate__form.is-rejected { animation: shake 420ms cubic-bezier(0.36, 0.07, 0.19, 0.97); }

/* ============================================================
   "It's a Match!" screen
   ------------------------------------------------------------
   Animation timeline (all `both` fill, so reduced-motion users
   land on the finished state instantly):
     0.10s  title
     0.30s  subtitle
     0.25s  photos fly in from the sides
     0.90s  flame badge pops
     1.05s  buttons rise
   ============================================================ */

.match {
  position: relative;
  z-index: 2;
  min-height: 100svh;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: clamp(28px, 6vh, 52px);
  padding:
    calc(40px + env(safe-area-inset-top)) var(--gutter)
    calc(32px + env(safe-area-inset-bottom));
  max-width: 460px;
  margin-inline: auto;
  text-align: center;
}

/* The photos animate in from off-screen, which would otherwise let
   you scroll sideways to them. Clip on the body so the page can
   still scroll DOWN if the screen is too short for the content —
   `overflow: hidden` on .match itself would trap it. */
.page-match { overflow-x: hidden; }

/* --- Headline --- */

.match__title {
  margin: 0;
  font-family: Georgia, "Times New Roman", serif;
  font-style: italic;
  font-weight: 700;
  font-size: clamp(2.6rem, 13vw, 4rem);
  line-height: 1.05;
  letter-spacing: -0.02em;
  background: var(--flame);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: match-title 700ms 100ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

.match__sub {
  max-width: 30ch;
  margin: 14px auto 0;
  font-size: 0.95rem;
  line-height: 1.5;
  color: var(--paper-dim);
  text-wrap: balance;   /* even line lengths, no orphaned last word */
  animation: rise 600ms 300ms ease-out both;
}

/* --- The two photos --- */

.match__pair {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  /* Wide enough that the 54px flame badge sits cleanly in the gap
     instead of overlapping the two white rings. */
  gap: clamp(52px, 18vw, 78px);
}

.avatar {
  position: relative;
  width: clamp(92px, 29vw, 140px);
  aspect-ratio: 1;
  margin: 0;
  border-radius: 50%;
  border: 3px solid rgba(255, 255, 255, 0.92);
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.45);
  background: linear-gradient(150deg, #3a2b3d, #221a20);
  overflow: hidden;
}

/* Fallback layer: the initial, shown when no photo is set. */
.avatar::before {
  content: var(--initial, "");
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: clamp(2.6rem, 9vw, 3.4rem);
  font-weight: 800;
  color: rgba(255, 255, 255, 0.30);
  letter-spacing: 0.02em;
}

/* Photo layer: a real element rather than a pseudo-element, so the
   image URL can be set inline in the HTML and resolve against the
   PAGE. A url() passed through a CSS custom property resolves
   against the stylesheet instead, which silently breaks paths. */
.avatar__photo {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
}

.avatar--left  { animation: fly-in-left  760ms 250ms cubic-bezier(0.22, 1.15, 0.36, 1) both; }
.avatar--right { animation: fly-in-right 760ms 250ms cubic-bezier(0.22, 1.15, 0.36, 1) both; }

/* --- Flame badge sitting between them --- */

.match__badge {
  position: absolute;
  left: 50%;
  top: 50%;
  display: grid;
  place-items: center;
  width: 54px;
  height: 54px;
  margin: -27px 0 0 -27px;
  border-radius: 50%;
  background: var(--ink-soft);
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.92),
              0 10px 26px rgba(253, 41, 123, 0.45);
  animation: badge-pop 520ms 900ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

/* The whole panel steps back and fades as it hands over to the
   invitation, so the next screen does not just appear. */
.matchpanel.is-leaving {
  animation: match-leave 420ms cubic-bezier(0.4, 0, 1, 1) both;
}

/* Motion sensitivity: the panel still hands over, it just does not
   fly about while it does. */
@media (prefers-reduced-motion: reduce) {
  .match__title,
  .avatar--left,
  .avatar--right,
  .match__badge,
  .matchpanel.is-leaving { animation: none; }
}

.match__badge img {
  height: 26px;
  width: auto;
  display: block;
}

/* The line that points people at the wedding site, where the real
   detail lives. */
.done__note {
  margin: clamp(20px, 5vw, 28px) auto 0;
  max-width: 34ch;
  font-size: 0.86rem;
  line-height: 1.7;
  color: var(--fw-soft);
}

/* --- Actions --- */

.match__actions {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
  animation: rise 600ms 1050ms ease-out both;
}

/* The primary button is an <a> here, so re-assert the centring
   that a <button> gets for free. */
.match__actions .btn {
  display: grid;
  place-items: center;
  text-decoration: none;
}

/* Secondary CTA — still a real action, just not the loud one. */
.btn--outline {
  background: transparent;
  border: 2px solid rgba(255, 255, 255, 0.85);
  color: var(--paper);
}

.btn--outline:hover { background: rgba(255, 255, 255, 0.12); }

/* Small utility line under the buttons. */
.match__meta {
  margin: 6px 0 0;
  font-size: 0.78rem;
  color: var(--paper-faint);
}

.linkish {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--paper-dim);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

.linkish:hover { color: var(--paper); }

/* ============================================================
   App shell — the swipeable details screen
   ============================================================ */

.page-app { background: #0d0b0a; }

/* Centre the app on very tall screens instead of stretching a
   phone-shaped card to 1800px. */
.page-app {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100svh;
  min-height: 100vh;
}

/* A flex item shrinks to its content unless told otherwise. In the
   single-file build the direct child here is a view wrapper rather
   than .app, and without this the whole section collapsed to about
   250px wide on a desktop screen. */
.page-app > * { width: 100%; }

.app {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  width: 100%;
  /* Three goes at the same thing, weakest first. 100vh is wrong on a
     phone - it is the height WITHOUT browser chrome, so the buttons
     end up behind the toolbar. 100svh is right but Samsung Internet
     and older Android WebViews ignore it. --app-h is measured by
     viewport.js and is what people actually see. */
  height: min(100vh, 880px);
  height: min(100svh, 880px);
  height: min(var(--app-h, 100svh), 880px);
  max-width: 460px;
  margin-inline: auto;
  /* No padding at the top: the photo runs to the edge of the screen. */
  padding:
    env(safe-area-inset-top) clamp(10px, 3.5vw, 16px)
    calc(14px + env(safe-area-inset-bottom));
  gap: clamp(8px, 1.6vh, 12px);
}

/* --- Top bar --- */

.appbar {
  /* The journey has no navigation - it is a linear story - so the bar
     was only a logo taking a strip off the top of every photo. */
  display: none;
}

.appbar__logo {
  height: 26px;
  width: auto;
  justify-self: center;
}

.appbar__btn {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: rgba(255, 255, 255, 0.62);
  cursor: pointer;
  transition: color 140ms ease, background-color 140ms ease;
}

.appbar__btn:hover { color: var(--paper); background: rgba(255, 255, 255, 0.08); }

.appbar__btn svg {
  width: 24px;
  height: 24px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* --- Card stack --- */

.stack {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;              /* lets the stack shrink inside the flex column */
  perspective: 1000px;
}

.card {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: 16px;
  background: linear-gradient(155deg, #45303f 0%, #241a22 60%, #1a1417 100%);
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.5);
  cursor: grab;
  touch-action: pan-y;        /* let the browser keep vertical scrolling */
  user-select: none;
  /* Cards sit slightly smaller and lower the deeper they are. */
  transform: translateY(calc(var(--depth) * 10px))
             scale(calc(1 - var(--depth) * 0.04));
  transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1),
              opacity 320ms ease;
}

.card[data-depth="0"] { cursor: grab; }
.card.is-dragging { transition: none; cursor: grabbing; }

/* Cards leaving the stack. */
.card.is-gone-right { transform: translateX(140%)  rotate(22deg);  opacity: 0; }
.card.is-gone-left  { transform: translateX(-140%) rotate(-22deg); opacity: 0; }

/* Placeholder initial, covered once --photo is supplied. */
.card__initial {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: clamp(4.5rem, 22vw, 9rem);
  font-weight: 800;
  color: rgba(255, 255, 255, 0.09);
}

.card__photo {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
}

/* --- Photo carousel --- */

/* One segment per photo, across the top of the card. */
.card__bars {
  pointer-events: none;   /* decorative - never block a photo tap */
  position: absolute;
  top: 8px;
  left: 8px;
  right: 8px;
  display: flex;
  gap: 4px;
  z-index: 3;
}

.card__bar {
  flex: 1;
  height: 3px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.32);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
  transition: background-color 200ms ease;
}

.card__bar.is-on { background: #ffffff; }

/* Invisible tap targets over the photo. They sit before .card__body
   in the DOM, so the name, bio and its scrolling win in that area. */
.card__tap {
  position: absolute;
  top: 0;
  bottom: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  /* No z-index on purpose. DOM order already puts .card__body above
     these, and the body is pointer-events:none, so taps fall through
     to the photo. Raising the zones above the body would put them
     over the bio too and stop it scrolling. */
  -webkit-tap-highlight-color: transparent;
}

/* No gap between them. Two 38% zones left a dead strip down the
   middle of the photo - which is exactly where a face usually is,
   and so exactly where people tap. Tinder splits the whole width:
   left third goes back, the rest goes forward. */
.card__tap--prev { left: 0;  width: 33%; }
.card__tap--next { right: 0; width: 67%; }

.card__tap:focus-visible {
  outline: 2px solid var(--paper);
  outline-offset: -4px;
  border-radius: 12px;
}

/* Keeps the name legible over any photo. */
.card__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top,
    rgba(0, 0, 0, 0.88) 0%,
    rgba(0, 0, 0, 0.45) 26%,
    transparent 52%);
}

/* The body sits above the photo tap zones so the bio can scroll.
   That means its whole box - including the empty space around the
   name - would swallow taps meant for the photo. Making the body
   transparent to pointers and re-enabling them only on the bio
   gives the photo back every pixel except the text you scroll. */
.card__body {
  pointer-events: none;
  position: absolute;
  inset: auto 0 0 0;
  padding: clamp(14px, 4.5vw, 20px);
  text-align: left;
}

.card__name {
  margin: 0;
  font-size: clamp(1.45rem, 5.6vw, 1.85rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

.card__age {
  margin-left: 8px;
  font-size: clamp(1.1rem, 4vw, 1.35rem);
  font-weight: 400;
  color: var(--paper-dim);
}

.card__meta {
  margin: 10px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 5px;
}

.card__meta li {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.86rem;
  color: rgba(255, 255, 255, 0.85);
}

.ico {
  flex: none;
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.9;
}

/* --- Expanded card (the "i" button) --- */

.card__bio {
  pointer-events: auto;
  margin: 0;
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  font-size: 0.92rem;
  line-height: 1.6;
  color: rgba(255, 255, 255, 0.86);
  transition: max-height 320ms ease, opacity 240ms ease, margin 320ms ease;
}

.card__bio p { margin: 0 0 0.85em; }
.card__bio p:last-child { margin-bottom: 0; }

/* Section headings on the wedding card. */
.card__heading {
  margin: 1.25em 0 0.5em;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--flame-coral);
}

.card__heading:first-child { margin-top: 0; }

/* The order of events. */
.card__list {
  margin: 0 0 0.85em;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 3px;
}

.card__list li {
  padding-left: 12px;
  border-left: 2px solid rgba(253, 41, 123, 0.45);
  font-variant-numeric: tabular-nums;   /* times line up in a column */
}

.card.is-expanded { cursor: default; }

.card.is-expanded .card__bio {
  /* The body is anchored to the bottom of the card and grows
     upward, so capping the bio is what keeps the text off the face
     in the photo. Deliberately tight on phones - it scrolls. */
  max-height: min(24svh, 165px);
  max-height: min(24dvh, 165px);
  margin-top: 14px;
  overflow-y: auto;
  opacity: 1;
  overscroll-behavior: contain;
}

.card.is-expanded .card__scrim {
  background: linear-gradient(to top,
    rgba(0, 0, 0, 0.95) 0%,
    rgba(0, 0, 0, 0.82) 45%,
    rgba(0, 0, 0, 0.35) 100%);
}

/* Room for more of the bio once the screen is tall enough. */
@media (min-width: 760px) and (min-height: 700px) {
  .card.is-expanded .card__bio { max-height: min(42vh, 300px); }
}

/* ============================================================
   Swipe hint
   ------------------------------------------------------------
   Sits on the right edge of a profile card, above the photo but
   below the text, and mimes the gesture. Pointer-transparent so
   it cannot swallow a tap meant to change the photo.
   ============================================================ */

.swipehint {
  pointer-events: none;
  position: absolute;
  right: 10px;
  top: 34%;
  z-index: 4;
  display: grid;
  justify-items: center;
  gap: 6px;
  padding: 10px 8px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.42);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  color: rgba(255, 255, 255, 0.92);
  animation: swipe-nudge 2s ease-in-out infinite;
}

.swipehint svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.swipehint span {
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  writing-mode: vertical-rl;
}

@keyframes swipe-nudge {
  0%, 100% { transform: translateX(0);    opacity: 0.75; }
  50%      { transform: translateX(-9px); opacity: 1; }
}

/* A moving element beside a photo is exactly what motion sensitivity
   flags, so hold it still and let it just sit there. */
@media (prefers-reduced-motion: reduce) {
  .swipehint { animation: none; }
}

/* --- Progress dots --- */

.dots {
  flex: none;
  display: flex;
  justify-content: center;
  gap: 6px;
}

.dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.22);
  transition: background-color 200ms ease, width 200ms ease;
}

.dot.is-on { width: 18px; border-radius: 3px; background: var(--flame-coral); }

/* --- Action buttons --- */

.actions {
  flex: none;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: clamp(12px, 4.5vw, 20px);
}

.action {
  display: grid;
  place-items: center;
  width: clamp(50px, 15vw, 58px);
  height: clamp(50px, 15vw, 58px);
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: #ffffff;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
  cursor: pointer;
  transition: transform 140ms ease, box-shadow 180ms ease;
}

.action:hover  { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5); }
.action:active { transform: scale(0.93); }

.action svg {
  width: clamp(22px, 6.5vw, 26px);
  height: clamp(22px, 6.5vw, 26px);
  fill: none;
  stroke: currentColor;
  stroke-width: 2.1;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Same colour language as the app: red, blue, green. */
.action--info { color: #fd5068; }
.action--plus { color: #1ec7ff; }
.action--next { color: #21d07a; }

/* --- Menu --- */

.menu {
  position: absolute;
  top: calc(56px + env(safe-area-inset-top));
  right: 14px;
  z-index: 10;
  min-width: 172px;
  padding: 6px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 12px;
  background: #1c1917;
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.6);
  animation: rise 180ms ease-out both;
}

.menu[hidden] { display: none; }

.menu__item {
  display: block;
  width: 100%;
  padding: 11px 14px;
  border: 0;
  border-radius: 8px;
  background: none;
  font: inherit;
  font-size: 0.9rem;
  color: var(--paper);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.menu__item:hover { background: rgba(255, 255, 255, 0.09); }
.menu__item--quiet { color: var(--paper-faint); }

/* The guest's recorded answer, shown at the top of the menu. */
.menu__status {
  margin: 2px 0 6px;
  padding: 8px 14px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.10);
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--paper-faint);
}

.menu__status.is-yes { color: #21d07a; }
.menu__status.is-no  { color: #fd5068; }

/* ============================================================
   The guided journey (invite.html)
   ============================================================ */

.appbar--solo { grid-template-columns: 1fr; }
.appbar--solo .appbar__logo { justify-self: center; }

/* The step container. Card steps fill it exactly; text steps
   scroll inside it so the action bar below stays put. */
.stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
}

.step {
  position: absolute;
  inset: 0;
}

.step[hidden] { display: none; }

/* .stack sizes itself with flex on the hub page, but a .step is
   absolutely positioned and not a flex container — so give the
   stack explicit bounds here or it collapses to zero height. */
.step .stack { position: absolute; inset: 0; }

.step--pad {
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  /* This is a scroll container. Plain `center` pushes overflow out
     of BOTH ends, making the top of a long panel unreachable.
     `safe center` centres when it fits and falls back to start when
     it does not; the plain value first keeps old engines working. */
  justify-content: center;
  justify-content: safe center;
  text-align: center;
  padding: 4px 2px;
}

/* The RSVP form panel */
.fw-form-wrap { width: 100%; padding: 8px 0 20px; }

/* ============================================================
   THE FORMAL SIDE
   ------------------------------------------------------------
   Everything from the invitation onward drops the Tinder styling
   and becomes the wedding site: near-white paper, high-contrast
   display serif, letterspaced capitals, hairline rules, and no
   colour at all beyond ink on paper.
   ============================================================ */

:root {
  --fw-paper:  #ffffff;
  --fw-shell:  #f6f5f2;
  --fw-ink:    #1a1a18;
  --fw-soft:   #4a4a45;
  --fw-muted:  #86867e;
  --fw-rule:   rgba(26, 26, 24, 0.14);
  --fw-gold:   #a58c5b;

  /* High-contrast "Didone" for display, geometric sans for body,
     and a script for the one flourish. Each falls back sensibly:
     Didot on Apple, Bodoni on Office-equipped Windows, Georgia
     everywhere else. */
  --fw-display: "Didot", "Bodoni MT", "Playfair Display", Georgia,
                "Times New Roman", serif;
  --fw-sans:    "Futura", "Century Gothic", "Avenir Next", "Segoe UI",
                system-ui, -apple-system, sans-serif;
  --fw-script:  "Snell Roundhand", "Apple Chancery", "Segoe Script",
                "Brush Script MT", cursive;
}

/* --- Shared formal primitives --- */

.fw-eyebrow {
  margin: 0;
  font-family: var(--fw-sans);
  font-size: 0.66rem;
  font-weight: 500;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--fw-soft);
}

.fw-rule {
  width: 56px;
  height: 0;
  margin: 22px auto;
  border: 0;
  border-top: 1px solid var(--fw-rule);
}

/* The real monogram. `currentColor` in the SVG means it takes the
   ink colour of whatever it sits in. */
.fw-mark {
  display: block;
  width: auto;
  height: clamp(80px, 16vw, 150px);
  color: var(--fw-ink);
}

.fw-mark--hero { height: clamp(120px, 26vw, 300px); }

@media (min-width: 1200px) {
  .fw-mark--hero { height: clamp(240px, 20vw, 380px); }
}

.appbar__mark {
  display: none;
  height: 30px;
  width: auto;
  justify-self: center;
  color: var(--fw-ink);
}

.is-formal .appbar__mark { display: block; }

/* The invitation and the RSVP carry their own monogram in the brand
   panel, so the bar would show a second one right above it. Hide the
   bar entirely on those two steps rather than leaving an empty strip. */
.is-invite .appbar,
.is-form .appbar { display: none; }

/* Kept for the offline fallback text monogram. */
.fw-monogram {
  margin: 0;
  font-family: var(--fw-display);
  font-size: clamp(3.4rem, 17vw, 5.4rem);
  font-weight: 400;
  line-height: 0.8;
  letter-spacing: -0.22em;   /* pulls the two letters into each other */
  padding-right: 0.22em;     /* ...without clipping the C */
  color: var(--fw-ink);
}

.fw-monogram--sm {
  font-size: clamp(1.6rem, 6vw, 2rem);
  line-height: 1;
}

/* ============================================================
   The wedding site (home.html)
   ============================================================ */

.page-formal {
  background: var(--fw-shell);
  color: var(--fw-ink);
  font-family: var(--fw-sans);
}

.fw {
  /* Full width of the page - the column grids below keep the text
     itself readable, so the layout can spread without the prose
     turning into 150-character lines. */
  max-width: none;
  margin-inline: auto;
  padding: 0 clamp(20px, 5vw, 90px)
           calc(48px + env(safe-area-inset-bottom));
}

/* --- Hero --- */

.fw-hero {
  min-height: 62svh;
  display: grid;
  grid-template-columns: 1fr;
  align-items: center;
  justify-items: center;
  gap: clamp(26px, 6vw, 48px);
  padding: clamp(40px, 10vw, 80px) 0 clamp(24px, 5vw, 40px);
  text-align: center;
}

@media (min-width: 760px) {
  .fw-hero { grid-template-columns: 1fr 1fr; }
}

.fw-hero__mark { display: grid; place-items: center; }

/* --- Countdown --- */

.fw-count__label {
  margin: 0 0 clamp(18px, 3vw, 26px);
  font-family: var(--fw-display);
  font-size: clamp(1.7rem, 6vw, 2.6rem);
  letter-spacing: 0.02em;
}

.fw-count__row {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: clamp(8px, 2vw, 16px);
}

.fw-count__unit { text-align: center; }

.fw-count__digits {
  display: flex;
  gap: 6px;
}

/* Each digit is its own tile. They carry a border and a little lift
   so the block reads as the centrepiece of the hero rather than as
   small print beside the monogram. */
.fw-count__digit {
  display: grid;
  place-items: center;
  min-width: clamp(46px, 13vw, 68px);
  padding: clamp(12px, 3vw, 20px) clamp(6px, 2vw, 10px);
  background: var(--fw-paper);
  border: 1px solid var(--fw-rule);
  border-radius: 4px;
  box-shadow: 0 4px 14px rgba(26, 26, 24, 0.07);
  font-family: var(--fw-display);
  font-size: clamp(1.9rem, 7vw, 3rem);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* Direct child only. The digits are spans too, one level deeper, and
   a plain descendant selector here outranks .fw-count__digit - which
   is why the digits had always rendered at label size. */
.fw-count__unit > span {
  display: block;
  margin-top: clamp(10px, 2vw, 14px);
  font-size: clamp(0.66rem, 2.2vw, 0.76rem);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--fw-soft);
}

.fw-count__sep {
  align-self: flex-start;
  padding-top: clamp(14px, 3.4vw, 26px);
  font-size: clamp(1.4rem, 5vw, 2.1rem);
  color: var(--fw-gold);
}

/* --- The strip under the hero --- */

.fw-strip {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
  padding: 20px 0;
  border-top: 1px solid var(--fw-rule);
  border-bottom: 1px solid var(--fw-rule);
  text-align: center;
  font-family: var(--fw-display);
  font-size: clamp(0.72rem, 2.6vw, 0.84rem);
  letter-spacing: 0.16em;
  color: var(--fw-soft);
}

@media (min-width: 620px) {
  /* Two items now the hashtag has gone, so they sit at the two ends
     rather than leaving a hole where the middle column was. */
  .fw-strip {
    grid-template-columns: 1fr 1fr;
    align-items: center;
  }
  .fw-strip > :first-child { text-align: left; }
  .fw-strip > :last-child  { text-align: right; }
}

.fw-strip p { margin: 0; }

/* --- Sections --- */

.fw-section { padding: clamp(46px, 11vw, 88px) 0; }

.fw-section + .fw-section { border-top: 1px solid var(--fw-rule); }

.fw-title {
  margin: 0 0 clamp(28px, 6vw, 48px);
  font-family: var(--fw-display);
  font-weight: 400;
  font-size: clamp(2.1rem, 8.5vw, 3.4rem);
  line-height: 1.02;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}

.fw-title small {
  display: block;
  margin-top: 12px;
  font-family: var(--fw-sans);
  font-size: clamp(0.72rem, 2.8vw, 0.84rem);
  letter-spacing: 0.22em;
  color: var(--fw-soft);
}

/* --- Column grids (About, FAQ) --- */

.fw-cols {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(30px, 6vw, 44px);
}

@media (min-width: 620px) { .fw-cols { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .fw-cols--3 { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 900px) { .fw-cols--4 { grid-template-columns: repeat(4, 1fr); } }
@media (min-width: 1400px) { .fw-cols { gap: clamp(44px, 4vw, 72px); } }

/* The hero has a whole half of a wide screen to itself - give the
   monogram and the countdown enough presence to fill it. */
@media (min-width: 1200px) {
  .fw-hero .fw-monogram { font-size: clamp(5.4rem, 7vw, 8.5rem); }
  .fw-hero { min-height: 68svh; }
}

.fw-item__icon {
  display: block;
  width: 58px;
  height: 58px;
  margin-bottom: 16px;
  stroke: var(--fw-ink);
  fill: none;
  stroke-width: 1.1;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.fw-item__title {
  margin: 0 0 10px;
  font-family: var(--fw-display);
  font-weight: 400;
  font-size: clamp(1.1rem, 4.4vw, 1.3rem);
}

.fw-item p {
  margin: 0;
  max-width: 46ch;          /* a wide column should not mean long lines */
  font-size: 0.88rem;
  line-height: 1.75;
  color: var(--fw-soft);
}

/* An answer can run to more than one paragraph. */
.fw-item p + p { margin-top: 0.9em; }

.fw-item a { color: var(--fw-ink); }

/* --- Schedule --- */

.fw-schedule {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(24px, 5vw, 40px);
  align-items: center;
}

@media (min-width: 760px) {
  .fw-schedule { grid-template-columns: 0.7fr 1.3fr; }
}

.fw-schedule__mark { display: grid; place-items: center; }



.fw-times {
  max-width: 760px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.fw-times li {
  display: grid;
  grid-template-columns: clamp(74px, 22vw, 96px) 1fr;
  gap: clamp(10px, 3vw, 20px);
  align-items: baseline;
  padding: clamp(14px, 3.4vw, 20px) 0;
  border-top: 1px solid var(--fw-rule);
}

.fw-times li:last-child { border-bottom: 1px solid var(--fw-rule); }

.fw-times time {
  font-size: clamp(0.84rem, 3.2vw, 0.95rem);
  font-variant-numeric: tabular-nums;
  color: var(--fw-soft);
}

.fw-times span {
  font-family: var(--fw-display);
  font-size: clamp(1rem, 4vw, 1.2rem);
}

/* --- RSVP call to action --- */

.fw-cta {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(24px, 5vw, 40px);
  align-items: center;
  justify-items: center;
  text-align: center;
}

@media (min-width: 760px) {
  .fw-cta { grid-template-columns: 0.8fr 1.2fr; text-align: left; justify-items: start; }
  .fw-cta__mark { justify-self: center; }
}

.fw-cta__title {
  margin: 0 0 26px;
  font-family: var(--fw-display);
  font-weight: 400;
  font-size: clamp(1.7rem, 7vw, 2.6rem);
  line-height: 1.12;
  text-transform: uppercase;
}

/* --- The status chip --- */

.fw-chip {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 9px 18px;
  border: 1px solid var(--fw-rule);
  border-radius: 999px;
  background: var(--fw-paper);
  font-size: 0.68rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fw-soft);
}

.fw-chip::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

.fw-chip.is-yes { color: var(--fw-ink); }
.fw-chip.is-no  { color: var(--fw-muted); }

/* --- Buttons --- */

.fw-btn {
  display: inline-grid;
  place-items: center;
  min-height: 48px;
  padding: 14px 40px;
  border: 1px solid var(--fw-ink);
  background: var(--fw-ink);
  font-family: var(--fw-sans);
  font-size: 0.7rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--fw-paper);
  text-decoration: none;
  cursor: pointer;
  transition: background-color 160ms ease, color 160ms ease;
}

.fw-btn:hover { background: var(--fw-paper); color: var(--fw-ink); }

/* Narrow phones: keep a two-button row on one line. */
@media (max-width: 420px) {
  .fw-btn { padding: 14px 20px; letter-spacing: 0.16em; }
}

.fw-btn--ghost {
  background: none;
  color: var(--fw-ink);
}

.fw-btn--ghost:hover { background: var(--fw-ink); color: var(--fw-paper); }

.fw-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: center;
}

@media (min-width: 760px) { .fw-cta .fw-actions { justify-content: flex-start; } }

/* --- Map --- */

.fw-map {
  display: block;
  margin-top: 10px;
  font-family: var(--fw-display);
  font-size: clamp(1rem, 4vw, 1.2rem);
  color: var(--fw-ink);
}

/* --- Footer --- */

.fw-foot {
  padding: clamp(30px, 7vw, 48px) 0 0;
  border-top: 1px solid var(--fw-rule);
  text-align: center;
  font-size: 0.74rem;
  letter-spacing: 0.04em;
  color: var(--fw-muted);
}

.fw-foot p { margin: 0 0 8px; }

.fw-foot button {
  /* Padded to a real tap target - it was 43x15 before. */
  padding: 12px 10px;
  margin: -12px -2px;
  min-height: 44px;
  border: 0;
  background: none;
  font: inherit;
  color: var(--fw-muted);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

.fw-foot button:hover { color: var(--fw-ink); }

/* ============================================================
   The RSVP form
   ============================================================ */

.fw-form {
  display: grid;
  gap: clamp(20px, 4.5vw, 30px);
  max-width: 460px;
  margin-inline: auto;
  text-align: left;
}

.fw-field > label,
.fw-field legend {
  display: block;
  margin-bottom: 9px;
  padding: 0;
  font-size: 0.66rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--fw-soft);
}

.fw-field input[type="text"],
.fw-field textarea {
  width: 100%;
  padding: 13px 14px;
  border: 1px solid var(--fw-rule);
  border-radius: 2px;
  background: var(--fw-paper);
  font-family: var(--fw-sans);
  font-size: 0.95rem;
  color: var(--fw-ink);
}

.fw-field textarea { min-height: 84px; resize: vertical; }

.fw-field input:focus-visible,
.fw-field textarea:focus-visible {
  outline: none;
  border-color: var(--fw-ink);
  box-shadow: 0 0 0 3px rgba(26, 26, 24, 0.08);
}

.fw-field fieldset { margin: 0; padding: 0; border: 0; }

.fw-note {
  margin: 8px 0 0;
  padding: 9px 12px;
  border-left: 2px solid var(--fw-rose, #a8656f);
  background: rgba(255, 255, 255, 0.6);
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--fw-soft);
}

.fw-hint {
  margin: 8px 0 0;
  font-size: 0.76rem;
  line-height: 1.5;
  color: var(--fw-muted);
}

/* Radio and checkbox rows sized as proper tap targets. */
.fw-choice {
  display: flex;
  align-items: center;
  gap: 11px;
  min-height: 44px;
  padding: 4px 0;
  font-size: 0.92rem;
  color: var(--fw-ink);
  cursor: pointer;
}

.fw-choice input { width: 18px; height: 18px; accent-color: var(--fw-ink); flex: none; }

/* One block per attending guest. */
.fw-guest {
  padding: clamp(16px, 4vw, 22px);
  border: 1px solid var(--fw-rule);
  background: rgba(255, 255, 255, 0.55);
  display: grid;
  gap: 16px;
}

.fw-guest + .fw-guest { margin-top: 16px; }

.fw-guest__name {
  margin: 0;
  font-family: var(--fw-display);
  font-weight: 400;
  font-size: clamp(1.05rem, 4.2vw, 1.2rem);
  letter-spacing: 0.02em;
}

.fw-guest .fw-field:last-child { margin-bottom: 0; }

.fw-error {
  margin: 0;
  font-size: 0.78rem;
  color: #b3261e;
  min-height: 1.1em;
}

/* ============================================================
   The journey turns formal at the invitation
   ------------------------------------------------------------
   Added to <body> by journey.js for the invite, form and done
   steps.
   ============================================================ */

.page-app.is-formal { background: var(--fw-shell); color: var(--fw-ink); }

.is-formal .appbar__logo { display: none; }

.appbar__monogram {
  display: none;
  margin: 0;
  justify-self: center;
  font-family: var(--fw-display);
  font-size: 1.5rem;
  line-height: 1;
  letter-spacing: -0.2em;
  padding-right: 0.2em;
  color: var(--fw-ink);
}

.is-formal .hero__title {
  font-family: var(--fw-display);
  font-weight: 400;
  font-size: clamp(1.6rem, 7vw, 2.2rem);
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: var(--fw-ink);
}

.is-formal .hero__sub { color: var(--fw-soft); font-family: var(--fw-sans); }

.is-formal .dot { background: rgba(26, 26, 24, 0.18); }
.is-formal .dot.is-on { background: var(--fw-ink); }

.is-formal .actions { gap: 10px; }

/* Wax-seal style stamps rather than a Tinder shout. */
.is-formal .stamp {
  font-family: var(--fw-display);
  font-weight: 400;
  font-size: 0.8rem;
  letter-spacing: 0.2em;
  border-width: 1px;
  border-radius: 0;
  color: var(--fw-ink);
}

.is-formal .stamp--yes,
.is-formal .stamp--no { color: var(--fw-ink); }

/* ============================================================
   Hide the default scrollbars inside the app
   ------------------------------------------------------------
   These panels scroll their own content; the chunky native bar
   sitting over a card or an invitation looks like a mistake.
   The page-level scrollbar on the wedding site is left alone.
   ============================================================ */

.stage,
.step--pad,
.card__bio,
.card--invite {
  scrollbar-width: none;          /* Firefox */
  -ms-overflow-style: none;       /* old Edge */
}

.stage::-webkit-scrollbar,
.step--pad::-webkit-scrollbar,
.card__bio::-webkit-scrollbar,
.card--invite::-webkit-scrollbar {
  width: 0;
  height: 0;
}

/* ============================================================
   THE FORMAL SIDE IS A FULL-BLEED DOCUMENT
   ------------------------------------------------------------
   The Tinder half is deliberately phone-shaped inside a dark
   shell - that is the metaphor. From the invitation onward there
   is no shell, no card and no app chrome: the monogram owns one
   half of the window and the wording the other, edge to edge, at
   every size.
   ============================================================ */

.page-app.is-formal {
  /* Was a fixed-height centring flex container. Centring a panel
     taller than the viewport pushes its top out of reach, and the
     locked height stopped the document growing at all. */
  display: block;
  height: auto;
  min-height: 100svh;
  min-height: 100vh;
  background: var(--fw-paper);
}

.page-app.is-formal .app {
  max-width: none;
  width: 100%;
  height: auto;
  min-height: 100svh;
  min-height: 100vh;
  margin: 0;
  padding: 0;
  gap: 0;
}

/* The phone chrome belongs to the Tinder half only. */
.is-formal .appbar,
.is-formal .dots,
.is-formal .actions { display: none; }

/* Steps stop being absolutely positioned layers and just flow. */
.page-app.is-formal .stage { flex: 1 1 auto; min-height: 0; }
.page-app.is-formal .step { position: relative; inset: auto; }
.page-app.is-formal .step .stack { position: relative; inset: auto; }

/* No inner scroll box and no padding - the page itself scrolls and
   the panel runs to the edges. */
.page-app.is-formal .step--pad {
  overflow: visible;
  justify-content: flex-start;
  padding: 0;
  text-align: left;
}

/* ============================================================
   THE FORMAL PANEL
   ------------------------------------------------------------
   The invitation, the RSVP form and the thank-you screen all sit
   in the same bordered card: monogram on the left, content on
   the right, a hairline between them with a small diamond at its
   midpoint. On phones it becomes one column - monogram at the
   top, content beneath.
   ============================================================ */

/* Full width, full height, no border - the divider between the two
   halves is the only line on the page. */
.panel {
  display: grid;
  grid-template-columns: 1fr;
  width: 100%;
  min-height: 100svh;
  min-height: 100vh;
  background: var(--fw-paper);
}

.panel__mark {
  display: grid;
  place-items: center;
  padding: clamp(22px, 5vw, 40px) 20px clamp(6px, 2vw, 16px);
}

.panel__mark img {
  display: block;
  width: auto;
  height: clamp(54px, 13vw, 78px);
  color: var(--fw-ink);
}

.panel__body {
  width: 100%;
  max-width: 620px;
  margin-inline: auto;
  padding: clamp(24px, 7vw, 44px) clamp(22px, 6vw, 48px)
           clamp(34px, 8vw, 56px);
  text-align: center;
}

/* --- Small marks used throughout --- */

/* A hairline with a diamond in the middle. */
.gem {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 10px;
  width: clamp(120px, 40%, 190px);
  margin: clamp(14px, 3vw, 20px) auto;
}

.gem::before,
.gem::after {
  content: "";
  height: 1px;
  background: var(--fw-gold);
  opacity: 0.55;
}

.gem i {
  display: block;
  width: 5px;
  height: 5px;
  background: var(--fw-gold);
  transform: rotate(45deg);
}

/* --- Invitation --- */

.inv__pre,
.inv__to {
  margin: 0;
  font-size: 0.6rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--fw-soft);
}

.inv__to { margin-top: 4px; }

.inv__script {
  margin: 6px 0 0;
  font-family: var(--fw-script);
  font-size: clamp(2.3rem, 11vw, 3.1rem);
  line-height: 1.05;
  color: var(--fw-ink);
}

.inv__names {
  margin: 8px 0 0;
  font-family: var(--fw-display);
  font-weight: 400;
  font-size: clamp(1.35rem, 6vw, 1.95rem);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  line-height: 1.25;
}

.inv__line {
  margin: 12px auto 0;
  max-width: 30ch;
  font-size: 0.78rem;
  line-height: 1.85;
  color: var(--fw-soft);
  white-space: pre-line;
}

/* The date bar: month/day/year stacked, weekday and time either
   side, separated by hairlines. */
.inv__date {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: clamp(12px, 4vw, 22px);
  margin: clamp(20px, 5vw, 28px) auto 0;
  max-width: 420px;
}

.inv__date b {
  padding: 10px 0;
  border-top: 1px solid var(--fw-gold);
  border-bottom: 1px solid var(--fw-gold);
  font-weight: 400;
  font-size: 0.66rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-align: center;
  color: var(--fw-ink);
}

.inv__day {
  padding: 0 clamp(12px, 4vw, 20px);
  border-left: 1px solid var(--fw-rule);
  border-right: 1px solid var(--fw-rule);
  font-family: var(--fw-display);
  line-height: 1.05;
}

.inv__day em {
  display: block;
  font-style: normal;
  font-family: var(--fw-sans);
  font-size: 0.58rem;
  letter-spacing: 0.22em;
  color: var(--fw-soft);
}

.inv__day strong {
  display: block;
  font-weight: 400;
  font-size: clamp(1.9rem, 8vw, 2.5rem);
  margin: 2px 0;
}

.inv__pin {
  margin: clamp(20px, 5vw, 28px) 0 0;
  line-height: 0;
}

.inv__pin svg {
  width: 17px;
  height: 17px;
  fill: none;
  stroke: var(--fw-ink);
  stroke-width: 1.3;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.inv__venue {
  margin: 8px 0 0;
  font-family: var(--fw-display);
  font-size: 0.92rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.inv__detail {
  margin: 5px 0 0;
  font-size: 0.72rem;
  line-height: 1.7;
  color: var(--fw-soft);
}

/* The three parts of the evening, separated by small gold
   diamonds rather than punctuation. */
.inv__parts {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
  margin: 6px 0 0;
  font-size: 0.72rem;
  color: var(--fw-soft);
}

.inv__parts i {
  width: 4px;
  height: 4px;
  background: var(--fw-gold);
  transform: rotate(45deg);
}

/* A hairline before the reply line. */
.inv__rule {
  width: clamp(150px, 55%, 260px);
  height: 1px;
  margin: clamp(20px, 5vw, 28px) auto 0;
  background: var(--fw-gold);
  opacity: 0.5;
}

.inv__rsvp {
  margin: clamp(16px, 4vw, 22px) 0 0;
  font-size: 0.63rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.inv__closing {
  margin: 8px 0 0;
  font-family: var(--fw-display);
  font-style: italic;
  font-size: 0.85rem;
  color: var(--fw-soft);
}

/* --- Headings shared by the RSVP and thank-you screens --- */

.panel__title {
  margin: 0;
  font-family: var(--fw-display);
  font-weight: 400;
  font-size: clamp(1.6rem, 7vw, 2.3rem);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.panel__sub {
  margin: 8px 0 0;
  font-size: 0.63rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fw-soft);
}

.panel__sub--italic {
  font-family: var(--fw-display);
  font-style: italic;
  font-size: 0.86rem;
  letter-spacing: 0;
  text-transform: none;
}

.panel__lead {
  margin: 0;
  font-family: var(--fw-display);
  font-size: clamp(0.95rem, 3.6vw, 1.05rem);
}

.panel__leadsub {
  margin: 6px 0 0;
  font-size: 0.82rem;
  color: var(--fw-soft);
}

/* The panel centres its headings; the form inside it is left-aligned
   like any form, and needs air under the introduction. */
.panel__body .fw-form {
  margin-top: clamp(22px, 5vw, 34px);
  text-align: left;
}

/* --- The thank-you screen --- */

.done__flag {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin: clamp(18px, 4vw, 26px) 0 0;
}

.done__tick {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  flex: none;
  border-radius: 50%;
  background: var(--fw-gold);
  color: #fff;
}

.done__tick svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2.4; }

.done__flagtext {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.done__body {
  margin: 14px 0 0;
  font-size: 0.84rem;
  line-height: 1.7;
  color: var(--fw-soft);
  white-space: pre-line;
}

/* The summary of what they submitted. */
.done__list {
  margin: clamp(20px, 5vw, 30px) 0 0;
  padding: clamp(18px, 4vw, 22px) 0 0;
  border-top: 1px solid var(--fw-rule);
  display: grid;
  gap: 16px;
  text-align: left;
}

.done__row {
  display: grid;
  grid-template-columns: 22px 1fr auto;
  gap: 12px;
  align-items: start;
  font-size: 0.84rem;
}

.done__row svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: var(--fw-ink);
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.done__label {
  font-size: 0.63rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fw-soft);
  padding-top: 2px;
}

.done__value { text-align: right; color: var(--fw-ink); }
.done__value em { display: block; font-size: 0.78rem; color: var(--fw-soft); }

.done__save {
  display: grid;
  grid-template-columns: 26px 1fr;
  gap: 14px;
  align-items: start;
  margin: clamp(20px, 5vw, 28px) 0 0;
  padding: clamp(16px, 4vw, 20px);
  background: var(--fw-shell);
  text-align: left;
}

.done__save svg {
  width: 22px; height: 22px;
  fill: none; stroke: var(--fw-ink);
  stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round;
}

.done__save h3 {
  margin: 0 0 4px;
  font-size: 0.66rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.done__save p {
  margin: 0;
  font-size: 0.84rem;
  line-height: 1.6;
  color: var(--fw-soft);
  white-space: pre-line;
}

/* --- Actions --- */

/* Stacked and full width at every size, primary on top - the buttons
   read as one block under the wording rather than a toolbar. */
.panel__actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: clamp(20px, 5vw, 30px);
}

.panel__actions .fw-btn { width: 100%; }

/* The invitation answers span both halves in a bar of their own. */
.panel__foot {
  display: flex;
  /* Stacked on a phone with the acceptance on top; the DOM keeps
     decline first so the desktop row reads decline-then-accept. */
  flex-direction: column-reverse;
  gap: 10px;
  padding: clamp(18px, 4vw, 24px) clamp(20px, 5vw, 32px)
           calc(clamp(18px, 4vw, 24px) + env(safe-area-inset-bottom));
  border-top: 1px solid var(--fw-rule);
  background: rgba(26, 26, 24, 0.02);
}

.panel__foot .fw-btn { width: 100%; }

/* ============================================================
   Desktop: the window splits in two
   ============================================================ */

@media (min-width: 900px) {
  .panel {
    grid-template-columns: 1fr 1fr;
    align-items: stretch;
  }

  /* The RSVP form is taller than the window. A monogram centred in
     a 1600px column would sit off-screen, so the left half sticks
     while the right half scrolls past it. */
  .panel__mark { padding: clamp(40px, 6vw, 80px); }

  /* The RSVP form is taller than the window. A monogram centred in a
     1600px column would sit off-screen, so on that step the left
     half sticks while the right half scrolls past it. */
  .is-form .panel__mark {
    position: sticky;
    top: 0;
    align-self: start;
    height: 100svh;
    height: 100vh;
  }

  /* Mark and wording share the first row; the answers bar is the
     second, so the invitation still fills exactly one screen. */
  .card--invite {
    grid-template-rows: 1fr auto;
    min-height: 100svh;
    min-height: 100vh;
  }

  /* The divider lives on the scrolling half so it runs the whole
     height of the document, not just the sticky box. */
  .panel__body { border-left: 1px solid var(--fw-rule); }

  .panel__mark img { height: min(52vh, 400px); }

  /* The diamond that sits on the dividing line. */
  .panel__mark::after {
    content: "";
    position: absolute;
    right: -4px;
    top: 50%;
    width: 7px;
    height: 7px;
    margin-top: -4px;
    background: var(--fw-gold);
    transform: rotate(45deg);
  }

  .panel__body {
    display: grid;
    align-content: center;
    padding: clamp(48px, 6vw, 80px) clamp(40px, 5vw, 72px);
  }

  /* The invitation's bar runs the full width under both halves;
     its two answers sit side by side there. */
  .panel__foot {
    grid-column: 1 / -1;
    flex-direction: row;
    justify-content: center;
    gap: 16px;
    padding: clamp(22px, 3vw, 30px);
  }

  .panel__foot .fw-btn { width: auto; min-width: 230px; }

  .done__row { grid-template-columns: 26px 210px 1fr; }
}

/* ============================================================
   The invitation, which still lives in the swipe stack
   ------------------------------------------------------------
   It has to be a `.card` so answering by swiping keeps working,
   but on the formal side it is not a card at all - no gradient,
   no shadow, no rounded corners.
   ============================================================ */

.card--invite {
  overflow: visible;
  background: var(--fw-paper);
  border-radius: 0;
  box-shadow: none;
  color: var(--fw-ink);
  cursor: default;
}

.page-app.is-formal .card--invite {
  position: relative;
  inset: auto;
  height: auto;
}

/* On phones the invitation is wording, not a photo, so the mark is
   smaller here than on the RSVP and thank-you screens. */
@media (max-width: 899px) {
  /* One column: the rows sit at the top rather than stretching to
     share a viewport-tall panel. */
  .card--invite { align-content: start; }
  .card--invite .panel__mark { padding-bottom: 0; }
  .card--invite .panel__mark img { height: clamp(48px, 12vw, 68px); }
  .card--invite .panel__body { padding-top: clamp(14px, 4vw, 22px); }
}

/* ============================================================
   Our Location
   ------------------------------------------------------------
   Mirrors the schedule block: the mark on one side, the words on
   the other, stacking on phones.
   ============================================================ */

.fw-place {
  display: grid;
  gap: clamp(20px, 4vw, 44px);
  align-items: start;
}

.fw-place__venue {
  margin: 0;
  font-family: var(--fw-display);
  font-weight: 400;
  font-size: clamp(1.05rem, 4.4vw, 1.25rem);
  letter-spacing: 0.06em;
}

.fw-place__address {
  margin: 10px 0 20px;
  font-size: 0.92rem;
  line-height: 1.8;
  color: var(--fw-soft);
}

.fw-place__body .fw-btn { width: 100%; }

/* The map tile. A fixed aspect keeps the row from jumping while the
   iframe loads - and leaves a sensible empty tile in the offline
   build, where Google cannot be reached at all. */
.fw-place__map iframe {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 1px solid var(--fw-rule);
  background: var(--fw-shell);
}

.fw-place__note {
  margin: 10px 0 0;
  font-size: 0.8rem;
  line-height: 1.7;
  color: var(--fw-soft);
}

@media (min-width: 760px) {
  .fw-place { grid-template-columns: 0.8fr 1.2fr; }
  .fw-place__body .fw-btn { width: auto; }
  .fw-place__map iframe { aspect-ratio: 21 / 10; }
}

/* The jump link is a phone affordance; on desktop the schedule is
   already a screen away. */
.fw-jump { display: block; width: 100%; margin-top: clamp(24px, 5vw, 32px); }

@media (min-width: 760px) {
  .fw-jump { display: none; }
}


/* ============================================================
   Banking details under Gifts
   ------------------------------------------------------------
   Read once, then typed into a banking app - so the values are
   set apart from the prose and the digits are monospaced, which
   makes a long account number far easier to read back.
   ============================================================ */

.fw-bank {
  margin: 16px 0 0;
  padding: 14px 16px;
  background: var(--fw-shell);
  border: 1px solid var(--fw-rule);
}

.fw-bank__intro {
  margin: 0 0 10px;
  font-size: 0.78rem;
  color: var(--fw-soft);
}

.fw-bank__list {
  display: grid;
  gap: 8px 10px;
  margin: 0;
}

.fw-bank__list dt {
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fw-soft);
}

.fw-bank__list dd {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 2px;
  font-size: 0.9rem;
  color: var(--fw-ink);
}

/* Tabular digits so the groups line up and can be read back. */
.fw-bank__value {
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  /* Long enough to need wrapping room on a narrow phone. */
  overflow-wrap: anywhere;
}

.fw-bank__copy {
  flex: none;
  /* A thumb needs more than the text does. Padding alone left it
     19px tall, which is well under a reliable tap target. */
  min-height: 40px;
  padding: 6px 14px;
  border: 1px solid var(--fw-rule);
  background: var(--fw-paper);
  color: var(--fw-soft);
  font: inherit;
  font-size: 0.6rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
}

.fw-bank__copy:hover { border-color: var(--fw-ink); color: var(--fw-ink); }
.fw-bank__copy.is-done { border-color: var(--fw-gold); color: var(--fw-gold); }

.fw-bank__note {
  /* Set apart, or it reads as another value in the list rather
     than an instruction about the payment. */
  margin: 14px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--fw-rule);
  font-size: 0.74rem;
  line-height: 1.6;
  color: var(--fw-soft);
}

/* Label beside value once there is room for it. */
@media (min-width: 520px) {
  .fw-bank__list {
    grid-template-columns: auto 1fr;
    align-items: baseline;
  }
  .fw-bank__list dt { padding-top: 1px; }
}

/* ============================================================
   Short screens (landscape phones)
   ------------------------------------------------------------
   There's very little vertical room, so shrink the chrome to
   leave the card as much height as possible.
   ============================================================ */

@media (max-height: 560px) {
  .app { gap: 6px; }
  .action { width: 44px; height: 44px; }
  .action svg { width: 20px; height: 20px; }
  /* Was 40vh, which on a 560px screen came out TALLER than the
     main rule and undid the point of capping the bio. */
  .card.is-expanded .card__bio { max-height: min(30svh, 120px); }

  /* Match screen: pull everything in so it fits without scrolling. */
  .match { gap: 16px; padding-block: 18px; }
  .match__title { font-size: clamp(1.9rem, 7vh, 2.6rem); }
  .match__sub { margin-top: 8px; font-size: 0.85rem; }
  .avatar { width: clamp(84px, 20vh, 110px); }
  .match__pair { gap: clamp(48px, 12vh, 64px); }
  .match__badge { width: 44px; height: 44px; margin: -22px 0 0 -22px; }
  .match__badge img { height: 24px; }
  .btn { height: 46px; }

  /* Login screen: the tall logo is the first thing to give. */
  .brand__flame { height: 60px; margin-bottom: 6px; }
  .login { padding-block: 20px; }
}

/* ============================================================
   Animations
   ============================================================ */

@keyframes match-title {
  from { opacity: 0; transform: translateY(14px) scale(0.9); }
  to   { opacity: 1; transform: none; }
}

@keyframes fly-in-left {
  from { opacity: 0; transform: translateX(-115%) rotate(-14deg); }
  to   { opacity: 1; transform: none; }
}

@keyframes fly-in-right {
  from { opacity: 0; transform: translateX(115%) rotate(14deg); }
  to   { opacity: 1; transform: none; }
}

@keyframes match-leave {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: scale(0.94) translateY(-10px); }
}

@keyframes badge-pop {
  from { opacity: 0; transform: scale(0.2); }
  to   { opacity: 1; transform: scale(1); }
}

@keyframes rise {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}

@keyframes spin { to { transform: rotate(360deg); } }

@keyframes shake {
  10%, 90% { transform: translateX(-2px); }
  20%, 80% { transform: translateX(4px); }
  30%, 50%, 70% { transform: translateX(-7px); }
  40%, 60% { transform: translateX(7px); }
}

/* Respect the OS "reduce motion" setting — kill decorative movement,
   keep opacity changes so state is still communicated. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}
