/* ============================================================
   Akriti & Srijan

   One terracotta wall, a collage of real stickers, a Polaroid, one
   headline. That is the whole idea.

   Layers, back to front:
     .background-layer   z 0    the wall photograph
     .content-layer      z 10   collage, headline, page
     .light-layer        z 20   the drifting light, multiply blend
     .site-nav           z 30   the pill nav
     .drawer-root        z 60   the detail Polaroids, over everything

   The nav has to sit outside .content-layer to reach z 30, because a
   child can never paint above a sibling of its parent.
   ============================================================ */

/* ---------- Recoleta ----------
   Loaded from the local Font/ folder. Only the weights actually used
   are declared, in woff2 with a woff fallback, so this costs about
   110 KB rather than the 9 MB the whole family would.

   Note for design tooling: the "overused font" check flags this. It is
   a false positive. Recoleta is not one of the ubiquitous defaults the
   rule targets (Inter, Roboto, Geist and friends); it is a licensed
   face the owner chose deliberately and supplied the files for, and it
   is the face in the approved design. Keeping it. */
@font-face {
  font-family: "Recoleta";
  src: url("Font/Recoleta-Regular.woff2") format("woff2"),
    url("Font/Recoleta-Regular.woff") format("woff");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Recoleta";
  src: url("Font/Recoleta-Medium.woff2") format("woff2"),
    url("Font/Recoleta-Medium.woff") format("woff");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Recoleta";
  src: url("Font/Recoleta-SemiBold.woff2") format("woff2"),
    url("Font/Recoleta-SemiBold.woff") format("woff");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

/* ---------- Tokens ---------- */
:root {
  --wall: #a94332;
  --wall-deep: #8f3527;
  --cream: #fdf6ea;
  --cream-warm: #f6ead6;
  --cream-edge: #e5d4bb;
  --ink: #33241a;
  --ink-soft: #56422f;
  /* Measured, not guessed: at #8a5f28 the small uppercase labels landed at
     4.49:1 against the cream sheet, a hair under the 4.5:1 AA threshold.
     This deeper bronze takes them to 5.3:1. Used by the day-trip times in
     the Bangalore drawer. */
  --brass: #7d5522;

  --font-display: "Recoleta", ui-serif, Georgia, serif;
  --font-body: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;

  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;
  --space-9: 96px;

  --radius: 6px;
  --radius-lg: 14px;

  /* How much of the light reaches the page. It is the same the whole way
     down: the wall and the shadow are meant to run unbroken from the
     collage to the footer, so nothing changes this at runtime. The 26%
     multiply itself lives in light.js as CONFIG.strength. */
  --light-strength: 1;

  --expo: cubic-bezier(0.16, 1, 0.3, 1);
}

* {
  box-sizing: border-box;
}

html,
body {
  width: 100%;
  min-height: 100%;
  margin: 0;
}

html {
  scroll-behavior: smooth;
  background: var(--wall);
}

body {
  min-height: 100vh;
  overflow-x: hidden;
  isolation: isolate;
  background: var(--wall);
  font-family: var(--font-body);
  color: var(--ink);
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

/* ---------- The fixed layers ----------
   Sized to the viewport plus --chrome-inset, which main.js measures. On
   phones the browser toolbars overlay the page, so every viewport unit
   reports a height shorter than the physical screen and a 100vh layer
   stops short of the bottom. Extending past it is the only way to
   paint the wall behind the toolbar. Zero on desktop. */
.background-layer,
.light-layer {
  position: fixed;
  top: 0;
  left: 0;
  display: block;
  width: 100vw;
  height: calc(100vh + var(--chrome-inset, 0px));
  pointer-events: none;
}

.background-layer {
  z-index: 0;
}

/* Multiply, matching microsoft.ai. The canvas paints near-white where
   the light pools and a gently tinted grey elsewhere, so multiplying
   leaves the wall's own colour alone under the light and shades it
   everywhere else. White in the canvas means "do not touch this
   pixel", which is why the wall colour never shifts. */
.light-layer {
  z-index: 20;
  overflow: hidden;
  mix-blend-mode: multiply;
  opacity: var(--light-strength);
}

.background-layer img,
.light-canvas {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.light-canvas {
  /* The canvas is drawn at half size and stretched. Everything in it is
     soft enough that nobody can tell, and it costs a quarter as much. */
  width: 100%;
  height: 100%;
}

@supports not (mix-blend-mode: multiply) {
  .light-layer {
    display: none;
  }
}

.content-layer {
  position: relative;
  z-index: 10;
  width: 100%;
}

/* ============================================================
   LIQUID GLASS

   Used by the nav pill and the RSVP button. Four qualities were asked
   for, and each maps to a specific layer:

     refraction   a heavier blur confined to the outer band, so the
                  middle stays sharp and the rim smears. That contrast
                  between a clear centre and a distorted edge is what the
                  eye reads as thickness. It is not true refraction: see
                  the note below.
     dispersion   a warm-to-cool gradient in `screen`, masked to the same
                  band, giving the faint colour fringe real glass throws.
     depth        inset shadows: a bright lip where light enters, a
                  dimmer bounce opposite, and a soft inner core shadow.
     light angle  all of the above are oriented to one source, up and to
                  the left, matching the direction the wall shadow falls.

   ⚠️ WHAT THIS DELIBERATELY IS NOT.
   The first attempt used an SVG feDisplacementMap over a duplicated copy
   of the wall, which is the usual recipe for geometric refraction on the
   web. It was built and rejected: the duplicated wall is opaque, so it
   painted over the collage and the Polaroid, and showed bare terracotta
   everywhere the real backdrop was not bare terracotta, which is most of
   where the nav travels. Screenshots are unambiguous.

   The alternative, `backdrop-filter: url(#filter)`, is unimplemented in
   Chromium and unreliable elsewhere, so genuinely bending a moving
   backdrop is not available cross-browser at all. What is here works
   over any backdrop, which matters because the nav is fixed and passes
   over the wall, a photograph and cream card stock as you scroll.

   Cost was measured before any of this was built, with four pills over
   the animating WebGL light: identical to baseline at 1x CPU, and at 6x
   CPU throttling the median frame was unchanged with no frame over 20ms.
   ============================================================ */
.glass {
  position: relative;
  isolation: isolate;
  border-radius: 999px;
  /* The tint is not decoration. The nav is fixed, so it crosses the
     venue photograph and the cream Polaroid cards as you scroll, and
     without a dark base the cream label loses contrast over both. */
  background: rgba(52, 18, 11, 0.52);
  backdrop-filter: blur(5px) saturate(1.28);
  -webkit-backdrop-filter: blur(5px) saturate(1.28);
  box-shadow:
    /* the lip, where the light enters */
    inset 0 1px 0 rgba(255, 240, 224, 0.5),
    inset 1px 0 0 rgba(255, 235, 214, 0.22),
    /* the bounce on the far side */ inset 0 -1px 0 rgba(255, 224, 200, 0.14),
    /* the body of the glass reading as thickness */
      inset 0 -10px 22px -14px rgba(30, 8, 4, 0.5),
    /* and what it casts on the wall */ 0 6px 22px rgba(46, 14, 7, 0.3);
}

/* Refraction: the same backdrop again, blurred much harder, but masked
   away from the middle. `backdrop-filter` on a descendant of an element
   that already has one filters the parent's result, so the rim ends up
   carrying both blurs while the centre carries only the light one. */
.glass::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  backdrop-filter: blur(17px) saturate(1.5) brightness(1.05);
  -webkit-backdrop-filter: blur(17px) saturate(1.5) brightness(1.05);
  -webkit-mask-image: radial-gradient(
    115% 250% at 50% 50%,
    transparent 42%,
    #000 88%
  );
  mask-image: radial-gradient(115% 250% at 50% 50%, transparent 42%, #000 88%);
  pointer-events: none;
}

/* Dispersion: warm on the side the light comes from, cool opposite,
   confined to the same band so it reads as a fringe rather than a wash. */
.glass::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: linear-gradient(
    150deg,
    rgba(255, 196, 130, 0.55) 0%,
    rgba(255, 255, 255, 0) 42%,
    rgba(255, 255, 255, 0) 58%,
    rgba(150, 200, 255, 0.42) 100%
  );
  mix-blend-mode: screen;
  opacity: 0.5;
  -webkit-mask-image: radial-gradient(
    110% 230% at 50% 50%,
    transparent 46%,
    #000 92%
  );
  mask-image: radial-gradient(110% 230% at 50% 50%, transparent 46%, #000 92%);
  pointer-events: none;
}

/* Anyone who cannot have it, or has asked not to, gets a solid pill.
   The label colour does not change, so contrast is the same or better in
   every one of these cases. */
@supports not (backdrop-filter: blur(1px)) {
  .glass {
    background: rgba(52, 18, 11, 0.94);
  }
  .glass::before,
  .glass::after {
    display: none;
  }
}

@media (prefers-reduced-transparency: reduce) {
  .glass {
    background: #3d1409;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  .glass::before,
  .glass::after {
    display: none;
  }
}

@media (prefers-contrast: more) {
  .glass {
    background: #300f06;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    box-shadow: inset 0 0 0 1px #fff3e4;
  }
  .glass::before,
  .glass::after {
    display: none;
  }
}

/* ---------- The RSVP button ---------- */
.btn-glass {
  display: inline-block;
  padding: clamp(12px, 1.2vw, 18px) clamp(24px, 2.4vw, 36px);
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(15px, 1.32vw, 20px);
  letter-spacing: 0.01em;
  color: #fff4e6;
  text-decoration: none;
  white-space: nowrap;
  transition:
    transform 300ms var(--expo),
    box-shadow 300ms var(--expo);
}

.btn-glass:hover {
  transform: translateY(-2px);
}

.btn-glass:focus-visible {
  outline: 3px solid #ffe9c9;
  outline-offset: 4px;
}

/* ============================================================
   PILL NAV
   ============================================================ */
.site-nav {
  position: fixed;
  top: var(--space-5);
  left: 50%;
  z-index: 30;
  transform: translateX(-50%);
}

/* Hidden only when scripting is available, so that if JavaScript never
   runs the nav is simply there from the start. */
.js .site-nav {
  opacity: 0;
  transition: opacity 700ms ease;
}

.js .site-nav.is-ready {
  opacity: 1;
}

.site-nav ul {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  padding: 6px;
  list-style: none;
}

.site-nav a {
  display: block;
  padding: 9px var(--space-5);
  border-radius: 999px;
  font-family: var(--font-display);
  font-size: 17px;
  color: #fdf1e2;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color 200ms ease, color 200ms ease;
}

/* Hover darkens rather than lightens, so the cream label keeps its contrast.
   The current section is the one that gets the solid cream chip, which makes
   "where am I" unmistakable without colliding with the hover state. */
.site-nav a:hover,
.site-nav a:focus-visible {
  background: rgba(30, 8, 4, 0.34);
}

.site-nav a.is-active {
  background: rgba(253, 246, 234, 0.94);
  color: #55200f;
}

.site-nav a.is-active:hover,
.site-nav a.is-active:focus-visible {
  background: #fdf6ea;
}

/* ---------- Secondary nav ----------
   The RSVP page has one real next action at a time: Continue, then Send
   it. "Back to the invitation" is a way out, not the thing to do, so it
   keeps the same glass pill and drops its weight: less tint, a fainter
   lip, a softer shadow, and smaller type. Scoped to the modifier class,
   so the nav on the invitation itself is untouched. */
.site-nav--secondary ul.glass {
  padding: 4px;
  background: rgba(52, 18, 11, 0.24);
  box-shadow:
    inset 0 1px 0 rgba(255, 240, 224, 0.2),
    inset 0 -1px 0 rgba(255, 224, 200, 0.06),
    0 3px 12px rgba(46, 14, 7, 0.14);
}

/* The dispersion fringe is the loudest part of the glass. Dimmed rather
   than removed, so the pill still reads as the same material. */
.site-nav--secondary ul.glass::after {
  opacity: 0.2;
}

.site-nav--secondary a {
  padding: 7px var(--space-4);
  font-size: 15px;
  color: rgba(253, 241, 226, 0.84);
}

.site-nav--secondary a:hover,
.site-nav--secondary a:focus-visible {
  background: rgba(30, 8, 4, 0.3);
  color: #fff6ea;
}

/* Without backdrop-filter the base pill falls back to near-solid. Left
   at that, the secondary version would come out heavier than the
   primary one it is meant to sit behind. */
@supports not (backdrop-filter: blur(1px)) {
  .site-nav--secondary ul.glass {
    background: rgba(52, 18, 11, 0.72);
  }
}

/* ============================================================
   THE COLLAGE

   A fixed-ratio stage. The stage keeps one aspect ratio and every piece
   inside it, including the headline, is positioned as a percentage of
   that box. So the whole composition scales as a single locked unit
   instead of drifting apart as the window changes shape.

   Sizes are in cqw, which is a percentage of the STAGE's width rather
   than the viewport's. That matters once the stage stops filling the
   window: with vw the type would keep growing after the artwork had
   stopped, and the two would come apart.

   Stacking is DOM order, not z-index. The Polaroid sits in the middle of
   the list: everything before it is behind and flies outward from under
   it, everything after is in front and flies inward from off-screen.

   There are two arrangements, both deliberate, neither random. The wide
   one matches the Figma. The tall one below 760px is repacked so the
   pieces stay large enough to read on a phone.
   ============================================================ */
.hero {
  position: relative;
  display: grid;
  /* Pinned to the top, not centred. Centring made the stage's own size
     fight its position: growing it pushed the headline UP into the nav,
     so the stage had to stay small. Pinned, growing it pushes the
     headline DOWN and away from the nav, so the only limit left is the
     bottom of the artwork. */
  justify-items: center;
  align-content: start;
  min-height: 100svh;
  /* Declared HERE, not on .hero-stage, because both need them and custom
     properties inherit downward only: a value set on the stage is
     invisible to its own parent. Setting them there made this padding
     silently resolve to zero. */
  --stage-top: 40px;
  --stage-foot: 24px;
  /* 1 / 0.92. The lowest piece sits at 92% of the stage height, so the
     bottom 8% of the box is empty wall. Sizing the stage against the
     whole box therefore threw away 8% of the artwork for nothing. This
     scales the height budget back up so it is the ARTWORK that lands on
     the bottom margin, not the empty box.

     ⚠️ Re-measure this if the bottom of the composition changes. Today
     the lowest piece is p-bangles. A multiplier rather than a division
     because multiplying by a unitless custom property has the wider
     browser support. */
  --content-scale: 1.087;
  padding: var(--stage-top) 0 var(--stage-foot);
  overflow: hidden;
}

.hero-stage {
  position: relative;

  /* The stage is sized by whichever runs out first, width or height, so the
     whole collage is always on screen. Height is the one that used to be
     ignored: a 1600px stage at this ratio is 1039px tall, which is taller
     than most laptop windows, so the bottom of the collage was cut off.

     --stage-ar has to be the aspect-ratio below written as a number, because
     a ratio cannot be used as a multiplier in calc. The two are changed
     together or the stage stops fitting.

     --stage-top, --stage-foot and --content-scale are declared on `.hero`,
     because that element needs them for its padding too. The nav is still
     allowed to overlap the stage's empty top area, as the design has it;
     what --stage-top reserves is only enough that the HEADLINE clears the
     nav on a narrow window, where the stage is limited by width and the
     headline therefore sits high. Before any of this, at 360x640 the
     headline sat 36px ON TOP of the nav. */
  --stage-ar: 1.5397; /* 1512 / 982 */
  width: min(
    100%,
    1600px,
    (100svh - var(--stage-top) - var(--stage-foot)) * var(--content-scale) *
      var(--stage-ar)
  );
  max-width: 1600px;
  aspect-ratio: 1512 / 982;
  /* Makes cqw inside resolve against this box. */
  container-type: inline-size;
}

.headline {
  position: absolute;
  /* The 1% nudge right and the top value are both measured, not eyeballed:
     the Figma headline's ink sits at x 153..1383, y 186 down. */
  left: 5%;
  top: 18.05%;
  width: 92%;
  margin: 0;
  font-family: var(--font-display);
  font-weight: 500;
  /* Solved, not guessed. Measure the rendered INK, not the layout box:
     the box includes side bearing the eye does not see, which is what
     made an earlier attempt 12% too narrow. At 8.578cqw the ink spanned
     81.4% of the stage; this is one step down from that, at 76.6%.
     Change the wording and this needs re-solving. */
  font-size: 8.075cqw;
  line-height: 0.94;
  letter-spacing: -0.018em;
  text-align: center;
  color: #fffaf2;
  text-shadow: 0 3px 30px rgba(60, 16, 8, 0.4);
}

.js .headline {
  opacity: 0;
}

.collage {
  position: absolute;
  inset: 0;
}

/* Wraps each piece so the mouse parallax has something to move that is
   not the same element the intro is animating. Two transforms on one
   element would fight. */
.layer {
  position: absolute;
  inset: 0;
  will-change: transform;
}

/* Each piece carries its own character:
     --x / --y    where its centre sits, in % of the stage
     --w          how wide it is, in % of the stage
     --rot        how far it tilts
     --px / --py  how far it drifts with the cursor, and in which
                  direction. Negative drifts against the cursor, which
                  is what separates neighbouring pieces.
     --pr         how far it rocks
     --pl         how quickly it chases the cursor. Lower is laggier, so
                  the piece trails its neighbours instead of moving in
                  lockstep. */
.piece {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--w);
  --rot: 0deg;
  --px: 1;
  --py: 1;
  --pr: 0;
  --pl: 0.08;
  transform: translate(-50%, -50%) rotate(var(--rot));
}

.js .piece {
  opacity: 0;
}

.piece img {
  display: block;
  width: 100%;
  height: auto;
}

/* ---------- Wide arrangement: measured off the design ----------
   --x and --y are the centre of each piece as a percentage of the
   1512 x 982 stage; --w is its width on the same scale.

   Solved, not eyeballed. Each piece was template-matched into the
   reference on its GRADIENT MAGNITUDE rather than its colour, because
   flat areas correlate with almost anything: matching on colour put the
   sun face on top of the postcard and the dosa up in the sky. The search
   was also constrained to a window around each piece's previous position,
   since this is a refinement of an existing composition rather than a
   rearrangement, so a piece crossing the frame is a bug not a finding. */

/* behind the Polaroid */
.p-kolam {
  --x: 32.7%;
  --y: 77.5%;
  --w: 17.8%;
  --rot: 0deg;
  --px: 0.35;
  --py: 0.3;
  --pl: 0.05;
  /* Chalk on a doorstep, not a watermark. It sits low and left where the
     Figma puts it, clear of the Polaroid, so it actually reads. */
  opacity: 0.85;
}

.p-stamp {
  --x: 19.7%;
  --y: 33.6%;
  --w: 7.4%;
  --rot: -4deg;
  --px: 0.9;
  --py: 0.5;
  --pr: -0.5;
  --pl: 0.085;
}

.p-sun {
  --x: 44%;
  --y: 32.4%;
  --w: 10.7%;
  --rot: -1deg;
  --px: 0.5;
  --py: 0.35;
  --pr: -0.3;
  --pl: 0.06;
}

.p-postcard {
  --x: 72%;
  --y: 43.7%;
  --w: 13.9%;
  --rot: 3deg;
  --px: 0.55;
  --py: 0.3;
  --pr: 0.35;
  --pl: 0.062;
}

.p-stamp-blr {
  --x: 80.2%;
  --y: 36.3%;
  --w: 6.9%;
  --rot: -5deg;
  --px: 0.85;
  --py: 0.5;
  --pr: -0.6;
  --pl: 0.09;
}

.p-coconut {
  --x: 17.3%;
  --y: 53.5%;
  --w: 12.3%;
  --rot: -2deg;
  --px: 0.6;
  --py: 0.45;
  --pr: 0.3;
  --pl: 0.065;
}

.p-lotus {
  --x: 26.8%;
  --y: 56.2%;
  --w: 12.8%;
  --rot: 1deg;
  --px: 0.7;
  --py: -0.4;
  --pr: -0.45;
  --pl: 0.075;
}

.p-couple {
  --x: 22.2%;
  --y: 73.2%;
  --w: 16.3%;
  --rot: -1deg;
  --px: 0.65;
  --py: -0.5;
  --pr: 0.4;
  --pl: 0.08;
}

.p-musicians {
  --x: 83.2%;
  --y: 53.5%;
  --w: 14.4%;
  --rot: 2deg;
  --px: 0.8;
  --py: 0.4;
  --pr: -0.4;
  --pl: 0.078;
}

.p-rickshaw {
  --x: 80.2%;
  --y: 71.2%;
  --w: 20.1%;
  --rot: -1deg;
  --px: 0.7;
  --py: -0.45;
  --pr: 0.3;
  --pl: 0.072;
}

/* Read off the reference by hand rather than matched. The thali is occluded
   on three sides, by the Polaroid above and the newspaper to its right, so
   there is not enough of it visible for the matcher to lock on: every
   candidate scored about the same and they were scattered across the bottom
   of the frame. Its visible left edge sits at 48.3%, its lowest point at
   90.5%, and the artwork is very nearly square. */
.p-dosa {
  --x: 54.4%;
  --y: 81.5%;
  --w: 12.2%;
  --rot: 2deg;
  --px: 0.5;
  --py: -0.75;
  --pr: 0.35;
  --pl: 0.085;
}

/* the anchor */
.p-polaroid {
  --x: 49.5%;
  --y: 55.1%;
  --w: 37.7%;
  --rot: -2deg;
  --px: 0.3;
  --py: 0.22;
  --pr: 0.18;
  --pl: 0.06;
}

/* in front of the Polaroid */
.p-bell {
  --x: 29.2%;
  --y: 35.9%;
  --w: 6.3%;
  --rot: 2deg;
  --px: 0.75;
  --py: 0.42;
  --pr: 0.6;
  --pl: 0.07;
}

.p-chai {
  --x: 30.2%;
  --y: 65.1%;
  --w: 4.9%;
  --rot: 3deg;
  --px: 0.95;
  --py: -0.6;
  --pr: -0.7;
  --pl: 0.095;
}

/* The upper of the two marigolds under the Ganesha postcard. */
.p-marigold-two {
  --x: 71%;
  --y: 62.9%;
  --w: 7%;
  --rot: 5deg;
  --px: 0.88;
  --py: -0.5;
  --pr: -0.55;
  --pl: 0.092;
}

.p-marigold-a {
  --x: 69%;
  --y: 69%;
  --w: 7.6%;
  --rot: -3deg;
  --px: 0.9;
  --py: -0.55;
  --pr: 0.6;
  --pl: 0.09;
}

.p-marigold-b {
  --x: 31.6%;
  --y: 78.1%;
  --w: 7.2%;
  --rot: 4deg;
  --px: 0.85;
  --py: -0.6;
  --pr: -0.5;
  --pl: 0.088;
}

.p-bangles {
  --x: 68%;
  --y: 81.6%;
  --w: 18.4%;
  --rot: -2deg;
  --px: 0.75;
  --py: -0.65;
  --pr: -0.4;
  --pl: 0.082;
}

/* ---------- Tall arrangement ----------
   A phone is nothing like a 1.5:1 letterbox. Scaling the wide layout
   down would leave every sticker too small to recognise, so the stage
   changes shape and the pieces are repacked around a bigger Polaroid.
   Still one fixed composition, still no randomness. */
@media (max-width: 760px) {
  /* ---------- Tall arrangement ----------
     Not a shrunken copy of the wide one. The pieces are repacked into a
     taller box so they stay large enough to read on a phone, and the
     whole collage is pushed down the stage so the headline has room to
     sit above it rather than underneath it. */
  /* The phone needs its own values for the same reasons. The headline sits
     at 4% of the stage rather than 18%, so it clears the nav only because
     of --stage-top; and the phone composition's lowest piece is at 84%
     rather than 92%, so the content scale differs too. */
  .hero {
    --stage-top: 92px;
    --content-scale: 1.19; /* 1 / 0.84 */
  }

  .hero-stage {
    /* Kept in step with the aspect-ratio below. See the note on .hero-stage. */
    --stage-ar: 0.5714; /* 400 / 700 */
    aspect-ratio: 400 / 700;
  }

  .headline {
    top: 4%;
    left: 2%;
    width: 96%;
    font-size: 15cqw;
    line-height: 0.98;
  }

  .p-kolam {
    --x: 33%;
    --y: 73.5%;
    --w: 30%;
  }
  .p-stamp {
    --x: 13%;
    --y: 29.1%;
    --w: 15%;
  }
  .p-bell {
    --x: 28%;
    --y: 27.1%;
    --w: 12%;
  }
  .p-sun {
    --x: 50%;
    --y: 24.3%;
    --w: 20%;
  }
  .p-postcard {
    --x: 78%;
    --y: 31.9%;
    --w: 25%;
  }
  .p-stamp-blr {
    --x: 90%;
    --y: 24.7%;
    --w: 13%;
  }
  .p-coconut {
    --x: 12%;
    --y: 45.5%;
    --w: 24%;
  }
  .p-lotus {
    --x: 20%;
    --y: 58.3%;
    --w: 19%;
  }
  .p-couple {
    --x: 21%;
    --y: 71.9%;
    --w: 27%;
  }
  .p-musicians {
    --x: 87%;
    --y: 47.9%;
    --w: 24%;
  }
  .p-rickshaw {
    --x: 82%;
    --y: 63.9%;
    --w: 33%;
  }
  .p-polaroid {
    --x: 50%;
    --y: 49.5%;
    --w: 68%;
  }
  .p-chai {
    --x: 14%;
    --y: 36.7%;
    --w: 9%;
  }
  .p-marigold-a {
    --x: 76%;
    --y: 55.9%;
    --w: 18%;
  }
  /* Tucked just above and outside -a, the same pairing as the wide layout. */
  .p-marigold-two {
    --x: 84%;
    --y: 46%;
    --w: 16%;
  }
  .p-marigold-b {
    --x: 34%;
    --y: 64.7%;
    --w: 14%;
  }
  .p-dosa {
    --x: 52%;
    --y: 77.5%;
    --w: 25%;
  }
  .p-bangles {
    --x: 72%;
    --y: 75.1%;
    --w: 27%;
  }
}


/* ============================================================
   THE ASSEMBLY

   The composition builds in three phases, and the whole thing is read as
   DEPTH rather than as movement across a flat page. Nothing jumps: every
   piece is on a continuous easing curve.

     1. THE POLAROID RISES.   It comes up from slightly below, slightly
        small, and settles. It arrives alone so the rest of the
        composition has something to come out from behind.

     2. THE PIECES BEHIND IT GROW OUTWARD.   They start stacked under the
        Polaroid at roughly half size and scale up as they push out to
        their places, which reads as them coming forward from deep
        behind it.

     3. THE PIECES IN FRONT SETTLE DOWN ONTO IT.   The reverse move.
        They start oversized, as though floating close to the lens, and
        shrink back onto the collage, finishing on top of the Polaroid.

   Phases 2 and 3 take exactly the same time per piece. They are the
   same gesture run in opposite directions, so giving one of them a
   longer duration made the composition feel lopsided.

   Every piece fades in as it travels, and every piece overshoots its
   target slightly and drops back. Rotation runs as a SECOND animation
   from the same curve, so a piece straightens out as it travels rather
   than arriving already square, and gets the same small bounce.
   ============================================================ */
:root {
  /* Phase 1 */
  --polaroid-dur: 900ms;
  --polaroid-delay: 60ms;

  /* ONE travel duration, shared by phase 2 and phase 3. A piece takes
     exactly as long to come out from behind the Polaroid as another one
     takes to settle onto it, so the two halves feel like one gesture
     rather than a fast move followed by a slow one. Change this and
     both phases change together, which is the point. */
  --travel-dur: 1400ms;

  /* Phase 2. Starts well before the Polaroid has finished settling:
     waiting for it left a second of nothing but a Polaroid growing. */
  --back-start: 380ms;
  --back-step: 55ms;
  --back-scale: 0.5; /* how small a behind piece starts */

  /* Phase 3. Overlaps the tail of phase 2, so the two never read as
     separate events. */
  --front-start: 1250ms;
  --front-step: 95ms;
  --front-scale: 1.42; /* how large a front piece starts */

  /* The headline waits for the collage. The last front piece starts at
     --front-start + 5 * --front-step and takes --travel-dur, so the
     composition is finished at 1250 + 475 + 1400 = 3125ms. Landing the
     headline a little inside that reads as "the collage settles and the
     words arrive", rather than as a sixth thing still moving. */
  --headline-delay: 3000ms;

  /* Two curves, one per direction of travel. Both end just past their
     target and drop back, which is the bounce. The overshoot is small
     on purpose: it lands on scale, position and tilt at once, so a
     value that looks gentle on any one of them reads as far too much
     when all three do it together.

     EMERGE decelerates hard. A piece leaves the Polaroid quickly and
     then eases a long way into place, which is what makes it feel
     pushed rather than dragged. Its overshoot takes each piece a hair
     past full size before it drops back.

     SETTLE is close to linear through the middle. That is the whole
     point of phase 3: the piece has to stay visibly larger than its
     final size for most of the duration and shrink continuously. An
     ease-out curve here (the obvious first choice) did nearly all the
     shrinking in the first fifth and then sat still, which reads as a
     snap, not a drift. Its overshoot dips each piece just under full
     size as it lands, like paper touching down. */
  --ease-emerge: cubic-bezier(0.34, 1.35, 0.64, 1);
  --ease-settle: cubic-bezier(0.45, 0.03, 0.55, 1.25);
}

/* The Polaroid arrives first and alone, so there is something for the
   rest of the composition to come out from behind. */
.intro-play .p-polaroid {
  animation: pol-in var(--polaroid-dur) var(--ease-emerge) var(--polaroid-delay)
    both;
}

/* The opacity finishes at 40%, well before the movement does, so the
   Polaroid is solid for most of its rise rather than fading the whole
   way up. */
@keyframes pol-in {
  from {
    opacity: 0;
    transform: translate(-50%, -50%) translateY(9%) scale(0.86)
      rotate(calc(var(--rot) + 4deg));
  }
  40% {
    opacity: 1;
  }
  to {
    opacity: 1;
    transform: translate(-50%, -50%) rotate(var(--rot));
  }
}

/* Everything except the Polaroid.

   All three animations run for the same --travel-dur, and the opacity
   ramp is a FRACTION of it rather than a fixed number of milliseconds,
   so the fade is welded to the scale: change the duration and the two
   still finish in proportion.

   ⚠️ The per-piece stagger rides on --fan-delay rather than on a more
   specific selector. `:not()` counts towards specificity, so this rule
   scores higher than a plain `.intro-play .p-stamp`, and the shorthand's
   implicit `animation-delay: 0` used to beat every per-piece rule. Every
   piece then animated at once. Do not "simplify" this back. */
.intro-play .piece:not(.p-polaroid) {
  animation:
    assemble var(--travel-dur) var(--piece-ease, var(--ease-emerge))
      var(--fan-delay, 0ms) both,
    wobble var(--travel-dur) var(--piece-ease, var(--ease-emerge))
      var(--fan-delay, 0ms) both,
    reveal calc(var(--travel-dur) * var(--fade-ratio, 0.45)) ease-out
      var(--fan-delay, 0ms) both;
}

/* Without this the pieces would be on screen from the very first frame,
   bunched at the Polaroid's centre, waiting for their delay to elapse.
   `animation-fill-mode: both` fills BACKWARDS during a delay, so the
   `from` keyframe is painted the whole time the piece is meant to be
   waiting. This fades them in over the first part of their travel
   instead.

   --fade-ratio sets how much of the travel the fade covers. It is 0.45
   for the pieces behind the Polaroid, which are mostly hidden by it
   while it runs, and 0.8 for the ones in front, which have nothing to
   hide behind: those fade almost the whole way down, so the opacity
   climbs while the piece shrinks and the two read as one move. */
@keyframes reveal {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* Pieces behind the Polaroid: bunched at its centre, then stepping out.

   ⚠️ Every default here is a var() FALLBACK, not a separate rule. There
   used to be a `.piece:not(.p-polaroid) { --from-x: 0cqw; ... }` block
   holding these defaults, and because `:not()` counts towards
   specificity it scored (0,2,0) against each per-piece rule's (0,1,0).
   It silently beat all seventeen of them, so no piece ever travelled and
   the whole assembly collapsed into a plain zoom. This is the same trap
   that --fan-delay hit. Do not reintroduce a defaults rule. */
@keyframes assemble {
  from {
    opacity: 1;
    transform: translate(-50%, -50%)
      translate(var(--from-x, 0cqw), var(--from-y, 0cqw))
      scale(var(--from-scale, var(--back-scale)));
  }
  to {
    opacity: 1;
    transform: translate(-50%, -50%) translate(0, 0) scale(1);
  }
}

@keyframes wobble {
  from {
    rotate: calc(var(--rot) + var(--from-rot, 8deg));
  }
  to {
    rotate: var(--rot);
  }
}

/* Because `assemble` owns `transform`, the resting tilt has to be
   applied through the separate `rotate` property or the two would
   overwrite each other. */
.intro-play .piece:not(.p-polaroid) {
  rotate: var(--rot);
}

/* ---------- behind: emerge from under the Polaroid ----------
   Every offset here is COMPUTED, not eyeballed: it is the exact vector
   from the piece's resting centre back to the Polaroid's centre, so all
   eleven start stacked underneath it and step outward to their places.

   The unit is cqw, a percentage of the stage's width, because that is
   the same coordinate space --x and --y live in. Percentages would not
   work: inside translate() a percentage resolves against the element's
   OWN box, so the same number means a different distance for every
   piece. That was the bug: a "60%" nudge on a small sticker is 100px,
   nowhere near the Polaroid.

   Vertical offsets are also in cqw, scaled by 982/1512, because the
   stage is that much shorter than it is wide. */
.p-kolam {
  --from-x: 16.8cqw;
  --from-y: -14.5cqw;
  --fan-delay: calc(var(--back-start) + 0 * var(--back-step));
  --from-rot: -6deg;
}
.p-stamp {
  --from-x: 29.8cqw;
  --from-y: 14cqw;
  --fan-delay: calc(var(--back-start) + 8 * var(--back-step));
  --from-rot: 10deg;
}
/* Almost entirely hidden by the Polaroid, so it emerges early: there is
   nothing to see until the Polaroid has cleared it. */
.p-dosa {
  --from-x: -4.9cqw;
  --from-y: -17.1cqw;
  --fan-delay: calc(var(--back-start) + 2 * var(--back-step));
  --from-rot: -5deg;
}
.p-sun {
  --from-x: 5.5cqw;
  --from-y: 14.7cqw;
  --fan-delay: calc(var(--back-start) + 4 * var(--back-step));
  --from-rot: 7deg;
}
.p-postcard {
  --from-x: -22.5cqw;
  --from-y: 7.4cqw;
  --fan-delay: calc(var(--back-start) + 1 * var(--back-step));
  --from-rot: -9deg;
}
.p-stamp-blr {
  --from-x: -30.7cqw;
  --from-y: 12.2cqw;
  --fan-delay: calc(var(--back-start) + 5 * var(--back-step));
  --from-rot: 11deg;
}
.p-coconut {
  --from-x: 32.2cqw;
  --from-y: 1cqw;
  --fan-delay: calc(var(--back-start) + 9 * var(--back-step));
  --from-rot: -8deg;
}
.p-lotus {
  --from-x: 22.7cqw;
  --from-y: -0.7cqw;
  --fan-delay: calc(var(--back-start) + 6 * var(--back-step));
  --from-rot: 9deg;
}
.p-couple {
  --from-x: 27.3cqw;
  --from-y: -11.8cqw;
  --fan-delay: calc(var(--back-start) + 10 * var(--back-step));
  --from-rot: -7deg;
}
.p-musicians {
  --from-x: -33.7cqw;
  --from-y: 1cqw;
  --fan-delay: calc(var(--back-start) + 3 * var(--back-step));
  --from-rot: 8deg;
}
.p-rickshaw {
  --from-x: -30.7cqw;
  --from-y: -10.5cqw;
  --fan-delay: calc(var(--back-start) + 7 * var(--back-step));
  --from-rot: -6deg;
}

/* ---------- in front: settle down onto the Polaroid ----------
   These sit on top of the Polaroid, so they cannot hide behind it and
   they cannot be the thing that reveals it. They are the last beat
   instead: they start oversized, as though floating close to the lens,
   and shrink back onto the collage.

   The start offset is not eyeballed, it is perspective. An object
   moving away from a camera travels TOWARDS the centre of the frame, so
   run backwards, a piece that begins close to the lens begins offset
   OUTWARD from the vanishing point. Using the Polaroid's centre
   (50%, 54.5%) as that point:

     offset = (rest position - 50%, 54.5%) x (--front-scale - 1) x 0.7

   The 0.7 damps it: the geometrically exact offset at 1.42x threw the
   lower pieces clean off the bottom of the stage.

   Vertical numbers are multiplied by 0.6495 as well, because the stage
   is 982/1512 as tall as it is wide and cqw measures WIDTH.

   ⚠️ --from-scale, --piece-ease and --fade-ratio are set
   once for all five in the grouped rule below, NOT per piece. Do not
   also set them in the individual blocks: the two rules score the same,
   so it would come down to source order. */
.p-bell,
.p-chai,
.p-marigold-two,
.p-marigold-a,
.p-marigold-b,
.p-bangles {
  --from-scale: var(--front-scale);
  --piece-ease: var(--ease-settle);
  /* Nearly the whole descent, because these have the bare wall behind
     them rather than the Polaroid. A short fade would land them solid
     while still oversized, which reads as a pop. */
  --fade-ratio: 0.8;
}

.p-chai {
  --from-x: -5.7cqw;
  --from-y: 1.9cqw;
  --fan-delay: calc(var(--front-start) + 0 * var(--front-step));
  --from-rot: 6deg;
}
.p-marigold-two {
  --from-x: 6.3cqw;
  --from-y: 1.5cqw;
  --fan-delay: calc(var(--front-start) + 1 * var(--front-step));
  --from-rot: -6deg;
}
.p-marigold-b {
  --from-x: -5.3cqw;
  --from-y: 4.4cqw;
  --fan-delay: calc(var(--front-start) + 2 * var(--front-step));
  --from-rot: 5deg;
}
.p-marigold-a {
  --from-x: 5.7cqw;
  --from-y: 2.7cqw;
  --fan-delay: calc(var(--front-start) + 3 * var(--front-step));
  --from-rot: -5deg;
}
.p-bell {
  --from-x: -6cqw;
  --from-y: -3.7cqw;
  --fan-delay: calc(var(--front-start) + 4 * var(--front-step));
  --from-rot: -12deg;
}
/* Last, because it is the largest of the six, so it makes the best final
   beat. */
.p-bangles {
  --from-x: 5.4cqw;
  --from-y: 5.1cqw;
  --fan-delay: calc(var(--front-start) + 5 * var(--front-step));
  --from-rot: 4deg;
}

/* ---------- travel on a phone ----------
   ⚠️ This has to come AFTER the wide travel values, not inside the
   arrangement block further up. A media query adds no specificity, so
   `.p-sun` inside `@media` and `.p-sun` outside it score the same and
   source order decides. When these lived up with the rest of the phone
   layout the desktop vectors below simply overwrote them, and pieces
   started in the wrong place and were left uncovered by the Polaroid.

   Same calculation as the wide one, run again against the 400 x 700
   stage: the pieces behind still start stacked under the Polaroid, and
   the five in front still start offset outward from its centre. The
   vertical numbers change the most, because a phone stage is 1.75x as
   tall as it is wide where the wide one is 0.65x. */
@media (max-width: 760px) {
  .p-kolam {
    --from-x: 17cqw;
    --from-y: -42.0cqw;
  }
  .p-stamp {
    --from-x: 37cqw;
    --from-y: 35.7cqw;
  }
  .p-dosa {
    --from-x: -2cqw;
    --from-y: -49cqw;
  }
  .p-sun {
    --from-x: 0cqw;
    --from-y: 44.1cqw;
  }
  .p-postcard {
    --from-x: -28cqw;
    --from-y: 30.8cqw;
  }
  .p-stamp-blr {
    --from-x: -40cqw;
    --from-y: 43.4cqw;
  }
  .p-coconut {
    --from-x: 38cqw;
    --from-y: 7.0cqw;
  }
  .p-lotus {
    --from-x: 30cqw;
    --from-y: -15.4cqw;
  }
  .p-couple {
    --from-x: 29cqw;
    --from-y: -39.2cqw;
  }
  .p-musicians {
    --from-x: -37cqw;
    --from-y: 2.8cqw;
  }
  .p-rickshaw {
    --from-x: -32cqw;
    --from-y: -25.2cqw;
  }
  /* Damped further than the geometry asks for. The exact offset put the
     glass half off the left edge of a phone at its largest. */
  .p-chai {
    --from-x: -6cqw;
    --from-y: -6.3cqw;
  }
  .p-marigold-two {
    --from-x: 10cqw;
    --from-y: -1.8cqw;
  }
  .p-marigold-a {
    --from-x: 7.3cqw;
    --from-y: 3.1cqw;
  }
  .p-marigold-b {
    --from-x: -4.5cqw;
    --from-y: 7.5cqw;
  }
  .p-bell {
    --from-x: -6.5cqw;
    --from-y: -11.5cqw;
  }
  .p-bangles {
    --from-x: 6.2cqw;
    --from-y: 12.5cqw;
  }
}
/* The headline is the one thing that does not stutter. It is type, not
   paper, so it simply resolves once the cut-outs have landed. */
.intro-play .headline {
  animation: fadeUp 900ms var(--expo) var(--headline-delay) forwards;
}

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

/* Anyone who has asked their system to reduce motion sees the finished
   composition immediately, with no movement at all. The mouse parallax
   is switched off for them too, in main.js. */
@media (prefers-reduced-motion: reduce) {
  .intro-play .piece,
  .intro-play .headline,
  .intro-play .collage {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
  }
}
/* ============================================================
/* ============================================================
   THE PAGE BELOW THE COLLAGE

   The cream panels are gone. Every word from here down sits directly on
   the wall, with the same shadow falling across it that falls across the
   collage, exactly as the Figma has it.

   That has one hard consequence worth stating: cream type on terracotta
   is a far tighter contrast budget than dark type on cream. Nothing here
   is small, nothing is dense, and every size below has been chosen so
   the text survives the darkest part of the light passing over it.

   Sizes come from the Figma, measured off the 1512-wide export rather
   than eyeballed. They are expressed with clamp() against vw so they
   scale down sensibly rather than snapping at a breakpoint.
   ============================================================ */
.invite,
.details {
  position: relative;
  z-index: 4;
  margin: 0 auto;
  padding: 0 var(--space-5);
  /* 1208px of cards plus the 24px gutters either side. The Figma puts the
     three cards at x 152..1360, each 318 wide with 127 between them, and
     this is the width that reproduces exactly that. */
  max-width: 1256px;
  text-align: center;
}

.invite {
  padding-top: clamp(56px, 8.3vw, 126px);
}

/* The two small lines that frame the date: "and we'd love for you to be
   there on" and "at". Figma has them at 29px on a 1512 frame. */
.kicker {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(17px, 1.92vw, 29px);
  line-height: 1.4;
  color: #fdf6ed;
  text-shadow: 0 2px 18px rgba(60, 16, 8, 0.45);
}

.kicker-at {
  margin-top: clamp(40px, 6.4vw, 97px);
}

/* The date, and "See you there!", are the two largest things on the
   page. Figma puts both at 100px. */
.display,
.signoff {
  margin: clamp(14px, 2.8vw, 42px) 0 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(40px, 6.6vw, 100px);
  line-height: 1.02;
  letter-spacing: -0.012em;
  color: #fffaf2;
  text-shadow: 0 3px 30px rgba(60, 16, 8, 0.42);
}

/* ---------- The venue photograph ----------
   Already a Polaroid: the frame is baked into the image, so there is no
   border here to go out of step with it. */
.venue-shot {
  margin: clamp(30px, 5.1vw, 77px) auto 0;
  width: min(72vw, 1100px);
}

.venue-shot img {
  display: block;
  width: 100%;
  height: auto;
  filter: drop-shadow(0 18px 44px rgba(46, 12, 6, 0.4));
}

.venue-name {
  margin: clamp(28px, 5.1vw, 77px) 0 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(26px, 3.5vw, 53px);
  line-height: 1.24;
  color: #f6efe6;
  text-shadow: 0 2px 22px rgba(60, 16, 8, 0.45);
}

/* The city sits a shade warmer than the house, as in the Figma, but not
   so far that it drops out of contrast. */
.venue-name span {
  color: #f9e3d4;
}

.cta-wrap {
  margin: clamp(34px, 6.9vw, 105px) 0 0;
}

.micro {
  margin: clamp(12px, 1.6vw, 24px) 0 0;
  font-size: clamp(13px, 1vw, 15px);
  letter-spacing: 0.02em;
  color: #faf0e5;
  text-shadow: 0 2px 14px rgba(60, 16, 8, 0.5);
}

/* ---------- Dividers ----------
   A hairline, not a border. Kept narrow so it reads as a pause in the
   page rather than a rule across it. */
.rule {
  position: relative;
  z-index: 4;
  width: min(260px, 40vw);
  margin: clamp(48px, 7.3vw, 110px) auto;
  border: 0;
  border-top: 1px solid rgba(255, 244, 232, 0.4);
}

/* ---------- The three detail cards ---------- */
.details-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(20px, 2.25vw, 34px);
  line-height: 1.35;
  color: #fdf7ef;
  text-shadow: 0 2px 20px rgba(60, 16, 8, 0.45);
}

.card-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(20px, 8.4vw, 127px);
  margin: clamp(40px, 6.7vw, 101px) 0 0;
  padding: 0;
  list-style: none;
}

/* The card is a button, not a link: it opens a drawer in place rather
   than going anywhere, and a screen reader should be told which. */
.card {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  transition:
    transform 320ms var(--expo),
    filter 320ms var(--expo);
}

.card-shot {
  display: block;
}

.card-shot img {
  display: block;
  width: 100%;
  height: auto;
  filter: drop-shadow(0 12px 30px rgba(46, 12, 6, 0.34));
}

/* The caption is hand-lettered into each Polaroid's white margin, so
   there is no text element here to position. The alt text carries it
   instead, which is also what names the button.

   ⚠️ Do not set alt="" on these. The label span that used to name the
   button is gone, so an empty alt would leave three buttons a screen
   reader announces as just "button". */
.card {
  position: relative;
}

.card:hover,
.card:focus-visible {
  transform: translateY(-6px) rotate(-1deg);
}

.card:focus-visible {
  outline: 3px solid #ffe9c9;
  outline-offset: 8px;
  border-radius: 4px;
}

/* ---------- Closing lines ---------- */
.closing {
  position: relative;
  z-index: 4;
  /* The Figma sets this paragraph 1086px wide on a 1512 frame. */
  max-width: min(74vw, 1090px);
  margin: clamp(44px, 7.2vw, 109px) auto 0;
  padding: 0 var(--space-5);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(22px, 3.3vw, 50px);
  line-height: 1.34;
  text-align: center;
  color: #f6efe6;
  text-shadow: 0 2px 24px rgba(60, 16, 8, 0.45);
}

.signoff {
  position: relative;
  z-index: 4;
  margin-top: clamp(60px, 12vw, 180px);
  text-align: center;
}
/* ---------- Footer ----------
   Light type on the wall, so it must be large and generously spaced to
   survive the light blend. Nothing small or dense out here. */
.site-footer {
  position: relative;
  z-index: 10;
  padding: var(--space-8) var(--space-5) var(--space-9);
  text-align: center;
}

.site-footer b {
  display: block;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(24px, 3vw, 34px);
  color: #fff6ea;
  text-shadow: 0 2px 22px rgba(60, 16, 8, 0.5);
}

.site-footer p {
  margin: var(--space-3) 0 0;
  font-size: 14.5px;
  color: #fff6ec;
  text-shadow: 0 1px 14px rgba(60, 16, 8, 0.55);
}

.site-footer a {
  color: inherit;
  text-underline-offset: 4px;
  /* WCAG 2.2 asks for a 24x24 target. A bare inline link in 14.5px type
     is 17px tall, so it gets padding to clear that. */
  display: inline-block;
  padding: 4px 2px;
}

/* ---------- Focus ---------- */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 3px;
  border-radius: 2px;
}

.site-nav a:focus-visible,
.site-footer a:focus-visible {
  outline-color: #fff3e4;
}

/* ---------- Reveals ----------
   The class is added by main.js at runtime, so if scripting never runs
   nothing is ever left hidden. */
.reveal {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 800ms var(--expo), transform 800ms var(--expo);
}

.reveal.is-visible {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .reveal,
  .reveal.is-visible {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ============================================================
   SMALL SCREENS

   Phones are far narrower than they are tall, so vmin shrinks
   everything. Enlarge the middle of the composition, pull the outer
   pieces inward, and drop the ones with nowhere left to go.
   ============================================================ */
@media (max-width: 760px) {
  /* Everything the collage needs on a phone now lives in the arrangement
     block further up. What used to be here was the old vmin layout, which
     survived the rewrite and silently overrode the new stage: it resized
     the Polaroid in vmin, moved pieces with --dx / --dy variables that no
     longer exist, and hid five pieces outright. Only the chrome tweaks
     below are still wanted. */
  .site-nav {
    top: var(--space-4);
  }

  .site-nav a {
    padding: 8px var(--space-4);
    font-size: 15px;
  }

  .section {
    padding: var(--space-4) var(--space-4) 0;
  }
}

/* ============================================================
   DETAIL MODALS

   A Polaroid, not a bottom sheet. Each card opens a centred card
   wearing the same warm white frame as the artwork printed on the
   cards themselves: a thin margin on three sides, a slightly deeper
   band along the bottom, and every word sitting in the photo window,
   which is the only part that scrolls.

   The frame is padding on .drawer rather than a border, so the window
   can carry its own paper colour and inset edge without a border and a
   background drifting out of step at the corners.

   It stays the same shape at every width. On a phone it simply gets
   narrower; it does not turn back into a sheet.

   One deliberate improvement on the reference this was modelled on:
   that one sets aria-modal="true" but does not actually trap focus, so
   tabbing walks straight out into the page behind it. This one traps
   focus properly and hands it back to the card that opened it. See
   main.js.
   ============================================================ */
.drawer-root {
  position: fixed;
  inset: 0;
  z-index: 60;
  /* One explicit cell, so the Polaroid's max-height: 100% has a
     definite track to measure against rather than an auto one. The
     padding is the gutter the card is never allowed to touch. */
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  grid-template-columns: minmax(0, 1fr);
  place-items: center;
  padding: clamp(20px, 4vw, 48px);
}

.drawer-root[hidden] {
  display: none;
}

.drawer-scrim {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  opacity: 0;
  transition: opacity 500ms cubic-bezier(0.4, 0, 0.2, 1);
}

.drawer-root.is-open .drawer-scrim {
  opacity: 1;
}

/* ---------- The Polaroid ----------
   --pol-edge is the white margin on three sides, --pol-foot the band
   beneath the window. On a real Polaroid the foot is roughly four
   times the edge. That is a great deal of empty white to ask for on a
   card carrying this much reading, so it is held to about double:
   enough to read as a Polaroid, not enough to waste a screen. */
.drawer {
  --pol-edge: clamp(11px, 1.5vw, 17px);
  --pol-foot: clamp(24px, 2.8vw, 34px);
  /* relative, not absolute: it is the grid's only in-flow child, so the
     grid centres it, and it still paints above the absolute scrim. */
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(100%, 620px);
  max-height: min(100%, 860px);
  padding: var(--pol-edge) var(--pol-edge) var(--pol-foot);
  border-radius: 4px;
  /* Not flat white. Real instant film is warm at the top and cooler at
     the foot, and the gradient is what stops a 620px slab of #fff
     reading as a browser dialog. */
  background: linear-gradient(#fffdf9, #f3ede1);
  box-shadow:
    0 32px 70px rgba(28, 8, 4, 0.45),
    0 4px 14px rgba(28, 8, 4, 0.26);
  opacity: 0;
  transform: translateY(18px) scale(0.965);
  transition:
    transform 500ms cubic-bezier(0.32, 0.72, 0, 1),
    opacity 320ms ease;
  will-change: transform, opacity;
}

.drawer[hidden] {
  display: none;
}

.drawer.is-open {
  opacity: 1;
  transform: translateY(0) scale(1);
}

/* The last line fading into the foot rather than being guillotined by
   it. It sits over the window's bottom padding, so at the end of the
   scroll there is nothing behind it to veil. */
.drawer::after {
  content: "";
  position: absolute;
  right: var(--pol-edge);
  bottom: var(--pol-foot);
  left: var(--pol-edge);
  height: 34px;
  border-radius: 0 0 2px 2px;
  background: linear-gradient(rgba(253, 246, 234, 0), var(--cream));
  pointer-events: none;
}

/* The close button rides the top-right corner of the frame, half on
   and half off. Anywhere inside the window and it would sit over the
   text scrolling beneath it, which reads as a bug rather than a
   button. Out here it never covers a word. */
.drawer-close {
  position: absolute;
  top: -10px;
  right: -10px;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--cream-edge);
  border-radius: 999px;
  background: var(--cream-warm);
  color: var(--ink-soft);
  cursor: pointer;
  transition:
    background-color 200ms ease,
    color 200ms ease;
}

.drawer-close:hover {
  background: #efe0c8;
  color: var(--ink);
}

/* The global button:focus-visible rule squares the corners off so a
   rectangular ring hugs the element. This one is a circle, so the
   radius has to be put back or the ring boxes it in. */
.drawer-close:focus-visible {
  outline: 3px solid var(--wall);
  outline-offset: 2px;
  border-radius: 999px;
}

/* ---------- The photo window ----------
   The only scrolling region. The frame itself must never scroll or the
   white border would slide away with the text.

   min-height: 0 is not optional. A flex child defaults to
   min-height: auto, which means "never shrink below your content", so
   without it the window grows to fit every word, the card outgrows the
   screen, and nothing scrolls at all. */
.drawer-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: clamp(26px, 3.4vw, 42px) clamp(20px, 3vw, 38px)
    clamp(30px, 3.8vw, 48px);
  border-radius: 2px;
  background: var(--cream);
  /* A hairline instead of a border, so the window's edge does not add
     to its width and push the frame out of square. */
  box-shadow: inset 0 0 0 1px rgba(51, 36, 26, 0.08);
  color: var(--ink-soft);
  font-size: 16.5px;
  line-height: 1.62;
  /* The default grey bar reads as browser furniture sitting on top of
     the photograph. Thinned and warmed so it belongs to the paper. */
  scrollbar-width: thin;
  scrollbar-color: rgba(51, 36, 26, 0.26) transparent;
}

/* Safari, which does not read the two properties above. The transparent
   border plus background-clip is the only way to inset a WebKit thumb
   from its track. */
.drawer-body::-webkit-scrollbar {
  width: 11px;
}

.drawer-body::-webkit-scrollbar-track {
  background: transparent;
}

.drawer-body::-webkit-scrollbar-thumb {
  border: 3px solid transparent;
  border-radius: 999px;
  background: rgba(51, 36, 26, 0.26);
  background-clip: content-box;
}

.drawer-body > * {
  max-width: 62ch;
  margin-left: auto;
  margin-right: auto;
}

/* The title scrolls with the text rather than sitting in a fixed
   header. The card is small enough that a pinned header would eat a
   third of the reading area. */
.drawer-title {
  margin: 0 auto var(--space-4);
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(25px, 3vw, 33px);
  line-height: 1.15;
  color: var(--ink);
}

.drawer-lede {
  margin: 0 auto var(--space-6);
  font-family: var(--font-display);
  font-size: clamp(17px, 1.5vw, 20px);
  line-height: 1.5;
  color: var(--ink);
}

.drawer-body h3 {
  margin: var(--space-7) auto var(--space-3);
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(18px, 1.8vw, 23px);
  color: var(--ink);
}

.drawer-body h3:first-of-type {
  margin-top: var(--space-5);
}

.drawer-body p {
  margin: 0 auto var(--space-4);
}

.drawer-body ul {
  margin: 0 auto var(--space-4);
  padding-left: 1.15em;
}

.drawer-body li {
  margin-bottom: var(--space-3);
}

.drawer-body b,
.drawer-body strong {
  color: var(--ink);
  font-weight: 600;
}

.drawer-body a {
  color: var(--wall-deep);
  text-underline-offset: 3px;
}

.drawer-body a:hover {
  color: #75281a;
}

.drawer-note {
  padding: var(--space-4) var(--space-5);
  border: 1px solid rgba(169, 67, 50, 0.35);
  border-radius: var(--radius);
  background: rgba(169, 67, 50, 0.06);
  font-size: 15px;
}

/* Day trips: the drive time sits with the place name rather than in its
   own column, so it stays attached when the row wraps on a phone. */
.trip-list {
  margin: 0;
}

.trip-list > div {
  padding: var(--space-4) 0;
  border-top: 1px solid var(--cream-edge);
}

.trip-list dt {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 18px;
  color: var(--ink);
}

.trip-list dt span {
  margin-left: var(--space-3);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 13px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--brass);
}

.trip-list dd {
  margin: var(--space-2) 0 0;
}

/* On a phone the card keeps its shape and simply gets tighter: a
   thinner white margin so the reading area is not squeezed, and a
   smaller corner button so it does not crowd the screen edge. The
   root's gutter grows rather than shrinks, so there is visible scrim
   above and below and it still reads as a card resting on the page
   rather than a sheet filling it. */
@media (max-width: 760px) {
  .drawer-root {
    padding: 46px 20px;
  }

  .drawer {
    --pol-edge: 9px;
    --pol-foot: 20px;
  }

  .drawer-close {
    top: -8px;
    right: -8px;
    width: 40px;
    height: 40px;
  }
}

/* The card arrives instantly for anyone who has asked for less motion.
   It still arrives: this is a disclosure, not decoration, so removing it
   would remove the content. */
@media (prefers-reduced-motion: reduce) {
  .drawer,
  .drawer-scrim {
    transition-duration: 1ms;
  }
}

/* ============================================================
   THE RSVP PAGE

   One question at a time, on the same wall, under the same shadow.
   Deliberately quiet: no panel, no card, nothing to look at but the
   question you are being asked.

   Every size here is larger than it would be on cream. Cream type on
   terracotta under a multiply blend has a much tighter contrast budget
   than dark type on a light panel, and small text is the first thing to
   fail it.
   ============================================================ */
.rsvp-page {
  position: relative;
  z-index: 4;
  max-width: 700px;
  margin: 0 auto;
  padding: clamp(96px, 14vh, 180px) var(--space-5) clamp(80px, 12vh, 140px);
  min-height: 100svh;
  /* One question at a time reads better centred: there is nothing else
     on the page to align to, so a left edge just floats. This is the
     only place alignment is set. Everything below either inherits it or
     is a flex row, and flex rows ignore text-align, which is why the
     three of them centre themselves explicitly. */
  text-align: center;
}

.rsvp-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(34px, 5.2vw, 62px);
  line-height: 1.06;
  color: #fffaf2;
  text-shadow: 0 3px 30px rgba(60, 16, 8, 0.42);
}

.rsvp-sub {
  margin: var(--space-4) 0 0;
  font-family: var(--font-display);
  font-size: clamp(15px, 1.5vw, 19px);
  color: #fcf3ea;
  text-shadow: 0 2px 16px rgba(60, 16, 8, 0.5);
}

.rsvp-flow {
  margin-top: clamp(40px, 7vh, 72px);
}

.rsvp-progress {
  margin: 0 0 var(--space-6);
  font-size: 13px;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: #fae8d9;
  text-shadow: 0 2px 14px rgba(60, 16, 8, 0.55);
}

/* A fieldset, because these really are groups of related controls, but
   stripped of the browser's box. */
.rsvp-step {
  margin: 0;
  padding: 0;
  border: 0;
}

/* Without scripting every step is on the page at once, so they need
   space between them. With scripting only the current one is shown.

   The current step is a class, not the `hidden` attribute, so that
   `hidden` is free to mean one thing only: this question does not apply
   to you. steps.js relies on that separation. */
.is-stepped .rsvp-step {
  display: none;
  margin-bottom: 0;
}

.is-stepped .rsvp-step.is-current {
  display: block;
  animation: rsvp-step-in 320ms var(--expo) both;
}

@keyframes rsvp-step-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .is-stepped .rsvp-step.is-current {
    animation-duration: 1ms;
  }
}

.rsvp-step + .rsvp-step,
#rsvp-attending-details .rsvp-step {
  margin-top: clamp(40px, 6vh, 64px);
}

.rsvp-q {
  display: block;
  padding: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(26px, 3.4vw, 40px);
  line-height: 1.16;
  color: #fffaf2;
  text-shadow: 0 3px 26px rgba(60, 16, 8, 0.45);
}

.rsvp-q:focus-visible {
  outline: 3px solid #ffe9c9;
  outline-offset: 6px;
  border-radius: 4px;
}

/* Underline fields group by proximity, not by a box, so the gap above a
   label has to beat the gap below it. Otherwise the label reads as
   belonging to the line above it rather than to its own. */
.rsvp-label {
  display: block;
  margin: var(--space-7) 0 4px;
  font-size: 15px;
  color: #fcf3ea;
  text-shadow: 0 2px 14px rgba(60, 16, 8, 0.5);
}

/* A single rule to write on rather than a boxed field. A cream box is
   the most legible thing possible, but it is also the most dated, and it
   is the only object on the page that does not belong to the
   photograph. The line keeps the affordance and loses the form.

   Legibility is bought back three ways instead: the same text shadow the
   rest of the page uses, a warm underline bright enough to read over the
   busiest part of the wall, and a brighter one still on focus. */
.rsvp-input {
  display: block;
  width: 100%;
  padding: 2px 4px 10px;
  border: 0;
  border-bottom: 1px solid rgba(255, 240, 224, 0.45);
  border-radius: 0;
  background: transparent;
  color: #fffaf2;
  caret-color: #ffe9c9;
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.5;
  text-shadow: 0 2px 14px rgba(60, 16, 8, 0.55);
  /* Inherited alignment does not cross into a form control, so the
     typed text has to be centred by hand or it sits left in an
     otherwise centred page. */
  text-align: center;
  transition: border-color 200ms ease;
}

/* The stepper arrows are the last piece of browser chrome on the page,
   and nobody books a table for 7 by clicking a 6px triangle. The field
   stays type="number", so validation and the numeric keypad on a phone
   are unaffected: only the widget goes. */
.rsvp-input[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
}

.rsvp-input[type="number"]::-webkit-outer-spin-button,
.rsvp-input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  appearance: none;
  margin: 0;
}

/* Chrome paints its own pale yellow over an autofilled field, which
   would put the box straight back on name and email. The inset shadow
   is the only way to override that fill, and the absurd transition
   delay stops it flashing in before the shadow lands. */
.rsvp-input:-webkit-autofill,
.rsvp-input:-webkit-autofill:hover,
.rsvp-input:-webkit-autofill:focus {
  -webkit-text-fill-color: #fffaf2;
  -webkit-box-shadow: 0 0 0 1000px rgba(52, 18, 11, 0.001) inset;
  caret-color: #ffe9c9;
  transition: background-color 9999s ease-in-out 0s;
}

/* A textarea with only a bottom rule leaves the line stranded three
   rows below whatever was typed, and parks the browser's resize grabber
   on top of it. field-sizing lets the field grow with its content, so
   the line stays under the last line of text and the grabber is no
   longer needed. Browsers without it fall back to the rows attribute
   and scroll, which is fine for a sentence about nuts. */
textarea.rsvp-input {
  resize: none;
  field-sizing: content;
  min-height: 2.4em;
}

/* Narrower than its column, so it needs the auto margins to sit in the
   middle. A block element with a max-width hugs the left otherwise. */
.rsvp-input--short {
  max-width: 140px;
  margin-inline: auto;
}

.rsvp-input:hover {
  border-bottom-color: rgba(255, 240, 224, 0.7);
}

/* A ring is the wrong focus indicator here. With no box to hug, the
   outline encloses empty space and, on a tall field, collides with the
   label above it. The line itself does the job instead: brightened and
   doubled in weight, which is unmistakable and cannot overlap anything.

   box-shadow rather than a 2px border, because changing border width
   would move the text by a pixel every time you tab into a field. */
.rsvp-input:focus-visible {
  outline: none;
  border-bottom-color: #ffe9c9;
  box-shadow: 0 1px 0 0 #ffe9c9;
}

/* Windows high contrast throws away both the border colour and the
   shadow, so there the ring is the only indicator left. */
@media (forced-colors: active) {
  .rsvp-input:focus-visible {
    outline: 2px solid;
    outline-offset: 2px;
  }
}

/* Only after a failed attempt, so nothing is marked wrong before it has
   been filled in. A red line rather than a red ring: there is no box
   left to draw a ring around. */
.was-validated .rsvp-input:invalid {
  border-bottom-color: #ff9d8a;
  box-shadow: 0 1px 0 0 #ff9d8a;
}

.rsvp-help {
  margin: var(--space-3) 0 0;
  font-size: 14.5px;
  line-height: 1.5;
  color: #faf0e5;
  text-shadow: 0 2px 14px rgba(60, 16, 8, 0.5);
}

/* ---------- Yes or no ---------- */
.rsvp-choices {
  display: grid;
  gap: var(--space-3);
  margin-top: var(--space-6);
}

.rsvp-choice {
  display: flex;
  align-items: center;
  /* A flex row does not inherit text-align. With the dot taken out of
     flow the label is the only child left, so this centres it exactly,
     and the two options line up with each other. */
  justify-content: center;
  position: relative;
  padding: 20px;
  border: 1px solid rgba(255, 240, 224, 0.3);
  border-radius: var(--radius-lg);
  background: rgba(52, 18, 11, 0.4);
  color: #fdf1e2;
  font-family: var(--font-display);
  font-size: clamp(17px, 1.7vw, 21px);
  cursor: pointer;
  transition:
    background-color 200ms ease,
    border-color 200ms ease;
}

.rsvp-choice:hover {
  background: rgba(52, 18, 11, 0.58);
}

/* The dot is hidden, not removed. Centring a dot and a label as a pair
   left the two dots 11px apart, because the labels are different
   lengths, and a ragged pair reads as a mistake next to everything else
   on this page lining up. The pill itself carries the state instead.

   ⚠️ Clipped rather than display:none or visibility:hidden. Both of
   those drop it out of the tab order and the accessibility tree, so it
   would stop being reachable or announceable. This way it is still a
   real radio: focusable, arrow-key navigable, and read out as checked.
   The label wraps it, so clicking anywhere on the pill still selects. */
.rsvp-choice input {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* With no dot, this is the only thing saying which one you picked, so it
   changes weight as well as colour: the inset ring doubles the border
   without moving anything. Colour alone would fail WCAG 1.4.1, and would
   also just be hard to see. */
.rsvp-choice:has(input:checked) {
  border-color: #ffe0bd;
  background: rgba(52, 18, 11, 0.72);
  box-shadow: inset 0 0 0 1px #ffe0bd;
}

.rsvp-choice:has(input:focus-visible) {
  outline: 3px solid #ffe9c9;
  outline-offset: 3px;
}

/* Without :has() nothing above can show the checked state, and the dot
   is the only signal left. Put it back rather than leave someone unable
   to tell what they picked. */
@supports not selector(:has(*)) {
  .rsvp-choice input {
    position: static;
    width: 22px;
    height: 22px;
    clip-path: none;
    accent-color: #fdf1e2;
  }

  .rsvp-choice {
    gap: var(--space-4);
  }
}

/* ---------- Review ---------- */
.rsvp-review {
  margin: var(--space-6) 0 0;
}

.rsvp-review > div {
  padding: var(--space-4) 0;
  border-top: 1px solid rgba(255, 240, 224, 0.25);
}

.rsvp-review dt {
  font-size: 13px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #eec9a8;
  text-shadow: 0 2px 14px rgba(60, 16, 8, 0.5);
}

.rsvp-review dd {
  margin: var(--space-2) 0 0;
  font-family: var(--font-display);
  font-size: clamp(17px, 1.7vw, 21px);
  color: #fff6ea;
  text-shadow: 0 2px 18px rgba(60, 16, 8, 0.45);
  overflow-wrap: anywhere;
}

/* ---------- Actions ----------
   Back keeps order: -1 so it stays to the left of Continue, and the
   pair is centred together. Centring the buttons individually would
   stack them; this keeps them on one line under the question. */
.rsvp-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  margin-top: clamp(36px, 6vh, 56px);
}

.rsvp-next,
.rsvp-send {
  padding: 15px 30px;
  border: 0;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 18px;
  color: #fff4e6;
  cursor: pointer;
}

.rsvp-next[hidden],
.rsvp-send[hidden],
.rsvp-back[hidden] {
  display: none;
}

.rsvp-back {
  order: -1;
  padding: 15px 6px;
  border: 0;
  background: none;
  color: #f0ddcd;
  font-family: var(--font-body);
  font-size: 15px;
  text-decoration: underline;
  text-underline-offset: 4px;
  cursor: pointer;
}

.rsvp-back:hover {
  color: #fff6ea;
}

.rsvp-next:focus-visible,
.rsvp-send:focus-visible,
.rsvp-back:focus-visible {
  outline: 3px solid #ffe9c9;
  outline-offset: 4px;
}

.form-status {
  margin: var(--space-5) 0 0;
  min-height: 1.4em;
  font-size: 15.5px;
  color: #f6e6d6;
  text-shadow: 0 2px 14px rgba(60, 16, 8, 0.55);
}

.form-status.is-error {
  color: #ffd7c9;
}

/* Off screen rather than display:none, so a bot's form parser still
   finds it while nobody using the page ever does. */
.rsvp-honey {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* The thank-you replaces the form, so it carries the page on its own. */
.rsvp-done {
  margin-top: clamp(40px, 7vh, 72px);
}

.rsvp-done b {
  display: block;
  margin-bottom: var(--space-4);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(28px, 3.6vw, 42px);
  color: #fffaf2;
  text-shadow: 0 3px 26px rgba(60, 16, 8, 0.45);
}

.rsvp-done .card-title {
  margin: 0 0 var(--space-4);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(28px, 3.6vw, 42px);
  color: #fffaf2;
  text-shadow: 0 3px 26px rgba(60, 16, 8, 0.45);
}

.rsvp-done p {
  margin: 0;
  font-size: 17px;
  line-height: 1.6;
  color: #f4e7da;
  text-shadow: 0 2px 16px rgba(60, 16, 8, 0.5);
}

/* Three Polaroids side by side works at the Figma's 1512, where each is
   318px wide. On a phone the same grid leaves them about 92px across
   with the label wrapping onto two lines, which is neither readable nor
   comfortably tappable. They stack instead. */
@media (max-width: 760px) {
  .card-row {
    grid-template-columns: 1fr;
    gap: clamp(28px, 8vw, 44px);
    justify-items: center;
  }

  .card {
    max-width: 260px;
  }
}
