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

:root {
  --f-display: 'Cormorant Garamond', 'Palatino Linotype', Palatino, Georgia, serif;
  --f-body:    'EB Garamond', 'Garamond', Georgia, serif;
  --f-caps:    'Cinzel', 'Trajan Pro', Georgia, serif;
  --f-script:  'Great Vibes', 'Cormorant Garamond', cursive;
  --cream:     #F9F5EE;
  --parchment: #F0E8D8;
  --gold:      #9A7B3C;
  --gold-lt:   #C8A85A;
  --gold-pale: #E8D9B0;
  /* AA-safe (4.5:1+) darker gold for readable text on cream/parchment; --gold/--gold-lt stay for decorative borders, dividers, icons where only 3:1 (non-text) applies. */
  --gold-text: #7A5D2E;
  --ink:       #2B2417;
  --sage:      #5C6B52;
}

html { scroll-behavior: smooth; }

body {
  font-family: var(--f-body);
  color: var(--ink);
  background: var(--cream);
  /* overflow-x:hidden here breaks position:sticky for descendants (the footer
     reveal): per spec, setting overflow-x to anything but visible forces
     overflow-y to auto too, which makes body itself — not the viewport — the
     sticky containing block, so the footer stuck almost immediately instead
     of only near the page's natural end. overflow-x:clip prevents the same
     horizontal-scrollbar issue without establishing a scroll container. */
  overflow-x: clip;
  position: relative;
}

/* fixed, page-wide grain — pure SVG noise, no image download, breaks the flat-fill sterility */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 40;
  pointer-events: none;
  opacity: 0.035;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ── Menu ── */
.menu-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 50; display: flex; justify-content: center; padding: 18px; }
.menu-toggle {
  font-family: var(--f-caps);
  font-size: 11px;
  letter-spacing: .24em;
  text-transform: uppercase;
  background: var(--cream);
  color: var(--ink);
  border: 1px solid var(--gold-pale);
  border-radius: 999px;
  padding: 10px 22px;
  cursor: pointer;
}
.menu-panel {
  position: fixed;
  top: 62px;
  left: 50%;
  transform: translateX(-50%) translateY(-12px);
  background: var(--cream);
  border: 1px solid var(--gold-pale);
  border-radius: 16px;
  box-shadow: 0 12px 40px rgba(0,0,0,.15);
  display: flex;
  flex-direction: column;
  padding: 10px 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s ease, transform .25s ease;
  min-width: 200px;
}
.menu-panel.open { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0); }
.menu-panel a {
  font-family: var(--f-caps);
  font-size: 10px;
  letter-spacing: .18em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink);
  padding: 12px 28px;
  text-align: center;
}
.menu-panel a:hover { background: var(--parchment); color: var(--gold-text); }

/* ── Hero ── */
.hero {
  position: relative;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  background: linear-gradient(180deg, var(--parchment) 0%, var(--cream) 60%);
  overflow: hidden;
}
#heroCanvas { position: absolute; inset: 0; z-index: 1; }
.hero-content { position: relative; z-index: 2; padding: 0 24px; }
/* A portion of the particles render on this second canvas, stacked above the
   text — combined with #heroCanvas below the text, particles read as
   floating both behind and in front of "Daniel & Victoria" at once, not all
   on one flat layer. pointer-events:none so it never blocks the text/countdown. */
#heroCanvasFront { position: absolute; inset: 0; z-index: 3; pointer-events: none; }
.eyebrow {
  font-family: var(--f-caps);
  font-size: 11px;
  letter-spacing: .32em;
  color: var(--sage);
  text-transform: uppercase;
  margin-bottom: 22px;
}
.names { display: flex; align-items: center; justify-content: center; gap: 18px; flex-wrap: wrap; }
.name {
  font-family: var(--f-script);
  font-style: italic;
  font-weight: 300;
  font-size: clamp(56px, 11vw, 110px);
  line-height: 1;
  color: var(--ink);
}
.ampersand {
  font-family: var(--f-script);
  font-style: italic;
  font-size: clamp(28px, 4vw, 44px);
  color: var(--gold);
}
@media (max-width: 640px) {
  /* Daniel / & / Victoria stack into their own centered lines instead of
     wrapping wherever there happens to be room — flex-wrap alone doesn't
     guarantee the ampersand lands on its own line. */
  .names { flex-direction: column; gap: 4px; }
}
.hero-date {
  font-family: var(--f-caps);
  font-size: 15px;
  letter-spacing: .16em;
  color: var(--gold-text);
  text-transform: uppercase;
  margin-top: 26px;
}
.countdown { display: flex; justify-content: center; gap: 28px; margin-top: 40px; }
.countdown-unit { display: flex; flex-direction: column; align-items: center; }
.countdown-unit span {
  font-family: var(--f-display);
  font-size: clamp(28px, 5vw, 42px);
  color: var(--ink);
  line-height: 1;
  /* Digits are zero-padded to 2 characters already, but the display serif
     isn't monospaced — "1" and "8" render at different widths, so the whole
     countdown visibly shifts sideways every second. Fixed width + centered
     text stops the jitter regardless of which digits are showing. */
  display: inline-block;
  width: 2.2ch;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.countdown-unit label {
  font-family: var(--f-caps);
  font-size: 9px;
  letter-spacing: .2em;
  color: var(--sage);
  text-transform: uppercase;
  margin-top: 6px;
}

/* ── Sections ── */
.section {
  position: relative;
  overflow: hidden;
  padding: 110px 24px;
  /* subtle tiled botanical watermark — echoes the invite's leaf/vine border motif as quiet paper-like texture instead of a flat fill */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cg fill='none' stroke='%237A5D2E' stroke-width='1' opacity='0.05'%3E%3Cpath d='M20 120 Q45 70 32 22 Q55 55 68 32 Q58 78 100 100'/%3E%3Ccircle cx='32' cy='22' r='3' fill='%237A5D2E'/%3E%3Ccircle cx='68' cy='32' r='2.4' fill='%237A5D2E'/%3E%3C/g%3E%3C/svg%3E");
}
.section.alt { background-color: var(--parchment); }
.section-inner { position: relative; z-index: 1; max-width: 680px; margin: 0 auto; text-align: center; }
.section-inner.wide { max-width: 1100px; }

/* ambient photo texture behind two flat sections — real couple photos, heavily blurred and dimmed,
   instead of another solid fill. Sits behind .section-inner (z-index:1) via .section's own stacking context. */
#when-where::before, #rsvp::before {
  content: "";
  position: absolute;
  inset: -40px;
  background-size: cover;
  background-position: center;
  filter: blur(28px) saturate(0.85);
  opacity: 0.15;
  z-index: 0;
}
#when-where::before { background-image: url('../images/couple-02.jpg'); }
#rsvp::before { background-image: url('../images/couple-05.jpg'); }

/* Our Story: photo + text split instead of flat centered text */
.story-split { display: grid; grid-template-columns: 1fr 1fr; gap: 56px; align-items: center; text-align: left; }
.story-photo img {
  width: 100%; aspect-ratio: 4/5; object-fit: cover;
  border-radius: 20px; border: 1px solid var(--gold-pale);
  box-shadow: 0 24px 50px -20px rgba(43, 36, 23, .35);
}
.story-copy .section-label, .story-copy .scripture-ref { text-align: left; }
.story-copy .divider { margin-left: 0; }
@media (max-width: 900px) {
  .story-split { grid-template-columns: 1fr; gap: 32px; text-align: center; }
  .story-copy .section-label, .story-copy .scripture-ref { text-align: center; }
  .story-copy .divider { margin: 36px auto; }
  /* Keep the same 4/5 portrait crop as desktop instead of a short 16/10 —
     the source photo is a tall full-body shot, and a landscape crop was
     hiding most of the couple, not just their heads. */
}

/* Gifts: couple-06.jpg as a warm ambient background instead of the old
   standalone full-bleed foreground strip — a background-size:cover photo
   recrops smoothly at every viewport width, so there's no visible "this framing
   looks off" moment the way a sharp foreground crop had. Less blur and higher
   opacity than the When&Where/RSVP ambient treatment (#gifts::before below
   them) since Gifting is a short, sparse section that reads better with a bit
   more presence; a gold/parchment veil (::after) keeps it warm and keeps the
   phone numbers legible instead of just fading to grey. */
#gifts::before {
  content: "";
  position: absolute;
  inset: -40px;
  background-image: url('../images/couple-06.jpg');
  background-size: cover;
  background-position: 50% 22%;
  filter: blur(2px) saturate(0.9);
  opacity: 0.32;
  z-index: 0;
}
#gifts::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--gold-pale) 60%, transparent) 0%,
    color-mix(in srgb, var(--parchment) 70%, transparent) 55%,
    var(--parchment) 100%);
  z-index: 0;
}
.section-label {
  font-family: var(--f-caps);
  font-size: 14px;
  letter-spacing: .3em;
  color: var(--gold-text);
  text-transform: uppercase;
  margin-bottom: 28px;
}
.story-text {
  font-family: var(--f-display);
  font-size: 25px;
  font-weight: 300;
  line-height: 1.7;
  margin-bottom: 22px;
}
.scripture-text {
  font-family: var(--f-display);
  font-style: italic;
  font-size: 24px;
  font-weight: 300;
  line-height: 1.65;
}
.scripture-ref {
  font-family: var(--f-caps);
  font-size: 12px;
  letter-spacing: .26em;
  color: var(--gold-text);
  text-transform: uppercase;
  margin-top: 10px;
}

.divider { display: flex; align-items: center; gap: 10px; margin: 36px auto; width: fit-content; }
.divider-vine { width: 52px; height: 22px; color: var(--gold-text); flex-shrink: 0; }
.divider-vine.r { transform: scaleX(-1); }
.divider-heart { font-size: 26px; color: var(--gold-text); line-height: 1; }

.venue-fullnames { font-family: var(--f-caps); font-size: 12px; letter-spacing: .18em; text-transform: uppercase; color: var(--gold-text); margin-bottom: 14px; }
.venue-name { font-family: var(--f-caps); font-size: 38px; letter-spacing: .06em; margin-bottom: 12px; }
.venue-detail { font-family: var(--f-display); font-style: italic; font-size: 23px; color: var(--sage); margin-bottom: 8px; }
.venue-time { font-family: var(--f-caps); font-size: 15px; letter-spacing: .1em; color: var(--gold-text); margin-bottom: 10px; }
.venue-address { font-family: var(--f-body); font-size: 19px; color: var(--ink); margin-bottom: 32px; }
/* the address is now also a tap target that opens the map modal — looks like
   plain text at rest, an underline on hover/focus is the only affordance,
   matching the site's general restraint around interactive-vs-static text */
.venue-address-btn {
  display: inline-block; background: none; border: none; padding: 0; cursor: pointer;
  font: inherit; color: inherit;
  border-bottom: 1px solid transparent;
  transition: border-color .2s ease, color .2s ease;
}
.venue-address-btn:hover, .venue-address-btn:focus-visible { color: var(--gold-text); border-bottom-color: var(--gold-pale); }

.btn-row { display: flex; justify-content: center; gap: 16px; flex-wrap: wrap; }
.btn {
  display: inline-block;
  font-family: var(--f-caps);
  font-size: 12px;
  letter-spacing: .16em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--cream);
  background: var(--gold-text);
  border: 1px solid var(--gold-text);
  border-radius: 999px;
  padding: 17px 38px;
  cursor: pointer;
  box-shadow: 0 10px 24px -10px rgba(122, 93, 46, .55);
  transition: background .2s, color .2s, box-shadow .2s, transform .15s;
}
.btn:hover { background: var(--gold-lt); border-color: var(--gold-lt); color: var(--ink); box-shadow: 0 14px 28px -10px rgba(122, 93, 46, .5); }
.btn:active { transform: translateY(1px); box-shadow: 0 6px 16px -10px rgba(122, 93, 46, .5); }
.btn:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; }
.btn-outline { background: transparent; color: var(--gold-text); box-shadow: none; }
.btn-outline:hover { background: var(--gold-text); color: var(--cream); }

.swatches { display: flex; justify-content: center; gap: 36px; margin-top: 20px; }
.swatch { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.swatch-color { width: 56px; height: 56px; border-radius: 50%; border: 1px solid var(--gold-pale); display: block; box-shadow: 0 12px 22px -10px rgba(43, 36, 23, .4); }
.swatch span:last-child { font-family: var(--f-caps); font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink); }

.gallery-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 24px; }
/* The outer (left/right) columns' two tiles converge toward each other on
   scroll (see the column-parallax choreography in main.js), each moving up
   to 55px — so worst case they close by up to ~110px combined. 90px of extra
   margin (on top of the 18px grid gap) is sized to cover that worst case
   across realistic viewport heights, verified against the actual scroll math
   rather than guessed — so they never overlap at any scroll position, not
   just at rest. The center column's tiles only diverge (moving apart can
   never overlap), so its gap is simply closed to 0 — touching, not negative;
   a negative margin here would make them overlap permanently at rest, which
   is worse than what we're fixing. 3-column layout only — mobile's 2-column
   grid has no true center column. */
@media (min-width: 641px) {
  /* The center column's top-row tile is the one that opens *upward* (see
   * main.js), by up to 55px — the default 24px gap above the grid isn't
   * enough clearance, so at scroll extremes it rides up into the "A few of
   * our favourite moments" text above. 90px gives it full clearance. */
  .gallery-grid { margin-top: 90px; }
  .gallery-tile:nth-child(4), .gallery-tile:nth-child(6) { margin-top: 100px; }
  .gallery-tile:nth-child(5) { margin-top: -18px; }
}

/* Calendar-flip gallery tile: a true desk-calendar / book-page reveal, hinged
   at the vertical centerline. The base photo always fills the tile exactly
   (no crop-shifting — see .parallax-img below for how the card itself, not
   the photo inside it, is what moves on scroll). When a new photo is due, the
   bottom half is swapped in instantly underneath (.flip-reveal), then
   .flip-flap — the TOP half, carrying the old top half on its front face and
   the new top half on its back — falls forward and down from the centerline
   hinge, like a page turning, landing flush over the bottom half. There is
   always a full image on screen; nothing goes edge-on/blank the way a
   single-axis rotateY swap does. */
.gallery-tile {
  position: relative; aspect-ratio: 3/4; cursor: pointer; overflow: hidden;
  border: 1px solid var(--gold-pale); border-radius: 20px;
  box-shadow: 0 16px 32px -16px rgba(43, 36, 23, .4);
  transition: box-shadow .3s ease; perspective: 2000px;
}
.gallery-tile:hover { box-shadow: 0 20px 40px -14px rgba(43, 36, 23, .45); }
.gallery-tile > picture { display: block; width: 100%; height: 100%; }
/* Scoped to the base picture's own img only (not the descendant combinator
   ".gallery-grid img", which was also matching .flip-slice-img inside the
   flap/reveal overlays and silently overriding their height:200% with 100%
   — since a class+tag selector beats a single class on specificity — cropping
   each half independently at the wrong scale and causing a visible mismatch). */
.gallery-tile > picture > img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .3s ease; }

/* Slice overlays use real <img> + object-fit:cover (not a background-size hack)
   so each photo's own aspect ratio is respected exactly like the base image —
   a background-size:X%/200% stretch was distorting non-square photos and is
   what caused the top/bottom halves to look mismatched. Each slice img is
   sized to the FULL tile height (200% of its half-height container) and
   shifted with translateY so the correct half shows through the clipped window. */
/* .flip-reveal's containing block is the tile (50% = half the tile), but
   .flap-face's containing block is .flip-flap, which is ALREADY 50% of the
   tile — so .flap-face needs height:100% (of its parent) to occupy that same
   half, not 50% (which would resolve to a quarter of the tile and crop the
   slice image far too tight, producing a mismatched, zoomed-in seam). */
/* pointer-events:none on both — the flap sits opaque over the top half even
   at rest, and the reveal layer is still hit-testable despite opacity:0 — so
   without this, real cursor movement over a tile never reaches the base
   img's mousemove listener and the hover-tilt effect never fires. */
.flip-reveal { position: absolute; left: 0; width: 100%; height: 50%; overflow: hidden; pointer-events: none; }
.flap-face { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.flip-slice-img { position: absolute; left: 0; top: 0; width: 100%; height: 200%; object-fit: cover; display: block; }
.flip-reveal .flip-slice-img { transform: translateY(-50%); }
/* The flap's front face sits in the TOP slot at rest, so it shows the top
   half (default, no shift) — but its back face is what the viewer actually
   sees once the flap has fallen and landed over the BOTTOM slot, so its
   content must be the bottom half too, or the image looks visibly wrong
   (top-half content sitting where the bottom half belongs) right as it lands. */
.flap-back .flip-slice-img { transform: translateY(-50%); }

/* .flip-reveal now masks the bottom half with the OLD photo the instant a
   flip starts (appearing instantly via .flip-instant, no fade-in — there's
   nothing to fade from, since it matches what's already showing), then fades
   OUT over its last 180ms so the reveal completes exactly as the flap lands
   (see the timing in main.js) — not fading IN early to show the new photo. */
.flip-reveal { bottom: 0; z-index: 2; opacity: 0; transition: opacity .18s ease-out; }
.flip-reveal.flip-visible { opacity: 1; }
.flip-reveal.flip-instant { transition: none; }
/* Easing borrowed from KokonutUI's shipped Card Flip component (ease-in-out
   momentum), then pulled further at the end (.175→.05) so the flap decelerates
   even more gently into its landing instead of arriving at a steady clip. */
.flip-flap {
  position: absolute; left: 0; top: 0; width: 100%; height: 50%;
  z-index: 3; transform-origin: bottom center; transform-style: preserve-3d;
  transition: transform .5s cubic-bezier(.77, 0, .05, 1);
  pointer-events: none;
}
.flip-flap.flip-active { transform: rotateX(-180deg); }
.flip-flap.flip-instant { transition: none; }
.flap-face { backface-visibility: hidden; }
.flap-back { transform: rotateX(180deg); }

@media (max-width: 640px) {
  .gallery-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
  /* Mobile's 2-column grid has an even column count, so main.js's
     isCenter check (colCount % 2 === 1) is never true here — both columns
     are "closing" columns, same as desktop's outer columns, converging by
     up to 55px each (110px combined) between their row-0 and row-1 tiles.
     Desktop compensates its outer columns' row-1 tiles with 100px extra
     margin-top; mobile needs the same treatment on tiles 3 and 4 (its
     row-1), which previously had no compensation at all — the closing
     gap only had the default 12px grid gap to work with, so the tiles
     visibly overlapped mid-scroll. Row-1/row-2 (tiles 3/4 vs 5/6) don't
     need compensation — both move by the same sign, so their relative
     distance never changes. */
  .gallery-tile:nth-child(3), .gallery-tile:nth-child(4) { margin-top: 100px; }
}

/* lightbox */
.lightbox {
  position: fixed; inset: 0; z-index: 100;
  display: flex; align-items: center; justify-content: center;
  background: rgba(20, 16, 8, 0.92);
  opacity: 0; pointer-events: none;
  transition: opacity .25s ease;
}
.lightbox.open { opacity: 1; pointer-events: auto; }
.lightbox img { max-width: min(92vw, 1100px); max-height: 88vh; object-fit: contain; border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,.5); }
.lightbox-close {
  position: absolute; top: 24px; right: 32px;
  font-family: var(--f-caps); font-size: 13px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--cream); background: transparent; border: 1px solid var(--gold-pale);
  border-radius: 999px;
  padding: 10px 20px; cursor: pointer;
}
.lightbox-close:hover { background: var(--gold-text); border-color: var(--gold-text); }
.lightbox-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  font-family: var(--f-caps); font-size: 20px; color: var(--cream);
  background: transparent; border: 1px solid var(--gold-pale);
  border-radius: 50%;
  width: 46px; height: 46px; cursor: pointer;
}
.lightbox-nav:hover { background: var(--gold-text); border-color: var(--gold-text); }
.lightbox-prev { left: 24px; }
.lightbox-next { right: 24px; }
@media (max-width: 640px) {
  .lightbox-nav { width: 40px; height: 40px; font-size: 16px; }
  .lightbox-prev { left: 8px; }
  .lightbox-next { right: 8px; }
  .lightbox-close { top: 14px; right: 14px; padding: 8px 14px; font-size: 11px; }
}

/* map modal — reuses .lightbox's overlay/close-button styling, swaps the
   centered <img> for a panel (embedded map + two Google Maps CTAs) */
.map-modal-panel {
  width: min(92vw, 720px);
  background: var(--cream);
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 20px 60px rgba(0, 0, 0, .5);
}
.map-modal-frame { width: 100%; height: min(60vh, 440px); border: 0; display: block; }
.map-modal-actions { display: flex; justify-content: center; gap: 16px; flex-wrap: wrap; padding: 24px; }

.faq-list { text-align: left; margin-top: 8px; }
.faq-item { border-bottom: 1px solid var(--gold-pale); }
.faq-q {
  width: 100%;
  background: none;
  border: none;
  font-family: var(--f-caps);
  font-size: 16px;
  letter-spacing: .03em;
  color: var(--ink);
  text-align: left;
  padding: 24px 4px;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.faq-chevron { color: var(--gold); transition: transform .25s ease; }
.faq-item.open .faq-chevron { transform: rotate(180deg); }
.faq-a {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .3s ease;
}
.faq-item.open .faq-a { grid-template-rows: 1fr; }
.faq-a-inner { overflow: hidden; }
.faq-a p { font-family: var(--f-body); font-size: 17px; line-height: 1.7; color: var(--sage); padding-bottom: 22px; }

.gifts-note { font-family: var(--f-caps); font-size: 11px; letter-spacing: .2em; text-transform: uppercase; color: var(--gold-text); margin-bottom: 18px; }
.gift-row { display: flex; justify-content: center; gap: 44px; flex-wrap: wrap; }
.gift-person {
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  background: var(--cream); border: 1px solid var(--gold-pale); border-radius: 20px;
  padding: 40px 52px; box-shadow: 0 14px 28px -16px rgba(43, 36, 23, .35);
}
.gift-icon { width: 30px; height: 30px; color: var(--gold); }
.gift-name { font-family: var(--f-caps); font-size: 15px; letter-spacing: .18em; text-transform: uppercase; }
/* Gifting reads bigger than its neighbors — larger intro type and a divider
   vine (reusing the same motif as Our Story) between the copy and the cards,
   per user request to give this section more presence, not just more padding. */
#gifts .story-text { font-size: 27px; }
#gifts .divider { margin: 8px auto 34px; }
/* Tap a gift number to copy it — the number slides up and out while "Copied"
   springs in from below to replace it, then both reverse after a couple of
   seconds. Two stacked layers (not a text-content swap) so the size never
   jumps and the transition has something to animate between. */
.gift-number {
  position: relative; display: inline-block; overflow: hidden;
  font-family: var(--f-display); font-style: italic; font-size: 30px; color: var(--sage);
  background: none; border: none; padding: 2px 4px; margin: -2px -4px; cursor: pointer;
  font: inherit; line-height: 1;
}
.gift-number-text, .gift-number-copied {
  display: block;
  /* Exponential ease-out — fast start, smooth settle, no overshoot — to match
     the rest of the site's motion language (the flip's easing was deliberately
     chosen without bounce for the same reason: it reads as elegant rather
     than a springy/tacky pop). */
  transition: transform .4s cubic-bezier(.16, 1, .3, 1), opacity .3s ease;
}
.gift-number-copied {
  position: absolute; inset: 2px 4px; opacity: 0; transform: translateY(100%);
  color: var(--gold-text);
}
.gift-number.copied .gift-number-text { opacity: 0; transform: translateY(-100%); }
.gift-number.copied .gift-number-copied { opacity: 1; transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .gift-number-text, .gift-number-copied { transition: opacity .2s ease; transform: none; }
}

.rsvp-form { display: flex; flex-direction: column; gap: 22px; margin-top: 12px; text-align: left; }
.rsvp-form label {
  font-family: var(--f-caps);
  font-size: 12px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--gold-text);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.rsvp-form input, .rsvp-form textarea {
  font-family: var(--f-body);
  font-size: 17px;
  color: var(--ink);
  background: var(--cream);
  border: 1px solid var(--gold-pale);
  border-radius: 10px;
  padding: 13px 15px;
  box-shadow: inset 0 1px 3px rgba(43, 36, 23, .08);
  resize: vertical;
  transition: box-shadow .2s, border-color .2s;
}
.rsvp-form input:focus, .rsvp-form textarea:focus { outline: none; border-color: var(--gold); box-shadow: inset 0 1px 3px rgba(43, 36, 23, .08), 0 0 0 3px rgba(154, 123, 60, .15); }
.rsvp-form .btn { align-self: center; margin-top: 8px; border: none; }
.rsvp-status { text-align: center; font-family: var(--f-body); font-size: 15px; min-height: 20px; }
.rsvp-status.ok { color: var(--sage); }
.rsvp-status.err { color: #a33; }

/* Footer reveal: the footer is position:sticky;bottom:0 with NO margin
   overlap trick (a margin sized to cancel the footer's own height also
   cancels 100% of the scroll room the reveal needs — see git history/prior
   comment). A sticky element snaps straight to its pinned position the
   instant it's eligible instead of sliding in continuously, and that snap is
   what reads as "already there, being revealed" as RSVP scrolls up and off.
   z-index IS still needed though: sticky's pinned position can visually
   overlap RSVP's own box mid-transition (RSVP scrolled most, not all, of the
   way past) even with no *document-flow* overlap between them, and without
   z-index the footer — later in DOM order — paints over RSVP's content
   during that overlap instead of staying behind it.
   .footer-reveal-wrap matters: position:sticky pins relative to the nearest
   positioned ancestor's box, not just "near the bottom of the page" —
   without this wrapper, that ancestor is body (the full ~7000px document),
   so the footer would snap to the viewport bottom from page load, bleeding
   into the hero, instead of only engaging once you scroll into this region.
   #rsvp's min-height:100vh just gives the reveal room to play out — without
   it RSVP (a short form) could be shorter than the viewport, leaving no
   scroll distance for the reveal to happen across. */
.footer-reveal-wrap { position: relative; }
.footer { position: sticky; bottom: 0; z-index: 0; text-align: center; padding: 60px 24px; background: var(--ink); color: var(--gold-pale); }
#rsvp { position: relative; z-index: 1; min-height: 65vh; display: flex; align-items: center; justify-content: center; background-color: var(--cream); }
.footer-names { font-family: var(--f-script); font-style: italic; font-size: 38px; margin-bottom: 8px; }
.footer-date { font-family: var(--f-caps); font-size: 12px; letter-spacing: .2em; text-transform: uppercase; }
.footer-credit { font-family: var(--f-body); font-size: 13px; color: var(--gold-pale); margin-top: 28px; opacity: .8; }
.footer-credit a { color: var(--gold-pale); text-decoration: underline; text-underline-offset: 3px; transition: color .2s; }
.footer-credit a:hover { color: var(--cream); }

/* scroll reveal — varied per section instead of one identical treatment throughout */
.reveal { opacity: 0; }
.reveal-up { transform: translateY(30px); }
.reveal-scale { transform: scale(0.94); }
.reveal-left { transform: translateX(-40px); }
.reveal-right { transform: translateX(40px); }

@media (max-width: 640px) {
  /* keep it to a simple fade on small screens — side-travel reveals feel cramped on narrow viewports */
  .reveal-left, .reveal-right { transform: translateY(20px); }
}

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; }
  html { scroll-behavior: auto; }
}

/* gentle continuous pulse on the divider heart — ties the page's motion language together */
.divider-heart { display: inline-block; animation: heart-pulse 3.2s ease-in-out infinite; }
@keyframes heart-pulse {
  0%, 100% { transform: scale(1); opacity: 0.85; }
  50% { transform: scale(1.15); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .divider-heart { animation: none; }
}

/* gallery parallax depth */
.gallery-grid { position: relative; }
.parallax-img { will-change: transform; }

/* ── Preloader (js/main.js) ──
   Shows from the very first paint (plain CSS, no JS needed to display it)
   and is hidden once the real page has loaded, straight to the hero. */
#preloader {
  position: fixed; inset: 0; z-index: 10000;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px;
  background: var(--cream);
  transition: opacity .5s ease;
}
#preloader.hide { opacity: 0; pointer-events: none; }
.preloader-heart { width: 40px; height: 36px; color: var(--gold); }
.preloader-heart path {
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-dasharray: 210; stroke-dashoffset: 210;
  animation: preloader-draw 1.6s ease-in-out infinite alternate;
}
@keyframes preloader-draw { to { stroke-dashoffset: 0; } }
.preloader-initials { font-family: var(--f-script); font-style: italic; font-size: 32px; color: var(--gold-text); }
@media (prefers-reduced-motion: reduce) {
  .preloader-heart path { animation: none; stroke-dashoffset: 0; }
}
